Skip to content

[high] fix(chainsaw): pass the --mapping that hunt mode's --sigma requires - #113

Draft
elhoim wants to merge 1 commit into
calebevans:mainfrom
elhoim:fix/chainsaw-hunt-mapping
Draft

[high] fix(chainsaw): pass the --mapping that hunt mode's --sigma requires#113
elhoim wants to merge 1 commit into
calebevans:mainfrom
elhoim:fix/chainsaw-hunt-mapping

Conversation

@elhoim

@elhoim elhoim commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

BLUF

  • Priority: high.
  • Chainsaw hunt mode can never execute on current main. It is Chainsaw's Sigma detection engine — the whole reason the tool is wrapped — and every invocation is rejected by argument parsing before a single log is opened.
  • Root cause: _run_chainsaw_hunt passes -s <sigma rules> but never --mapping. Chainsaw declares --mapping a hard requirement of --sigma.
  • The requirement is invisible in chainsaw hunt --help, which lists --mapping as an ordinary option — it is enforced only at parse time. That is why the defect survived review.
  • Fix: resolve the mapping the chainsaw asset already ships and pass it with --sigma; report a missing mapping as file_not_found instead of letting it surface as an opaque exit 2; add a mapping_path override mirroring the existing sigma_rules_path.
  • Scope: src/mulder/server/tools/chainsaw.py, hunt mode only. No shared helper, no new abstraction, no change to search/srum/timeline.

The bug

    output_file = output_dir / "chainsaw_hunt.json"
    cmd = [
        binary,
        "hunt",
        str(evidence_path),
        "-s",
        str(sigma_rules_path),
        "--json",
        "--output",
        str(output_file),
    ]

Verified empirically against the pinned release — I downloaded chainsaw_x86_64-unknown-linux-gnu.tar.gz for v2.16.0 (the version in manifest.py) and ran mulder's exact argv:

$ ./chainsaw --version
chainsaw 2.16.0

$ ./chainsaw hunt ./evidence -s ./sigma --json --output out.json
error: the following required arguments were not provided:
  --mapping <MAPPING>

Usage: chainsaw hunt --mapping <MAPPING> --sigma <SIGMA> --json --output <OUTPUT> <RULES> [PATH]...
(exit 2)

Adding the mapping is also sufficient — the same command line then runs, and Chainsaw resolves mulder's positional layout correctly:

$ ./chainsaw hunt ./evidence -s ./sigma -m mappings/sigma-event-logs-all.yml --json --output out.json
[+] Loading detection rules from: ./sigma
[+] Loaded 1 detection rules
[+] Loading forensic artefacts from: ./evidence (extensions: .evt, .evtx)

(It then exits 1 only because my throwaway evidence directory was empty.)

The fix

def _default_chainsaw_mapping() -> Path:
    """The Sigma-to-EVTX mapping Chainsaw requires alongside ``--sigma``."""
    return asset_display_path("chainsaw", "mappings", "sigma-event-logs-all.yml")
        "-s",
        str(sigma_rules_path),
        "--mapping",
        str(mapping_path),

plus a pre-flight so a missing asset is legible:

    mapping = Path(mapping_path) if mapping_path else _default_chainsaw_mapping()

    if mode == "hunt" and not mapping.exists():
        return error_response(
            tc_id, "run_chainsaw", params,
            f"Chainsaw Sigma mapping not found: {mapping}",
            error_type="file_not_found",
            suggestion=(
                "Run 'mulder setup' (provisions Chainsaw 2.16.0 and its "
                "mappings/ directory), or pass mapping_path explicitly."
            ),
        )

No new asset is required. mappings/sigma-event-logs-all.yml ships inside the release tarball (strip_components=1 lands it in the asset dir) and is also listed in the asset's supplement_paths=("mappings", "rules").

The mapping_path parameter mirrors the existing sigma_rules_path and exists for a concrete reason: _chainsaw_binary() deliberately lets a PATH install win ("so a SIFT/apt/cargo install keeps being used"), and such an install keeps its mappings elsewhere. Without an override, the pre-flight would hard-block exactly the users that docstring protects.

