Skip to content

[high] fix(binary): request JSON from capa and FLOSS with the flag they define - #116

Draft
elhoim wants to merge 1 commit into
calebevans:mainfrom
elhoim:fix/flare-json-flags
Draft

[high] fix(binary): request JSON from capa and FLOSS with the flag they define#116
elhoim wants to merge 1 commit into
calebevans:mainfrom
elhoim:fix/flare-json-flags

Conversation

@elhoim

@elhoim elhoim commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

BLUF

  • Priority: high.
  • run_capa and run_floss never work. Both pass --format json, which neither tool accepts, so argparse rejects the command line before the sample is opened.
  • In both tools -f/--format selects the input format; JSON output is -j/--json. Verified against the pinned releases: capa exits 2, FLOSS exits 255.
  • FLOSS has a second defect on the same line: the bare --only emitted when include_static=False is nargs="+" with choices, so it swallows the sample pathinvalid choice: 'sample.bin'.
  • Fix: use --json for both; use --no static instead of a bare --only; move the positional sample ahead of any list-valued flag.
  • Scope: src/mulder/server/tools/binary.py, the two cmd lists only. No shared helper, no new abstraction, nothing else in the module.

The bug

    cmd = [capa_bin, "--format", "json", "--quiet"]
    cmd = [
        floss_bin,
        "--format",
        "json",
        "--minimum-length",
        str(minimum_length),
    ]
    if not include_static:
        cmd.append("--only")
    cmd.append(str(target))

Run against the pinned binaries (capa 9.4.0, floss v3.1.0-0-gdb9af41, fetched from the URLs in assets/manifest.py):

$ ./capa --format json --quiet sample.bin
capa: error: argument -f/--format: invalid choice: 'json'
  (choose from 'auto', 'pe', 'dotnet', 'elf', 'sc32', 'sc64', 'cape',
   'drakvuf', 'vmray', 'freeze', 'binexport2', 'binja_database')
exit 2

$ ./floss --format json --minimum-length 4 --only sample.bin
floss: error: argument -f/--format: invalid choice: 'json'
  (choose from 'auto', 'pe', 'sc32', 'sc64')
exit 255

And with the format flag corrected, the bare --only still eats the sample:

$ ./floss --json --minimum-length 4 --only sample.bin
floss: error: argument --only: invalid choice: 'sample.bin'
  (choose from 'static', 'stack', 'tight', 'decoded')
exit 255

The fix

    # -f/--format selects capa's *input* format (auto/pe/elf/sc32/...); JSON
    # output is -j/--json. Passing "json" to --format is an invalid choice and
    # capa exits 2 from argparse before it opens the sample.
    cmd = [capa_bin, "--json", "--quiet"]
    cmd = [
        floss_bin,
        "--json",
        "--minimum-length",
        str(minimum_length),
        # The sample must precede --no/--only: both are nargs="+" with
        # `choices`, so argparse otherwise consumes the sample path as one of
        # their values and exits 2 on an invalid choice.
        str(target),
    ]
    if not include_static:
        # Skipping static extraction is "--no static"; a bare "--only" is not
        # a valid invocation, since --only requires at least one analysis type.
        cmd.extend(["--no", "static"])

With the corrected argv, capa --json --quiet sample.bin exits 0, and FLOSS gets past argparse into INFO: floss: extracting static strings — the line that only appears once argument parsing has succeeded.

Why capa and FLOSS are one PR, not two

One root cause, one module, one mechanical correction: both are FLARE tools whose --format is an input-format selector, and both were given json as if it were an output format. Splitting this into two near-identical two-line PRs would obscure that they are the same mistake, and the FLOSS half cannot be verified without establishing the same grammar fact for capa. The --only/--no correction is part of building a FLOSS command line that parses at all, so it belongs in the same change.

Deliberately out of scope

Verification

  • uvx pre-commit run --all-files (new test staged first, so the hooks actually see it) → ruff, ruff-format, mypy all pass.
  • uv run --locked --extra dev pytest tests/ -q863 passed, nothing deselected, nothing skipped.
  • Discriminating check — with binary.py restored to origin/main and the new tests kept, 6 of 8 fail:
FAILED test_capa_does_not_pass_json_as_an_input_format
  - AssertionError: --format json is an invalid choice and capa exits 2
FAILED test_capa_requests_json_with_the_json_flag
  - AssertionError: assert ('--json' in ['/usr/bin/tool', '--format', 'json', ...
FAILED test_floss_does_not_pass_json_as_an_input_format
  - AssertionError: --format json is an invalid choice and floss exits 255
FAILED test_floss_requests_json_with_the_json_flag
  - AssertionError: assert ('--json' in ['/usr/bin/tool', '--format', 'json', ...
FAILED test_floss_never_leaves_only_or_no_without_an_analysis_type
  - AssertionError: --only is followed by '/tmp/.../sample.bin', which argparse
    would reject as an invalid choice (include_static=False)
FAILED test_floss_puts_the_sample_before_any_list_valued_flag
  - AssertionError: --only precedes the sample; argparse would consume the sample path
6 failed, 2 passed

The two that pass on both trees are the narrowness guards — the sample is still passed exactly once, and --minimum-length is unchanged — so they pin the fix rather than the bug. The tests assert on the argv actually built and check it against the real choices tuples from each pinned release, so they cannot pass against a codebase where the bug never existed.

Context

Recovered from closed PR #69 (fix/tool-cli-invocations), which changed every wrapped tool at once. Only the binary.py portion is here; the chainsaw/hayabusa/zircolite parts of that branch are separate tools and are not included. The rejected outcome framework and its classify_tool_exit helper are not reintroduced — this PR adds no status taxonomy and no shared abstraction. Per the review on #32:

focused fixes for specific tools that currently swallow failures or lose partial-result information would be welcome.

Branched fresh from current main (2e5432c); the closed branch was not revised in place.

🤖 Generated with Claude Code

Both wrappers passed "--format json". In capa 9.4.0 and FLOSS 3.1.0 alike,
-f/--format selects the tool's *input* format -- capa accepts auto|pe|dotnet|
elf|sc32|sc64|cape|drakvuf|vmray|freeze|binexport2|binja_database and FLOSS
accepts auto|pe|sc32|sc64 -- while JSON output is -j/--json. "json" is not a
valid choice for either, so argparse rejected the command line before the
sample was opened: capa exited 2 and FLOSS exited 255. Every run_capa and
run_floss call failed.

FLOSS had a second defect on the same command line. --only and --no are both
nargs="+" with choices, so the bare "--only" emitted when include_static is
False consumed the positional sample path as one of its values and failed with
"invalid choice: 'sample.bin'". Skipping static extraction is "--no static",
and the sample has to precede any list-valued flag.

Verified against the pinned release binaries: capa 9.4.0 and floss
v3.1.0-0-gdb9af41, downloaded from the URLs in assets/manifest.py.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant