attackmap 0.4.10__tar.gz → 0.4.21__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of attackmap might be problematic. Click here for more details.

Files changed (116) hide show
  1. {attackmap-0.4.10/src/attackmap.egg-info → attackmap-0.4.21}/PKG-INFO +169 -28
  2. {attackmap-0.4.10 → attackmap-0.4.21}/README.md +168 -27
  3. {attackmap-0.4.10 → attackmap-0.4.21}/pyproject.toml +1 -1
  4. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/__init__.py +1 -1
  5. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/analyzers.py +37 -18
  6. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/anomalies.py +214 -8
  7. attackmap-0.4.21/src/attackmap/authz.py +451 -0
  8. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/cli.py +210 -39
  9. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/cve.py +11 -1
  10. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/exploitability.py +3 -1
  11. attackmap-0.4.21/src/attackmap/hunt_harness.py +451 -0
  12. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/llm_review.py +36 -7
  13. attackmap-0.4.21/src/attackmap/lockfiles.py +387 -0
  14. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/models.py +45 -7
  15. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/review_prompts.py +233 -0
  16. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/sbom.py +31 -3
  17. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/scanner.py +81 -5
  18. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/taint.py +330 -47
  19. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/threat_model.py +93 -7
  20. attackmap-0.4.21/src/attackmap/triage.py +151 -0
  21. {attackmap-0.4.10 → attackmap-0.4.21/src/attackmap.egg-info}/PKG-INFO +169 -28
  22. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap.egg-info/SOURCES.txt +7 -0
  23. attackmap-0.4.21/tests/test_anomalies.py +663 -0
  24. attackmap-0.4.21/tests/test_authz.py +472 -0
  25. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_cve.py +91 -0
  26. attackmap-0.4.21/tests/test_hunt_harness.py +558 -0
  27. attackmap-0.4.21/tests/test_lockfiles.py +348 -0
  28. attackmap-0.4.21/tests/test_recall.py +310 -0
  29. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_scanner.py +79 -0
  30. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_taint.py +119 -0
  31. attackmap-0.4.21/tests/test_triage.py +201 -0
  32. attackmap-0.4.10/src/attackmap/authz.py +0 -196
  33. attackmap-0.4.10/tests/test_anomalies.py +0 -397
  34. attackmap-0.4.10/tests/test_authz.py +0 -241
  35. {attackmap-0.4.10 → attackmap-0.4.21}/LICENSE +0 -0
  36. {attackmap-0.4.10 → attackmap-0.4.21}/setup.cfg +0 -0
  37. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/analyzer.py +0 -0
  38. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/analyzer_contracts.py +0 -0
  39. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/asset_model.py +0 -0
  40. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/attack_taxonomy.py +0 -0
  41. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/config_scanner.py +0 -0
  42. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/context_pack.py +0 -0
  43. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/control_model.py +0 -0
  44. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/crypto.py +0 -0
  45. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/defensive_review.py +0 -0
  46. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/detection_opportunities.py +0 -0
  47. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/diagrams.py +0 -0
  48. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/diff.py +0 -0
  49. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/graph.py +0 -0
  50. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/insights.py +0 -0
  51. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/merge.py +0 -0
  52. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/progress.py +0 -0
  53. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/recon_models.py +0 -0
  54. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/recon_to_analysis.py +0 -0
  55. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/report.py +0 -0
  56. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/review_eval.py +0 -0
  57. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/review_json.py +0 -0
  58. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/route_auth_fusion.py +0 -0
  59. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/sarif.py +0 -0
  60. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/sdk/__init__.py +0 -0
  61. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/sdk/contracts.py +0 -0
  62. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/sdk/models.py +0 -0
  63. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/security_overlay.py +0 -0
  64. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/srcpaths.py +0 -0
  65. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/suggest.py +0 -0
  66. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/suppress.py +0 -0
  67. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/topology.py +0 -0
  68. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/weaknesses.py +0 -0
  69. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/webhardening.py +0 -0
  70. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap/workflow_scanner.py +0 -0
  71. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap.egg-info/dependency_links.txt +0 -0
  72. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap.egg-info/entry_points.txt +0 -0
  73. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap.egg-info/requires.txt +0 -0
  74. {attackmap-0.4.10 → attackmap-0.4.21}/src/attackmap.egg-info/top_level.txt +0 -0
  75. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_analyzer.py +0 -0
  76. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_analyzer_metadata_schema.py +0 -0
  77. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_analyzers.py +0 -0
  78. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_artifact_schemas.py +0 -0
  79. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_config_scanner.py +0 -0
  80. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_context_pack.py +0 -0
  81. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_crypto.py +0 -0
  82. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_defensive_review.py +0 -0
  83. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_diagrams.py +0 -0
  84. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_diff.py +0 -0
  85. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_exploitability.py +0 -0
  86. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_graph.py +0 -0
  87. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_hunt.py +0 -0
  88. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_injection_sinks.py +0 -0
  89. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_llm_review.py +0 -0
  90. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_merge.py +0 -0
  91. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_modules_cli.py +0 -0
  92. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_pr_comment.py +0 -0
  93. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_progress.py +0 -0
  94. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_provenance.py +0 -0
  95. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_recon_to_analysis.py +0 -0
  96. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_remediate.py +0 -0
  97. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_report.py +0 -0
  98. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_review_eval.py +0 -0
  99. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_review_json.py +0 -0
  100. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_review_prompts.py +0 -0
  101. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_route_auth_fusion.py +0 -0
  102. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_sarif.py +0 -0
  103. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_sbom.py +0 -0
  104. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_security_overlay.py +0 -0
  105. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_shared_contract_imports.py +0 -0
  106. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_signal_v2.py +0 -0
  107. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_srcpaths.py +0 -0
  108. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_suggest.py +0 -0
  109. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_suppress.py +0 -0
  110. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_taint_sanitizer.py +0 -0
  111. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_threat_intel.py +0 -0
  112. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_threat_model.py +0 -0
  113. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_topology.py +0 -0
  114. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_weaknesses.py +0 -0
  115. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_webhardening.py +0 -0
  116. {attackmap-0.4.10 → attackmap-0.4.21}/tests/test_workflow_scanner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: attackmap