Deliberately out of scope

  • sigma-event-logs-legacy.yml. Chainsaw ships two mappings; -all is the general one. Selecting per-log-format is a separate concern — the new mapping_path parameter is the escape hatch in the meantime.
  • Surfacing the failure. Open PR [high] fix(chainsaw): report a failed Chainsaw run instead of a clean scan #95 (fix/chainsaw-exit-code) makes a failed Chainsaw run report an error instead of a clean scan. The two are complementary and independent: [high] fix(chainsaw): report a failed Chainsaw run instead of a clean scan #95 makes this failure visible, this PR removes its cause. Neither depends on the other, and both branch off main.
  • The srum invocation has its own separate CLI defects; those are a different PR against a different subcommand.
  • I noticed while testing that chainsaw analyse srum exits 0 even when it fails to parse the database, so a returncode guard alone will not catch that case. Noting it; not addressed here.

Verification

  • uvx pre-commit run --all-files (new test staged first) → ruff, ruff-format, mypy all pass.
  • uv run --locked --extra dev pytest tests/ -q862 passed, nothing deselected, nothing skipped.
  • Discriminating check — with chainsaw.py restored to origin/main and the new tests kept, 5 of 6 fail:
FAILED test_hunt_passes_the_mapping_that_sigma_requires - AssertionError: chainsaw 2.16.0 exits 2 on this argv: ['/usr/bin/chainsaw', ...]
FAILED test_the_mapping_accompanies_the_sigma_flag - AssertionError: assert '--mapping' in ['/usr/bin/chainsaw', 'hunt', '/tmp/p...
FAILED test_the_default_mapping_is_one_the_chainsaw_asset_provides - AssertionError: the mapping was not resolved from the chainsaw asset; looku...
FAILED test_a_missing_mapping_is_reported_not_left_to_clap - AssertionError: assert 'os_error' == 'file_not_found'
FAILED test_the_resolver_exists_and_names_the_shipped_mapping - AssertionError: _default_chainsaw_mapping is missing
5 failed, 1 passed

The tests assert on the parsed argv, not on a mock call count, and they deliberately avoid importing the new resolver at module scope so the failure is the missing flag rather than an ImportError. test_timeline_mode_does_not_get_a_mapping passes against both trees — it pins the fix's narrowness rather than the bug.

  • Three tests in tests/test_binary_resolution.py now provision the mapping. They assert on binary resolution and none of them asserts anything about mappings; the mapping is incidental setup needed to reach subprocess.run at all. Nothing they check was weakened.

Context

Recovered from closed PR #69, which changed the CLI contract of every wrapped tool at once. This is one tool, one subcommand, one flag. The rejected outcome framework is not reintroduced and there is no classify_tool_exit or second status taxonomy here, 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

_run_chainsaw_hunt built `chainsaw hunt <evidence> -s <sigma> --json
--output <file>`. Chainsaw declares --mapping as a hard requirement of
--sigma, so clap rejects that command line before Chainsaw opens a single
log. Verified against the pinned release, chainsaw 2.16.0:

    $ chainsaw hunt ./evidence -s ./sigma --json --output out.json
    error: the following required arguments were not provided:
      --mapping <MAPPING>
    (exit 2)

The requirement is invisible in `chainsaw hunt --help`, which lists
--mapping as an ordinary option; it is enforced only at parse time. Hunt
mode is Chainsaw's Sigma engine, so it could never execute.

Resolve the mapping the chainsaw asset already ships
(mappings/sigma-event-logs-all.yml, present in the release tarball and in
the asset's supplement_paths) and pass it alongside --sigma. A missing
mapping is now reported as file_not_found with a 'mulder setup'
suggestion rather than left to surface as an opaque exit 2, and a new
mapping_path parameter mirrors sigma_rules_path so a PATH-installed
chainsaw can point at its own mappings.

Three tests in test_binary_resolution.py now provision the mapping as
incidental setup; they assert on binary resolution and are unchanged in
what they check.

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