Skip to content

feat(attack-path): add a native complex file finding type (#6647) - #7016

Merged
Sébastien MIGUEL (Seb-MIGUEL) merged 6 commits into
mainfrom
feat/attack-path-file-finding-type
Jul 30, 2026
Merged

Sébastien MIGUEL (Seb-MIGUEL) merged 6 commits into
mainfrom
feat/attack-path-file-finding-type

Conversation

@Seb-MIGUEL

@Seb-MIGUEL Sébastien MIGUEL (Seb-MIGUEL) commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Adds a native file finding type end to end and surfaces it in the attack path, so files discovered on SMB shares (NetExec spider_plus) become first-class findings.

Completes the kill-chain: nmap → 445 → SMB → enum shares (share) → spider_plus (file on that share) — e.g. a secret.ps1 on SYSVOL holding a hard-coded password shows up as a file finding you can pivot from.

Self-contained: the interim share → file stand-in removal (formerly #6999, now closed) is already folded into this PR. Targets main.

Why complex, not primitive

The same basename means different things by location. A file is a complex finding carrying file_name + path + share (empty for local files, e.g. a future FTP/NFS --ls) + host. Its stored value is the full location (a unique dedup key, so two secret.ps1 never merge) while the front renders only the basename; the share sub-field reuses PrimitiveType.ShareName so a share-hosted file links back to its share finding.

Changes

Model / chaining — ContractOutputType.File, PrimitiveType.FileName + FilePath, ComplexType.File; registered complex with its sub-field → primitive map (file_name→FileName, path→FilePath, share→ShareName, host, asset_id).

API — FileOutputProcessor (complex, mirrors ShareOutputProcessor): validate requires file_name; toFindingValue builds \\host\share\dir\file for share files, a host-prefixed path for local files. AttackPathGraphService gains a files counter + files drawer category (mirrors shares); AttackPathKeyMatcher reconciles file_name → file; AttackPathCounters gains files.

Front — maskFindingValue shows a file as its basename (full path in the detail panel); "Captured Files" card + files filter/category; ContractOutputElementType.file; api-types (AttackPathCounters.files, "file" in the type unions); i18n across all nine locales.

Docs — updated the add-contract-output-type skill: a located file is complex (not primitive), and the JSON-metadata output path used by spider_plus.

Tests (validated locally)

  • FileOutputProcessorTest (10) — validate + value formatting (UNC / local / same-name-distinct-by-share).
  • ChainingTypeRegistryTest — File resolves complex, sub-fields → primitives incl. share→ShareName.
  • AttackPathShareTypeApiTest — extended: share and file each count under their own name; file on graph node / collapsed counts / files counter.
  • Front: maskFindingValue('file') → basename, FILTER_TO_FINDING_TYPES.files, findingCategoryNoun('file'). check-ts + eslint + vitest green.

Dependencies

Behind the ATTACK_PATH feature flag.

@github-actions

Copy link
Copy Markdown
Contributor

/review docs

📖 Docs Reviewer triggered automatically.

27 files changed — 22 functional, 0 documentation.

⚠️ No documentation files modified. Agent will check if doc updates are needed.

@laugiov

Copy link
Copy Markdown
Contributor

Two things before merge:

Heads-up: the rebase will hit the just-merged #7017 (complex-finding sub-field matcher), which still carries the interim share_name -> file that your PR moves to native share/file. That matcher is my area, I'll pair with you on the reconciliation so nothing regresses.

@Seb-MIGUEL

Copy link
Copy Markdown
Contributor Author

Thanks Laurent.

Rather than push that auto-merged resolution, I'd like to take you up on the pairing offer: I'll resolve the two front conflicts, and we settle AttackPathKeyMatcher / AttackPathGraphService together (target state of the reconcile map + confirm your sub-field matcher handles the native share/file types). How do you want to do it — you push the intended matcher state and I rebase on top, or we go through it live?

@laugiov

Copy link
Copy Markdown
Contributor

Thanks Seb, you're right that the matcher is the real silent-regression risk. I dug into both backend auto-merges and they're not equal:

  • AttackPathGraphService, the auto-merge is actually correct, keep it. Your presentType removal and feat(chaining): match complex-finding sub-fields for kill-chain events (#6647) #7017's applyEventDependencies/matchedFindingIds are in disjoint regions, so they merge cleanly, and once findings are native they flow straight into matchedFindingIds, which is what we want. Don't reset it to main (that'd re-add the bridge you're removing); the tests will confirm it.
  • AttackPathKeyMatcher, this is the one, leave it to me. The auto-merge takes your share_name->share but keeps feat(chaining): match complex-finding sub-fields for kill-chain events (#6647) #7017's SUBFIELDS["file"]={share_name->…}, so share_name EQ stops extracting from share findings and starts spuriously matching file basenames. I'll push the reconciled matcher + tests so it's proven, not assumed:
    • map -> your target (share_name->share, file_name->file, password->credentials) OK
    • SUBFIELDS: move the share_name extractor from file to share; add file_name -> basename under file
    • update AttackPathKeyMatcherTest + the kill-chain graph test to native share/file

So: you take the two front files + keep the graph-service auto-merge; I own the matcher + its tests.

One thing to confirm: your registry maps file.share -> ShareName, so I could let share_name also match file findings. I'd scope the graph matcher to share_name -> {share} only (a file isn't a share, otherwise spurious edges); the file->ShareName link stays a runtime-chaining concern. Good with that?

@Seb-MIGUEL

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and force-pushed — tip is now 057376d3a (was 303d30b5f). Ready for your matcher commit whenever.

What I took (front) + the graph-service auto-merge

  • SimulationAttackPath.tsx — the only real conflict was the import block: main added Fullscreen* / IconButton / Pagination, my branch added Popover / alpha and re-added InsertDriveFileOutlined. Resolved to the union (checked each symbol is actually used, no dead import).
  • AttackPathGraphService.java — kept the auto-merge exactly as you called it, did not reset to main.

Heads-up: the auto-merge silently corrupted one of my front tests — the same risk you flagged, but on the front side

  • The hub test (collapses N findings … single causal edge) auto-merged my share fixture with main's file assertions (toHaveLength(3) + NODE_FINDING|file| source). Restored to the intended native-share assertions (toHaveLength(1), NODE_FINDING|share| source).
  • Main's two new front tests — collapses more than 4 … cluster and anchors the causal edge on the resolved producer — were written against the interim share_name → file bridge (file producers consumed by share_name IS_NOT_NULL). The merged front helper reconciles share_name → share only, so I migrated both fixtures from file to share; the tests stay, they just exercise the clustering/anchoring under the native type now.

Didn't touch AttackPathKeyMatcher / AttackPathKeyMatcherTest — all yours.

On the share_name scope question — yes, agreed: scope the graph matcher to share_name → {share} only. A file isn't a share, and matching file findings on share_name would re-introduce exactly the spurious cross-type edges this PR removes. The file.share → ShareName registry link stays a runtime-chaining concern, out of the graph matcher. FWIW the front already reflects this — KEYTYPE_TO_FINDING_TYPE maps share_name → 'share' only — which is why the two front tests above had to move to native share.

One caveat: I couldn't run the front suite locally in this worktree (its node_modules carries the WSL/Linux native bindings, so a Windows vitest can't load @rolldown/binding-win32-x64-msvc) — the front resolution is static-analysis-backed and I'm leaning on CI. Backend matcher tests will be red until your commit lands, as expected.

@Seb-MIGUEL
Sébastien MIGUEL (Seb-MIGUEL) force-pushed the feat/attack-path-file-finding-type branch 2 times, most recently from 31d099c to 3bddb4a Compare July 29, 2026 11:34
@codecov

codecov Bot commented Jul 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.83051% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.44%. Comparing base (57ce8b0) to head (8632d90).

Files with missing lines Patch % Lines
...enaev/service/attackpath/AttackPathKeyMatcher.java 57.14% 1 Missing and 2 partials ⚠️
...simulation/attack_path/attack-path-flow-helpers.ts 50.00% 1 Missing and 1 partial ⚠️
.../openaev/output_processor/FileOutputProcessor.java 96.00% 0 Missing and 1 partial ⚠️

❌ Your project check has failed because the head coverage (7.48%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #7016      +/-   ##
============================================
- Coverage     44.48%   44.44%   -0.05%     
+ Complexity     9628     9626       -2     
============================================
  Files          2733     2734       +1     
  Lines         83446    83450       +4     
  Branches      12815    12827      +12     
============================================
- Hits          37125    37089      -36     
- Misses        43552    43569      +17     
- Partials       2769     2792      +23     
Flag Coverage Δ
backend 67.73% <91.83%> (-0.03%) ⬇️
e2e 16.93% <ø> (-0.93%) ⬇️
frontend 7.48% <80.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Introduce a real `file` finding type end to end, replacing the interim
share->file stand-in removed in the previous change. A file is a complex
finding (not a primitive): the same basename means different things by
location, so it carries file_name + path + share (empty for local files) +
host. Its stored value is the full location (a unique dedup key) while the
front shows only the basename; the `share` sub-field reuses ShareName so a
file discovered on a share links back to its share finding.

Model / chaining:
- ContractOutputType.File, PrimitiveType.FileName + FilePath, ComplexType.File.
- Register File as complex and map its sub-fields (file_name -> FileName,
  path -> FilePath, share -> ShareName, host, asset_id).

API:
- FileOutputProcessor (complex, mirrors ShareOutputProcessor): validate
  requires file_name; toFindingValue builds the UNC path for share files
  (\\host\share\dir\file) or a host-prefixed path for local files.
- AttackPathGraphService: a `files` counter and a "files" drawer category,
  mirroring shares; AttackPathKeyMatcher reconciles file_name -> file.
- AttackPathCounters gains a `files` field.

Front:
- maskFindingValue shows a file as its basename (full path kept in the detail
  panel); "Captured Files" card + files filter/category; ContractOutputElementType.file.
- api-types regenerated inputs (AttackPathCounters.files, "file" in the type unions),
  i18n key across all nine locales.

Docs: update the add-contract-output-type skill (a located file is complex, and
the JSON-metadata output path for spider_plus).

Requires the file output type in client-python and the spider_plus producer in
the netexec injector.
… findings (#6647)

Autolearn on the add-contract-output-type skill: a registry entry only makes
the ContractOutputElement appear on the contract; findings are produced only
when an extractor populates the field (e.g. netexec `text` is declared but has
no extractor, so it yields no findings).
@Seb-MIGUEL
Sébastien MIGUEL (Seb-MIGUEL) force-pushed the feat/attack-path-file-finding-type branch from 724fcd2 to 8632d90 Compare July 30, 2026 07:18
@Seb-MIGUEL
Sébastien MIGUEL (Seb-MIGUEL) merged commit 6d30c74 into main Jul 30, 2026
38 checks passed
@Seb-MIGUEL
Sébastien MIGUEL (Seb-MIGUEL) deleted the feat/attack-path-file-finding-type branch July 30, 2026 07:44
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.

2 participants