3
- Version: 0.4.10
3
+ Version: 0.4.21
4
4
  Summary: AI-assisted defensive security analyzer for codebases — scans a repository, models assets and controls, finds cross-cutting weaknesses, and generates an evidence-grounded review with MITRE ATT&CK mappings and detection-engineering hints.
5
5
  Author: AttackMap Contributors
6
6
  Author-email: Matthew Davis <matthewd@matthewd.xyz>
@@ -77,12 +77,24 @@ managers who need to triage an unfamiliar codebase.
77
77
  [![Python: 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/)
78
78
  [![PyPI](https://img.shields.io/pypi/v/attackmap.svg)](https://pypi.org/project/attackmap/)
79
79
 
80
- > **Status: beta (v0.4.0).** Core engine and 14 analyzer plugins are published
80
+ > **Status: beta (v0.4.18).** Core engine and 14 analyzer plugins are published
81
81
  > to PyPI, Homebrew, and GHCR and validated against real-world codebases.
82
82
  > AttackMap is heuristic by design — findings are confidence-tiered evidence,
83
83
  > not proof. See [Project status](#project-status) for what's solid and what's
84
84
  > still maturing.
85
85
 
86
+ ### The macOS app
87
+
88
+ Prefer a GUI? AttackMap ships a native macOS front-end that drives this CLI and
89
+ renders every result view — install with `brew install --cask mlaify/tap/attackmap-app`
90
+ (the cask depends on the `attackmap` formula, so this pulls the CLI too).
91
+
92
+ | Overview | Exploitability |
93
+ |---|---|
94
+ | [![AttackMap macOS app — overview](https://raw.githubusercontent.com/mlaify/AttackMap/main/docs/screenshots/04-overview.png)](https://docs.matthewd.xyz/gui/) | [![AttackMap macOS app — exploitability](https://raw.githubusercontent.com/mlaify/AttackMap/main/docs/screenshots/06-exploitability-ranked.png)](https://docs.matthewd.xyz/gui/) |
95
+
96
+ *Scanning OWASP Juice Shop. Full walkthrough in the [macOS app docs](https://docs.matthewd.xyz/gui/); source at [mlaify/AttackMap-mac](https://github.com/mlaify/AttackMap-mac).*
97
+
86
98
  ---
87
99
 
88
100
  ## Quickstart
@@ -187,6 +199,7 @@ Every `attackmap analyze` run writes:
187
199
  | `defensive-review-llm.md` *(with `--llm`)* | Claude-narrated review |
188
200
  | `defensive-review-llm.meta.json` *(with `--llm`)* | Backend, model, token usage |
189
201
  | `vulnerability-hypotheses.md` *(with `--hunt`)* | LLM-generated, evidence-cited exploit-chain **hypotheses** to confirm (leads, not detections) |
202
+ | `triage.md` *(with `--triage`)* | Clustered, de-duplicated, ranked shortlist of the existing findings (LLM-backed; deterministic fallback) |
190
203
 
191
204
  ### GitHub Code Scanning integration
192
205
 
@@ -345,7 +358,11 @@ left on).
345
358
 
346
359
  Sinks that are only dangerous with attacker-controlled input (SSRF, SSTI, NoSQL,
347
360
  open redirect, `open`) are gated on a request-shaped identifier in the call — a constant URL or
348
- template is not flagged. It's a heuristic (import-edge ≠ call-edge), so findings
361
+ template is not flagged. The walk is **call-graph-aware** (#138): for Python and
362
+ JS/TS an import edge is kept only when the imported symbol is actually used in
363
+ the file, so a dead import no longer fans a route out to that module's sinks
364
+ (the plain import-graph remains the fallback for namespace/star/side-effect
365
+ imports and for Go/PHP). It's still a heuristic (use ≉ proven call), so findings
349
366
  are evidence, not proof; confidence tapers with hop distance. Chains appear in
350
367
  `attackmap-report.json` under `scan.taint_chains`.
351
368
 
@@ -367,6 +384,27 @@ excluded from all the heuristic passes above by default, since dangerous
367
384
  patterns in test scaffolding are rarely real exposure. Set
368
385
  `ATTACKMAP_INCLUDE_TESTS=1` to scan them too (e.g. for test-quality reviews).
369
386
 
387
+ **Recall mode (`--recall`).** Aggressiveness is only useful *behind* a verifier —
388
+ so `--recall` widens taint discovery and marks the extra reach **speculative**
389
+ instead of asserting it. It:
390
+
391
+ - raises the max import-hop depth (2 → 4) and per-route visit budget;
392
+ - surfaces reaches the default pass conservatively hides, like a
393
+ static-literal-looking `eval`/`exec`/deserialize argument;
394
+ - **enumerates capability-reach**: every reach to a powerful capability —
395
+ network (`requests`/`httpx`/`axios`/`fetch`/`urlopen`), template
396
+ (`render_template_string`/`Template`), filesystem (`open`), redirect
397
+ (`redirect`/`res.redirect`) — even with **no known-bad pattern**, i.e. the
398
+ bare call without a request-derived argument (a genuine request-derived sink
399
+ still fires once, as a confirmed finding, not a duplicate).
400
+
401
+ Speculative chains carry downgraded confidence, get their own LOW-severity
402
+ finding (`SPECULATIVE (recall mode)…`), are excluded from every asserted
403
+ downstream pass (exploitability, BOLA, attack paths), and are kept **out of
404
+ `--fail-on-new-high`** — they are leads for `--hunt --verify` / `--triage` to
405
+ adjudicate, not detections. Default behavior is unchanged; pair `--recall` with
406
+ `--hunt --verify` to widen the net and then adjudicate what it catches.
407
+
370
408
  ### Web hardening gaps
371
409
 
372
410
  Route- and config-level checks for common web misconfigurations, each an
@@ -403,6 +441,32 @@ identifier; cipher/ECB tokens are matched case-sensitively so algorithm names
403
441
  aren't confused with prose (e.g. the French word "des"). Results appear under
404
442
  `scan.crypto_weaknesses`.
405
443
 
444
+ ### Hard-coded secret detection
445
+
446
+ Two layers, precision-first. **Typed provider-signature detectors** recognize the
447
+ distinctive shapes of leaked keys and fire at high confidence with a
448
+ provider-specific `kind`:
449
+
450
+ | Provider | Prefix / shape |
451
+ |---|---|
452
+ | AWS | `AKIA…` / `ASIA…` |
453
+ | GitHub | `ghp_` / `gho_` / `ghu_` / `ghs_` / `ghr_` |
454
+ | OpenAI | `sk-…` / `sk-proj-…` / `sk-svcacct-…` |
455
+ | Anthropic | `sk-ant-…` |
456
+ | GitLab / npm | `glpat-…` / `npm_…` |
457
+ | Stripe / Slack / Google | `sk_live_…` / `xox[bpsare]-…` / `AIza…` |
458
+ | SendGrid / Mailgun / Twilio | `SG.…` / `key-…` / `AC…` SID |
459
+ | PEM / JWT | `-----BEGIN … PRIVATE KEY-----` / `eyJ….eyJ….…` (header verified) |
460
+
461
+ Below the typed layer, a **Shannon-entropy fallback** catches high-entropy
462
+ literals that don't match a known provider. It is calibrated to stay quiet:
463
+ pure-hex hashes, URLs, version/number strings, base64/base32 alphabet constants
464
+ (#96), Subresource-Integrity / lockfile digests (`sha384-…`), and UUIDs are
465
+ suppressed, and a typed match always takes precedence over the entropy hit for
466
+ the same span. Values are redacted (`ghp_…6789`) before they reach any report —
467
+ the raw secret never appears. No liveness verification is performed. Results
468
+ appear as `scan.secret_hints`.
469
+
406
470
  ### GitHub Actions / CI workflow security
407
471
 
408
472
  CI config is a real attack surface. AttackMap parses `.github/workflows/*.yml`
@@ -442,18 +506,22 @@ adds the general mutating-endpoint case, with the resolved chain as evidence.
442
506
  OWASP API Security #1. AttackMap flags a route as a BOLA/IDOR candidate when it
443
507
  composes three signals it already has:
444
508
 
445
- 1. the route takes a **resource id** in the path (`/users/{id}`,
446
- `/orders/:orderId`, `/docs/<int:doc_id>`), and
447
- 2. it **reaches a datastore** — a DB hint in the same file/module, or a taint
448
- chain from the route to a SQL execute sink, and
509
+ 1. the route takes a **resource id** — via a **path** template (`/users/{id}`,
510
+ `/orders/:orderId`), an id-bearing **query parameter** (`?orderId=`), an
511
+ **RPC method** (XRPC `/xrpc/…getRecord`, tRPC `user.byId`), or a **GraphQL
512
+ field** (`user(id: ID!)`) — and
513
+ 2. it **reaches a datastore** — a DB hint in the same file/module, a taint chain
514
+ from the route to a SQL execute sink, or (for RPC/GraphQL) an object-access
515
+ operation, and
449
516
  3. **no ownership/authorization check** is visible near the handler
450
- (`current_user`, `request.user`, `authorize`, a policy/guard, or a
451
- `filter_by(user_id=…)`-style scoped query).
517
+ (`current_user`, `request.user`, `authorize`, a policy/guard, a
518
+ `filter_by(user_id=…)`-style scoped query, or a GraphQL `@auth`/`@hasRole`
519
+ schema directive on the field).
452
520
 
453
- Write routes (POST/PUT/PATCH/DELETE) are HIGH, reads MEDIUM. Candidates appear
454
- under `scan.authz_candidates`; the ownership-marker scan is the main
455
- false-positive reducer, so a well-scoped handler is not flagged. Path-template
456
- routes today; query-parameter and RPC-method ids are future work.
521
+ Write operations (POST/PUT/PATCH/DELETE, GraphQL mutations) are HIGH, reads
522
+ MEDIUM. Candidates appear under `scan.authz_candidates` (each carrying its
523
+ `surface`); the ownership-marker scan is the main false-positive reducer, so a
524
+ well-scoped handler or an `@auth`-guarded field is not flagged.
457
525
 
458
526
  ### Anomaly / outlier detection
459
527
 
@@ -469,6 +537,20 @@ within each cohort the odd-one-out is reported:
469
537
  | `auth_outlier` | siblings carry an auth/authorization signal near the handler; this route doesn't | HIGH |
470
538
  | `validation_outlier` | among a cohort's state-changing handlers, peers validate input and this one shows no validation marker | LOW |
471
539
  | `method_outlier` | a lone state-changing method in an otherwise read-only cohort | MEDIUM |
540
+ | `invariant_violation` | a mined-invariant outlier: among handlers that reach the same dangerous sink kind, the majority guard the request *before* the sink and this one doesn't — evidence cites the mined rule | HIGH |
541
+
542
+ The `invariant_violation` kind is the **mined-invariant** pass (#149a): its
543
+ cohort is not a route-path prefix but the set of handlers that reach the same
544
+ dangerous sink (from the taint pass). When a strong majority guard the request
545
+ before the sink, that pattern is mined as an implicit invariant and the handler
546
+ that reaches the same sink kind with no preceding guard is flagged, citing the
547
+ mined rule (e.g. "9 of 10 handlers that reach a database (SQL execute) sink
548
+ apply an auth/validation guard before it"). It reasons only about **same-file**
549
+ flows, where guard-before-sink ordering is actually verifiable (a cross-file
550
+ sink could be called before an `authorize(...)` that textually follows it), and
551
+ counts one handler per declaration regardless of how many verbs it exposes.
552
+ Sanitized chains are excluded so only genuinely undefended flows form the cohort
553
+ — it measures the code against its own norm and needs no signature.
472
554
 
473
555
  Everything is peer-relative and **confidence scales with how consistent the
474
556
  cohort is** — a lone deviation among many agreeing siblings is likelier a mistake
@@ -520,8 +602,27 @@ of the common manifest files:
520
602
  | Composer (PHP) | `composer.json` (require + require-dev; platform reqs skipped) |
521
603
 
522
604
  Each entry appears in `attackmap-report.json` under `scan.dependencies` with
523
- `{name, version, ecosystem, file, dev}`. Version ranges are kept verbatim
524
- (`^4.16.0`, `>=2,<3`, `latest`) — this slice does not resolve lockfiles.
605
+ `{name, version, ecosystem, file, dev, resolved, direct, via}`. Manifest ranges
606
+ are kept verbatim (`^4.16.0`, `>=2,<3`, `latest`).
607
+
608
+ **Lockfile resolution (#143).** When a lockfile is present it is parsed for
609
+ *exact* resolved versions and the full **transitive** tree — where most
610
+ known-vulnerable dependencies actually live. Resolved entries set
611
+ `resolved=true`, mark `direct`/transitive, and carry a `via` resolution path
612
+ (`express > body-parser > qs`). A lockfile supersedes the range-only manifest
613
+ in its **own directory** (so a monorepo's per-service lockfiles don't shadow
614
+ each other).
615
+
616
+ | Lockfile | Ecosystem |
617
+ |---|---|
618
+ | `package-lock.json` (v1/v2/v3), `pnpm-lock.yaml` | npm |
619
+ | `poetry.lock`, `uv.lock` | PyPI |
620
+ | `Cargo.lock` | Cargo |
621
+
622
+ Go needs no lockfile pass: `go.mod` already pins exact versions and flags
623
+ `// indirect` (transitive) deps, so it's treated as resolved directly.
624
+ (`go.sum` is a checksum history, not the build list, so it is not used for
625
+ inventory.)
525
626
 
526
627
  ### CVE cross-reference (opt-in)
527
628
 
@@ -542,9 +643,12 @@ attackmap analyze . --cve
542
643
  the network.
543
644
  - **Offline-tolerant.** If the network's unavailable but the cache is warm,
544
645
  cached results still surface; only fresh queries are skipped.
545
- - **Version resolution is best-effort.** Manifest ranges (`^4.16.0`,
546
- `>=2.28,<3`) resolve to a queryable lower-bound; OSV does the range math.
547
- Unpinned specs (`*`, `latest`) are skipped.
646
+ - **Exact when a lockfile exists.** Lockfile-resolved dependencies (direct and
647
+ transitive) are queried at their pinned version, and a vulnerable transitive
648
+ dep is reported with its resolution path. For range-only manifests with no
649
+ lockfile, version resolution is best-effort — ranges (`^4.16.0`, `>=2.28,<3`)
650
+ resolve to a queryable lower-bound and OSV does the range math; unpinned specs
651
+ (`*`, `latest`) are skipped.
548
652
 
549
653
  The structured vulnerability list is available under
550
654
  `scan.vulnerabilities` in `attackmap-report.json` with
@@ -595,9 +699,35 @@ to confirm, not detections.** The same grounding contract as the review applies
595
699
  assignment, no exploit code, confidence-tiered, and each lead lists exactly what
596
700
  a human must verify. Uses the same auth/backend resolution as `--llm`.
597
701
 
702
+ With `--verify`, each hypothesis is adjudicated against the actual source at its
703
+ cited locations. **`--verify-votes N`** (default 3) turns that into a jury: **N
704
+ independent skeptics** each adjudicate the same fixed hypothesis list. A lead is
705
+ **CONFIRMED only on a strict majority** (NEEDS_REVIEW only when a majority flags
706
+ the evidence as insufficient); ties and uncertainty are **REFUTED** — so a lead
707
+ only one skeptic would confirm is dropped. **`--hunt-lenses N`** additionally
708
+ fans generation out into N independent passes, each specialised in one failure
709
+ mode (auth-bypass, TOCTOU/race, IDOR, deserialization, SSRF-to-internal,
710
+ secret-misuse), and dedupes the leads across passes before verifying — more
711
+ unique novel leads than a single generalist pass. **`--hunt-rounds N`** loops
712
+ generation for up to N rounds, accumulating new (deduped) leads while a
713
+ completeness critic seeds each next round with untried angles, and stops early
714
+ once a round finds nothing new; **`--hunt-budget T`** caps the multi-round output
715
+ tokens.
716
+ This is the verifier the unknown-bug initiative
717
+ ([`docs/unknown-bug-epic.md`](docs/unknown-bug-epic.md)) is built on;
718
+ `--verify-votes 1` is the classic single pass.
719
+
598
720
  Layered on top: **MITRE ATT&CK technique mappings** on every insight and
599
721
  **detection opportunities** (Sigma/KQL/Splunk-style hints) for each weakness.
600
722
 
723
+ **6. Triage (`--triage`).** Where `--hunt` finds *new* leads, `--triage`
724
+ distills the *existing* heuristic findings: `attackmap analyze . --triage` has
725
+ the LLM cluster them by root cause, de-duplicate, and rank into a prioritized
726
+ shortlist that cites real finding IDs (it organizes, it never invents). Output
727
+ goes to `triage.md`. When no LLM backend is available it degrades to a
728
+ **deterministic**, score-ordered, clustered shortlist — reproducible enough to
729
+ diff across runs — rather than erroring.
730
+
601
731
  ---
602
732
 
603
733
  ## Supported ecosystems
@@ -645,7 +775,11 @@ attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPE
645
775
  attackmap analyze <path> --llm --llm-provider openai --llm-model gpt-5.5 # any OpenAI model ID, passed through
646
776
  attackmap analyze <path> --hunt # LLM vulnerability-hypothesis hunt (leads to confirm)
647
777
  attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (confirmed/refuted/review)
778
+ attackmap analyze <path> --hunt --verify --verify-votes 3 # majority vote of N independent skeptics (default 3; 1 = single pass)
779
+ attackmap analyze <path> --hunt --verify --hunt-lenses 4 # N lens-specialised generation passes, deduped, then verified
780
+ attackmap analyze <path> --hunt --verify --hunt-rounds 4 # loop-until-dry generation (completeness critic seeds each round)
648
781
  attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
782
+ attackmap analyze <path> --triage # cluster/dedupe/rank existing findings (triage.md; deterministic fallback)
649
783
  attackmap analyze <path> --pr-comment pr.md # Markdown PR summary comment for CI
650
784
 
651
785
  # CI / PR diff gating
@@ -679,9 +813,10 @@ introduces a new HIGH finding.
679
813
  Dependabot go deeper on transitive resolution and lockfiles. AttackMap's
680
814
  value is folding the CVE signal into an architecture-aware narrative
681
815
  ("this public route reaches this vulnerable ORM").
682
- - **A true taint engine.** The data-flow pass is a heuristic import-graph walk
683
- (import-edge ≈ call-edge), not sound interprocedural taint analysis. It
684
- favors precision over recall; findings are evidence, not proof.
816
+ - **A true taint engine.** The data-flow pass is a heuristic call-graph-refined
817
+ import-graph walk (edges pruned to used symbols, but symbol *use* ≉ a proven
818
+ data-carrying call), not sound interprocedural taint analysis. It favors
819
+ precision over recall; findings are evidence, not proof.
685
820
  - **Exhaustive.** AttackMap is heuristic by design. Findings are confidence-tiered
686
821
  with explicit guardrails for stale signals.
687
822
 
@@ -689,7 +824,7 @@ introduces a new HIGH finding.
689
824
 
690
825
  ## Project status
691
826
 
692
- AttackMap is **beta** (v0.4.0) — published and validated on real codebases, but
827
+ AttackMap is **beta** (v0.4.18) — published and validated on real codebases, but
693
828
  pre-1.0 and heuristic.
694
829
 
695
830
  **Solid today:**
@@ -711,8 +846,9 @@ pre-1.0 and heuristic.
711
846
  explainable 0–100 "exploitable now" score that ranks route→sink combinations
712
847
  and folds in known-vulnerable dependencies on the path.
713
848
  - **`--hunt`** (LLM exploit-chain hypotheses) with **`--verify`** (adjudicate
714
- each lead CONFIRMED/REFUTED/NEEDS-REVIEW against the actual source), and
715
- **`--remediate`** (review-first fix suggestions).
849
+ each lead CONFIRMED/REFUTED/NEEDS-REVIEW against the actual source),
850
+ **`--remediate`** (review-first fix suggestions), and **`--triage`**
851
+ (cluster/dedupe/rank existing findings, with a deterministic fallback).
716
852
  - SBOM inventory (5 ecosystems) + OSV.dev CVE cross-reference (`--cve`).
717
853
  - Output: Markdown + JSON + **SARIF 2.1.0** + **Mermaid / Graphviz** diagrams;
718
854
  **diff/baseline** PR gating; a reusable **GitHub Action + PR bot**; optional
@@ -724,11 +860,16 @@ pre-1.0 and heuristic.
724
860
 
725
861
  **Still maturing:**
726
862
 
727
- - Taint is Python/JS/TS/Go/PHP and path-template scoped; query-param / RPC-method
728
- authorization and more languages are planned.
729
- - The import-graph taint walk approximates call-edges with import-edges — precision
863
+ - BOLA/IDOR authorization now covers path templates, query parameters, RPC
864
+ methods (XRPC/tRPC), and GraphQL fields; deeper resolver-level GraphQL
865
+ ownership analysis and more languages are planned.
866
+ - The taint walk is call-graph-aware for Python/JS/TS (import edges are pruned to
867
+ symbols actually used in the file) but still approximates: symbol *use* is not a
868
+ proven data-carrying call, and Go/PHP keep the plain import-graph. Precision
730
869
  over recall; findings are evidence, not proof.
731
- - CVE lookup resolves a best-effort concrete version, not full lockfile ranges.
870
+ - CVE lookup uses exact lockfile-resolved versions (direct + transitive) when a
871
+ lockfile is present; range-only manifests without a lockfile still fall back to
872
+ a best-effort concrete version.
732
873
  - Anomaly / exploitability reasoning is route-cohort and taint-chain scoped.
733
874
 
734
875
  ---
@@ -16,12 +16,24 @@ managers who need to triage an unfamiliar codebase.
16
16
  [![Python: 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/)
17
17
  [![PyPI](https://img.shields.io/pypi/v/attackmap.svg)](https://pypi.org/project/attackmap/)
18
18
 
19
- > **Status: beta (v0.4.0).** Core engine and 14 analyzer plugins are published
19
+ > **Status: beta (v0.4.18).** Core engine and 14 analyzer plugins are published
20
20
  > to PyPI, Homebrew, and GHCR and validated against real-world codebases.
21
21
  > AttackMap is heuristic by design — findings are confidence-tiered evidence,
22
22
  > not proof. See [Project status](#project-status) for what's solid and what's
23
23
  > still maturing.
24
24
 
25
+ ### The macOS app
26
+
27
+ Prefer a GUI? AttackMap ships a native macOS front-end that drives this CLI and
28
+ renders every result view — install with `brew install --cask mlaify/tap/attackmap-app`
29
+ (the cask depends on the `attackmap` formula, so this pulls the CLI too).
30
+
31
+ | Overview | Exploitability |
32
+ |---|---|
33
+ | [![AttackMap macOS app — overview](https://raw.githubusercontent.com/mlaify/AttackMap/main/docs/screenshots/04-overview.png)](https://docs.matthewd.xyz/gui/) | [![AttackMap macOS app — exploitability](https://raw.githubusercontent.com/mlaify/AttackMap/main/docs/screenshots/06-exploitability-ranked.png)](https://docs.matthewd.xyz/gui/) |
34
+
35
+ *Scanning OWASP Juice Shop. Full walkthrough in the [macOS app docs](https://docs.matthewd.xyz/gui/); source at [mlaify/AttackMap-mac](https://github.com/mlaify/AttackMap-mac).*
36
+
25
37
  ---
26
38
 
27
39
  ## Quickstart
@@ -126,6 +138,7 @@ Every `attackmap analyze` run writes:
126
138
  | `defensive-review-llm.md` *(with `--llm`)* | Claude-narrated review |
127
139
  | `defensive-review-llm.meta.json` *(with `--llm`)* | Backend, model, token usage |
128
140
  | `vulnerability-hypotheses.md` *(with `--hunt`)* | LLM-generated, evidence-cited exploit-chain **hypotheses** to confirm (leads, not detections) |
141
+ | `triage.md` *(with `--triage`)* | Clustered, de-duplicated, ranked shortlist of the existing findings (LLM-backed; deterministic fallback) |
129
142
 
130
143
  ### GitHub Code Scanning integration
131
144
 
@@ -284,7 +297,11 @@ left on).
284
297
 
285
298
  Sinks that are only dangerous with attacker-controlled input (SSRF, SSTI, NoSQL,
286
299
  open redirect, `open`) are gated on a request-shaped identifier in the call — a constant URL or
287
- template is not flagged. It's a heuristic (import-edge ≠ call-edge), so findings
300
+ template is not flagged. The walk is **call-graph-aware** (#138): for Python and
301
+ JS/TS an import edge is kept only when the imported symbol is actually used in
302
+ the file, so a dead import no longer fans a route out to that module's sinks
303
+ (the plain import-graph remains the fallback for namespace/star/side-effect
304
+ imports and for Go/PHP). It's still a heuristic (use ≉ proven call), so findings
288
305
  are evidence, not proof; confidence tapers with hop distance. Chains appear in
289
306
  `attackmap-report.json` under `scan.taint_chains`.
290
307
 
@@ -306,6 +323,27 @@ excluded from all the heuristic passes above by default, since dangerous
306
323
  patterns in test scaffolding are rarely real exposure. Set
307
324
  `ATTACKMAP_INCLUDE_TESTS=1` to scan them too (e.g. for test-quality reviews).
308
325
 
326
+ **Recall mode (`--recall`).** Aggressiveness is only useful *behind* a verifier —
327
+ so `--recall` widens taint discovery and marks the extra reach **speculative**
328
+ instead of asserting it. It:
329
+
330
+ - raises the max import-hop depth (2 → 4) and per-route visit budget;
331
+ - surfaces reaches the default pass conservatively hides, like a
332
+ static-literal-looking `eval`/`exec`/deserialize argument;
333
+ - **enumerates capability-reach**: every reach to a powerful capability —
334
+ network (`requests`/`httpx`/`axios`/`fetch`/`urlopen`), template
335
+ (`render_template_string`/`Template`), filesystem (`open`), redirect
336
+ (`redirect`/`res.redirect`) — even with **no known-bad pattern**, i.e. the
337
+ bare call without a request-derived argument (a genuine request-derived sink
338
+ still fires once, as a confirmed finding, not a duplicate).
339
+
340
+ Speculative chains carry downgraded confidence, get their own LOW-severity
341
+ finding (`SPECULATIVE (recall mode)…`), are excluded from every asserted
342
+ downstream pass (exploitability, BOLA, attack paths), and are kept **out of
343
+ `--fail-on-new-high`** — they are leads for `--hunt --verify` / `--triage` to
344
+ adjudicate, not detections. Default behavior is unchanged; pair `--recall` with
345
+ `--hunt --verify` to widen the net and then adjudicate what it catches.
346
+
309
347
  ### Web hardening gaps
310
348
 
311
349
  Route- and config-level checks for common web misconfigurations, each an
@@ -342,6 +380,32 @@ identifier; cipher/ECB tokens are matched case-sensitively so algorithm names
342
380
  aren't confused with prose (e.g. the French word "des"). Results appear under
343
381
  `scan.crypto_weaknesses`.
344
382
 
383
+ ### Hard-coded secret detection
384
+
385
+ Two layers, precision-first. **Typed provider-signature detectors** recognize the
386
+ distinctive shapes of leaked keys and fire at high confidence with a
387
+ provider-specific `kind`:
388
+
389
+ | Provider | Prefix / shape |
390
+ |---|---|
391
+ | AWS | `AKIA…` / `ASIA…` |
392
+ | GitHub | `ghp_` / `gho_` / `ghu_` / `ghs_` / `ghr_` |
393
+ | OpenAI | `sk-…` / `sk-proj-…` / `sk-svcacct-…` |
394
+ | Anthropic | `sk-ant-…` |
395
+ | GitLab / npm | `glpat-…` / `npm_…` |
396
+ | Stripe / Slack / Google | `sk_live_…` / `xox[bpsare]-…` / `AIza…` |
397
+ | SendGrid / Mailgun / Twilio | `SG.…` / `key-…` / `AC…` SID |
398
+ | PEM / JWT | `-----BEGIN … PRIVATE KEY-----` / `eyJ….eyJ….…` (header verified) |
399
+
400
+ Below the typed layer, a **Shannon-entropy fallback** catches high-entropy
401
+ literals that don't match a known provider. It is calibrated to stay quiet:
402
+ pure-hex hashes, URLs, version/number strings, base64/base32 alphabet constants
403
+ (#96), Subresource-Integrity / lockfile digests (`sha384-…`), and UUIDs are
404
+ suppressed, and a typed match always takes precedence over the entropy hit for
405
+ the same span. Values are redacted (`ghp_…6789`) before they reach any report —
406
+ the raw secret never appears. No liveness verification is performed. Results
407
+ appear as `scan.secret_hints`.
408
+
345
409
  ### GitHub Actions / CI workflow security
346
410
 
347
411
  CI config is a real attack surface. AttackMap parses `.github/workflows/*.yml`
@@ -381,18 +445,22 @@ adds the general mutating-endpoint case, with the resolved chain as evidence.
381
445
  OWASP API Security #1. AttackMap flags a route as a BOLA/IDOR candidate when it
382
446
  composes three signals it already has:
383
447
 
384
- 1. the route takes a **resource id** in the path (`/users/{id}`,
385
- `/orders/:orderId`, `/docs/<int:doc_id>`), and
386
- 2. it **reaches a datastore** — a DB hint in the same file/module, or a taint
387
- chain from the route to a SQL execute sink, and
448
+ 1. the route takes a **resource id** — via a **path** template (`/users/{id}`,
449
+ `/orders/:orderId`), an id-bearing **query parameter** (`?orderId=`), an
450
+ **RPC method** (XRPC `/xrpc/…getRecord`, tRPC `user.byId`), or a **GraphQL
451
+ field** (`user(id: ID!)`) — and
452
+ 2. it **reaches a datastore** — a DB hint in the same file/module, a taint chain
453
+ from the route to a SQL execute sink, or (for RPC/GraphQL) an object-access
454
+ operation, and
388
455
  3. **no ownership/authorization check** is visible near the handler
389
- (`current_user`, `request.user`, `authorize`, a policy/guard, or a
390
- `filter_by(user_id=…)`-style scoped query).
456
+ (`current_user`, `request.user`, `authorize`, a policy/guard, a
457
+ `filter_by(user_id=…)`-style scoped query, or a GraphQL `@auth`/`@hasRole`
458
+ schema directive on the field).
391
459
 
392
- Write routes (POST/PUT/PATCH/DELETE) are HIGH, reads MEDIUM. Candidates appear
393
- under `scan.authz_candidates`; the ownership-marker scan is the main
394
- false-positive reducer, so a well-scoped handler is not flagged. Path-template
395
- routes today; query-parameter and RPC-method ids are future work.
460
+ Write operations (POST/PUT/PATCH/DELETE, GraphQL mutations) are HIGH, reads
461
+ MEDIUM. Candidates appear under `scan.authz_candidates` (each carrying its
462
+ `surface`); the ownership-marker scan is the main false-positive reducer, so a
463
+ well-scoped handler or an `@auth`-guarded field is not flagged.
396
464
 
397
465
  ### Anomaly / outlier detection
398
466
 
@@ -408,6 +476,20 @@ within each cohort the odd-one-out is reported:
408
476
  | `auth_outlier` | siblings carry an auth/authorization signal near the handler; this route doesn't | HIGH |
409
477
  | `validation_outlier` | among a cohort's state-changing handlers, peers validate input and this one shows no validation marker | LOW |
410
478
  | `method_outlier` | a lone state-changing method in an otherwise read-only cohort | MEDIUM |
479
+ | `invariant_violation` | a mined-invariant outlier: among handlers that reach the same dangerous sink kind, the majority guard the request *before* the sink and this one doesn't — evidence cites the mined rule | HIGH |
480
+
481
+ The `invariant_violation` kind is the **mined-invariant** pass (#149a): its
482
+ cohort is not a route-path prefix but the set of handlers that reach the same
483
+ dangerous sink (from the taint pass). When a strong majority guard the request
484
+ before the sink, that pattern is mined as an implicit invariant and the handler
485
+ that reaches the same sink kind with no preceding guard is flagged, citing the
486
+ mined rule (e.g. "9 of 10 handlers that reach a database (SQL execute) sink
487
+ apply an auth/validation guard before it"). It reasons only about **same-file**
488
+ flows, where guard-before-sink ordering is actually verifiable (a cross-file
489
+ sink could be called before an `authorize(...)` that textually follows it), and
490
+ counts one handler per declaration regardless of how many verbs it exposes.
491
+ Sanitized chains are excluded so only genuinely undefended flows form the cohort
492
+ — it measures the code against its own norm and needs no signature.
411
493
 
412
494
  Everything is peer-relative and **confidence scales with how consistent the
413
495
  cohort is** — a lone deviation among many agreeing siblings is likelier a mistake
@@ -459,8 +541,27 @@ of the common manifest files:
459
541
  | Composer (PHP) | `composer.json` (require + require-dev; platform reqs skipped) |
460
542
 
461
543
  Each entry appears in `attackmap-report.json` under `scan.dependencies` with
462
- `{name, version, ecosystem, file, dev}`. Version ranges are kept verbatim
463
- (`^4.16.0`, `>=2,<3`, `latest`) — this slice does not resolve lockfiles.
544
+ `{name, version, ecosystem, file, dev, resolved, direct, via}`. Manifest ranges
545
+ are kept verbatim (`^4.16.0`, `>=2,<3`, `latest`).
546
+
547
+ **Lockfile resolution (#143).** When a lockfile is present it is parsed for
548
+ *exact* resolved versions and the full **transitive** tree — where most
549
+ known-vulnerable dependencies actually live. Resolved entries set
550
+ `resolved=true`, mark `direct`/transitive, and carry a `via` resolution path
551
+ (`express > body-parser > qs`). A lockfile supersedes the range-only manifest
552
+ in its **own directory** (so a monorepo's per-service lockfiles don't shadow
553
+ each other).
554
+
555
+ | Lockfile | Ecosystem |
556
+ |---|---|
557
+ | `package-lock.json` (v1/v2/v3), `pnpm-lock.yaml` | npm |
558
+ | `poetry.lock`, `uv.lock` | PyPI |
559
+ | `Cargo.lock` | Cargo |
560
+
561
+ Go needs no lockfile pass: `go.mod` already pins exact versions and flags
562
+ `// indirect` (transitive) deps, so it's treated as resolved directly.
563
+ (`go.sum` is a checksum history, not the build list, so it is not used for
564
+ inventory.)
464
565
 
465
566
  ### CVE cross-reference (opt-in)
466
567
 
@@ -481,9 +582,12 @@ attackmap analyze . --cve
481
582
  the network.
482
583
  - **Offline-tolerant.** If the network's unavailable but the cache is warm,
483
584
  cached results still surface; only fresh queries are skipped.
484
- - **Version resolution is best-effort.** Manifest ranges (`^4.16.0`,
485
- `>=2.28,<3`) resolve to a queryable lower-bound; OSV does the range math.
486
- Unpinned specs (`*`, `latest`) are skipped.
585
+ - **Exact when a lockfile exists.** Lockfile-resolved dependencies (direct and
586
+ transitive) are queried at their pinned version, and a vulnerable transitive
587
+ dep is reported with its resolution path. For range-only manifests with no
588
+ lockfile, version resolution is best-effort — ranges (`^4.16.0`, `>=2.28,<3`)
589
+ resolve to a queryable lower-bound and OSV does the range math; unpinned specs
590
+ (`*`, `latest`) are skipped.
487
591
 
488
592
  The structured vulnerability list is available under
489
593
  `scan.vulnerabilities` in `attackmap-report.json` with
@@ -534,9 +638,35 @@ to confirm, not detections.** The same grounding contract as the review applies
534
638
  assignment, no exploit code, confidence-tiered, and each lead lists exactly what
535
639
  a human must verify. Uses the same auth/backend resolution as `--llm`.
536
640
 
641
+ With `--verify`, each hypothesis is adjudicated against the actual source at its
642
+ cited locations. **`--verify-votes N`** (default 3) turns that into a jury: **N
643
+ independent skeptics** each adjudicate the same fixed hypothesis list. A lead is
644
+ **CONFIRMED only on a strict majority** (NEEDS_REVIEW only when a majority flags
645
+ the evidence as insufficient); ties and uncertainty are **REFUTED** — so a lead
646
+ only one skeptic would confirm is dropped. **`--hunt-lenses N`** additionally
647
+ fans generation out into N independent passes, each specialised in one failure
648
+ mode (auth-bypass, TOCTOU/race, IDOR, deserialization, SSRF-to-internal,
649
+ secret-misuse), and dedupes the leads across passes before verifying — more
650
+ unique novel leads than a single generalist pass. **`--hunt-rounds N`** loops
651
+ generation for up to N rounds, accumulating new (deduped) leads while a
652
+ completeness critic seeds each next round with untried angles, and stops early
653
+ once a round finds nothing new; **`--hunt-budget T`** caps the multi-round output
654
+ tokens.
655
+ This is the verifier the unknown-bug initiative
656
+ ([`docs/unknown-bug-epic.md`](docs/unknown-bug-epic.md)) is built on;
657
+ `--verify-votes 1` is the classic single pass.
658
+
537
659
  Layered on top: **MITRE ATT&CK technique mappings** on every insight and
538
660
  **detection opportunities** (Sigma/KQL/Splunk-style hints) for each weakness.
539
661
 
662
+ **6. Triage (`--triage`).** Where `--hunt` finds *new* leads, `--triage`
663
+ distills the *existing* heuristic findings: `attackmap analyze . --triage` has
664
+ the LLM cluster them by root cause, de-duplicate, and rank into a prioritized
665
+ shortlist that cites real finding IDs (it organizes, it never invents). Output
666
+ goes to `triage.md`. When no LLM backend is available it degrades to a
667
+ **deterministic**, score-ordered, clustered shortlist — reproducible enough to
668
+ diff across runs — rather than erroring.
669
+
540
670
  ---
541
671
 
542
672
  ## Supported ecosystems
@@ -584,7 +714,11 @@ attackmap analyze <path> --llm --llm-provider openai # use OpenAI/Codex (OPE
584
714
  attackmap analyze <path> --llm --llm-provider openai --llm-model gpt-5.5 # any OpenAI model ID, passed through
585
715
  attackmap analyze <path> --hunt # LLM vulnerability-hypothesis hunt (leads to confirm)
586
716
  attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (confirmed/refuted/review)
717
+ attackmap analyze <path> --hunt --verify --verify-votes 3 # majority vote of N independent skeptics (default 3; 1 = single pass)
718
+ attackmap analyze <path> --hunt --verify --hunt-lenses 4 # N lens-specialised generation passes, deduped, then verified
719
+ attackmap analyze <path> --hunt --verify --hunt-rounds 4 # loop-until-dry generation (completeness critic seeds each round)
587
720
  attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
721
+ attackmap analyze <path> --triage # cluster/dedupe/rank existing findings (triage.md; deterministic fallback)
588
722
  attackmap analyze <path> --pr-comment pr.md # Markdown PR summary comment for CI
589
723
 
590
724
  # CI / PR diff gating
@@ -618,9 +752,10 @@ introduces a new HIGH finding.
618
752
  Dependabot go deeper on transitive resolution and lockfiles. AttackMap's
619
753
  value is folding the CVE signal into an architecture-aware narrative
620
754
  ("this public route reaches this vulnerable ORM").
621
- - **A true taint engine.** The data-flow pass is a heuristic import-graph walk
622
- (import-edge ≈ call-edge), not sound interprocedural taint analysis. It
623
- favors precision over recall; findings are evidence, not proof.
755
+ - **A true taint engine.** The data-flow pass is a heuristic call-graph-refined
756
+ import-graph walk (edges pruned to used symbols, but symbol *use* ≉ a proven
757
+ data-carrying call), not sound interprocedural taint analysis. It favors
758
+ precision over recall; findings are evidence, not proof.
624
759
  - **Exhaustive.** AttackMap is heuristic by design. Findings are confidence-tiered
625
760
  with explicit guardrails for stale signals.
626
761
 
@@ -628,7 +763,7 @@ introduces a new HIGH finding.
628
763
 
629
764
  ## Project status
630
765
 
631
- AttackMap is **beta** (v0.4.0) — published and validated on real codebases, but
766
+ AttackMap is **beta** (v0.4.18) — published and validated on real codebases, but
632
767
  pre-1.0 and heuristic.
633
768
 
634
769
  **Solid today:**
@@ -650,8 +785,9 @@ pre-1.0 and heuristic.
650
785
  explainable 0–100 "exploitable now" score that ranks route→sink combinations
651
786
  and folds in known-vulnerable dependencies on the path.
652
787
  - **`--hunt`** (LLM exploit-chain hypotheses) with **`--verify`** (adjudicate
653
- each lead CONFIRMED/REFUTED/NEEDS-REVIEW against the actual source), and
654
- **`--remediate`** (review-first fix suggestions).
788
+ each lead CONFIRMED/REFUTED/NEEDS-REVIEW against the actual source),
789
+ **`--remediate`** (review-first fix suggestions), and **`--triage`**
790
+ (cluster/dedupe/rank existing findings, with a deterministic fallback).
655
791
  - SBOM inventory (5 ecosystems) + OSV.dev CVE cross-reference (`--cve`).
656
792
  - Output: Markdown + JSON + **SARIF 2.1.0** + **Mermaid / Graphviz** diagrams;
657
793
  **diff/baseline** PR gating; a reusable **GitHub Action + PR bot**; optional
@@ -663,11 +799,16 @@ pre-1.0 and heuristic.
663
799
 
664
800
  **Still maturing:**
665
801
 
666
- - Taint is Python/JS/TS/Go/PHP and path-template scoped; query-param / RPC-method
667
- authorization and more languages are planned.
668
- - The import-graph taint walk approximates call-edges with import-edges — precision
802
+ - BOLA/IDOR authorization now covers path templates, query parameters, RPC
803
+ methods (XRPC/tRPC), and GraphQL fields; deeper resolver-level GraphQL
804
+ ownership analysis and more languages are planned.
805
+ - The taint walk is call-graph-aware for Python/JS/TS (import edges are pruned to
806
+ symbols actually used in the file) but still approximates: symbol *use* is not a
807
+ proven data-carrying call, and Go/PHP keep the plain import-graph. Precision
669
808
  over recall; findings are evidence, not proof.
670
- - CVE lookup resolves a best-effort concrete version, not full lockfile ranges.
809
+ - CVE lookup uses exact lockfile-resolved versions (direct + transitive) when a
810
+ lockfile is present; range-only manifests without a lockfile still fall back to
811
+ a best-effort concrete version.
671
812
  - Anomaly / exploitability reasoning is route-cohort and taint-chain scoped.
672
813
 
673
814
  ---
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "attackmap"
7
- version = "0.4.10"
7
+ version = "0.4.21"
8
8
  description = "AI-assisted defensive security analyzer for codebases — scans a repository, models assets and controls, finds cross-cutting weaknesses, and generates an evidence-grounded review with MITRE ATT&CK mappings and detection-engineering hints."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,2 +1,2 @@
1
1
  __all__ = ["__version__"]
2
- __version__ = "0.4.10"
2
+ __version__ = "0.4.21"