attackmap 0.4.6__tar.gz → 0.4.15__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 (112) hide show
  1. {attackmap-0.4.6/src/attackmap.egg-info → attackmap-0.4.15}/PKG-INFO +207 -28
  2. {attackmap-0.4.6 → attackmap-0.4.15}/README.md +205 -27
  3. {attackmap-0.4.6 → attackmap-0.4.15}/pyproject.toml +2 -1
  4. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/__init__.py +1 -1
  5. attackmap-0.4.15/src/attackmap/authz.py +448 -0
  6. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/cli.py +118 -0
  7. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/cve.py +11 -1
  8. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/exploitability.py +4 -0
  9. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/llm_review.py +3 -1
  10. attackmap-0.4.15/src/attackmap/lockfiles.py +387 -0
  11. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/merge.py +4 -0
  12. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/models.py +59 -6
  13. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/report.py +20 -1
  14. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/review_prompts.py +61 -0
  15. attackmap-0.4.15/src/attackmap/route_auth_fusion.py +299 -0
  16. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/sarif.py +55 -36
  17. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/sbom.py +31 -3
  18. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/scanner.py +83 -3
  19. attackmap-0.4.15/src/attackmap/suppress.py +418 -0
  20. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/taint.py +218 -35
  21. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/threat_model.py +135 -6
  22. attackmap-0.4.15/src/attackmap/triage.py +151 -0
  23. attackmap-0.4.15/src/attackmap/workflow_scanner.py +328 -0
  24. {attackmap-0.4.6 → attackmap-0.4.15/src/attackmap.egg-info}/PKG-INFO +207 -28
  25. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap.egg-info/SOURCES.txt +12 -1
  26. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap.egg-info/requires.txt +1 -0
  27. attackmap-0.4.15/tests/test_authz.py +472 -0
  28. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_cve.py +91 -0
  29. attackmap-0.4.15/tests/test_lockfiles.py +348 -0
  30. attackmap-0.4.15/tests/test_route_auth_fusion.py +249 -0
  31. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_scanner.py +79 -0
  32. attackmap-0.4.15/tests/test_suppress.py +340 -0
  33. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_taint.py +119 -0
  34. attackmap-0.4.15/tests/test_taint_sanitizer.py +122 -0
  35. attackmap-0.4.15/tests/test_triage.py +201 -0
  36. attackmap-0.4.15/tests/test_workflow_scanner.py +416 -0
  37. attackmap-0.4.6/src/attackmap/authz.py +0 -196
  38. attackmap-0.4.6/tests/test_authz.py +0 -241
  39. {attackmap-0.4.6 → attackmap-0.4.15}/LICENSE +0 -0
  40. {attackmap-0.4.6 → attackmap-0.4.15}/setup.cfg +0 -0
  41. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/analyzer.py +0 -0
  42. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/analyzer_contracts.py +0 -0
  43. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/analyzers.py +0 -0
  44. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/anomalies.py +0 -0
  45. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/asset_model.py +0 -0
  46. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/attack_taxonomy.py +0 -0
  47. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/config_scanner.py +0 -0
  48. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/context_pack.py +0 -0
  49. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/control_model.py +0 -0
  50. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/crypto.py +0 -0
  51. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/defensive_review.py +0 -0
  52. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/detection_opportunities.py +0 -0
  53. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/diagrams.py +0 -0
  54. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/diff.py +0 -0
  55. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/graph.py +0 -0
  56. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/insights.py +0 -0
  57. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/progress.py +0 -0
  58. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/recon_models.py +0 -0
  59. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/recon_to_analysis.py +0 -0
  60. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/review_eval.py +0 -0
  61. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/review_json.py +0 -0
  62. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/sdk/__init__.py +0 -0
  63. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/sdk/contracts.py +0 -0
  64. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/sdk/models.py +0 -0
  65. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/security_overlay.py +0 -0
  66. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/srcpaths.py +0 -0
  67. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/suggest.py +0 -0
  68. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/topology.py +0 -0
  69. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/weaknesses.py +0 -0
  70. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap/webhardening.py +0 -0
  71. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap.egg-info/dependency_links.txt +0 -0
  72. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap.egg-info/entry_points.txt +0 -0
  73. {attackmap-0.4.6 → attackmap-0.4.15}/src/attackmap.egg-info/top_level.txt +0 -0
  74. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_analyzer.py +0 -0
  75. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_analyzer_metadata_schema.py +0 -0
  76. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_analyzers.py +0 -0
  77. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_anomalies.py +0 -0
  78. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_artifact_schemas.py +0 -0
  79. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_config_scanner.py +0 -0
  80. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_context_pack.py +0 -0
  81. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_crypto.py +0 -0
  82. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_defensive_review.py +0 -0
  83. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_diagrams.py +0 -0
  84. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_diff.py +0 -0
  85. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_exploitability.py +0 -0
  86. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_graph.py +0 -0
  87. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_hunt.py +0 -0
  88. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_injection_sinks.py +0 -0
  89. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_llm_review.py +0 -0
  90. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_merge.py +0 -0
  91. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_modules_cli.py +0 -0
  92. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_pr_comment.py +0 -0
  93. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_progress.py +0 -0
  94. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_provenance.py +0 -0
  95. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_recon_to_analysis.py +0 -0
  96. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_remediate.py +0 -0
  97. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_report.py +0 -0
  98. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_review_eval.py +0 -0
  99. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_review_json.py +0 -0
  100. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_review_prompts.py +0 -0
  101. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_sarif.py +0 -0
  102. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_sbom.py +0 -0
  103. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_security_overlay.py +0 -0
  104. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_shared_contract_imports.py +0 -0
  105. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_signal_v2.py +0 -0
  106. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_srcpaths.py +0 -0
  107. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_suggest.py +0 -0
  108. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_threat_intel.py +0 -0
  109. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_threat_model.py +0 -0
  110. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_topology.py +0 -0
  111. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_weaknesses.py +0 -0
  112. {attackmap-0.4.6 → attackmap-0.4.15}/tests/test_webhardening.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: attackmap
3
- Version: 0.4.6
3
+ Version: 0.4.15
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>
@@ -34,6 +34,7 @@ License-File: LICENSE
34
34
  Requires-Dist: typer>=0.12.3
35
35
  Requires-Dist: pydantic>=2.7.0
36
36
  Requires-Dist: networkx>=3.3
37
+ Requires-Dist: PyYAML>=6.0
37
38
  Provides-Extra: llm
38
39
  Requires-Dist: anthropic>=0.40.0; extra == "llm"
39
40
  Requires-Dist: openai>=1.60.0; extra == "llm"
@@ -76,12 +77,24 @@ managers who need to triage an unfamiliar codebase.
76
77
  [![Python: 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/)
77
78
  [![PyPI](https://img.shields.io/pypi/v/attackmap.svg)](https://pypi.org/project/attackmap/)
78
79
 
79
- > **Status: beta (v0.4.0).** Core engine and 14 analyzer plugins are published
80
+ > **Status: beta (v0.4.15).** Core engine and 14 analyzer plugins are published
80
81
  > to PyPI, Homebrew, and GHCR and validated against real-world codebases.
81
82
  > AttackMap is heuristic by design — findings are confidence-tiered evidence,
82
83
  > not proof. See [Project status](#project-status) for what's solid and what's
83
84
  > still maturing.
84
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
+
85
98
  ---
86
99
 
87
100
  ## Quickstart
@@ -186,6 +199,7 @@ Every `attackmap analyze` run writes:
186
199
  | `defensive-review-llm.md` *(with `--llm`)* | Claude-narrated review |
187
200
  | `defensive-review-llm.meta.json` *(with `--llm`)* | Backend, model, token usage |
188
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) |
189
203
 
190
204
  ### GitHub Code Scanning integration
191
205
 
@@ -271,6 +285,48 @@ on unrelated commits, so a finding that persists across scans has the same id
271
285
  in both. The diff has three sections — **New**, **Persisted**, **Resolved** —
272
286
  which drop cleanly into a PR comment.
273
287
 
288
+ ### Suppressing findings
289
+
290
+ Silence a residual false positive without going blind to new signal. Two
291
+ mechanisms, both honored across every pass:
292
+
293
+ **Repo-level baseline** — `.attackmap-suppress.yaml` at the repo root:
294
+
295
+ ```yaml
296
+ version: 1
297
+ suppress:
298
+ # by stable finding id (the 16-hex id in attackmap-report.json)
299
+ - id: 1a2b3c4d5e6f7a8b
300
+ reason: accepted risk, tracked in JIRA-1234
301
+
302
+ # by rule (a slug of the finding title — same string as the SARIF ruleId),
303
+ # optionally scoped to paths
304
+ - rule: hard-coded-secret-literals-were-found-in-source-or-config
305
+ reason: example keys in docs, never shipped
306
+ paths: ["docs/**", "examples/**"]
307
+
308
+ # by path only — any finding whose evidence is *entirely* within these globs
309
+ - path: "vendor/**"
310
+ reason: third-party code, out of scope
311
+ ```
312
+
313
+ A `path` selector matches a finding only when **every** file its evidence cites
314
+ falls under the glob, so a finding that also touches live code is never hidden.
315
+ `*` spans path separators (`vendor/*` covers the whole subtree).
316
+
317
+ **Inline directives** — a comment on the flagged line, in any language:
318
+
319
+ ```python
320
+ API_KEY = "AKIA…" # attackmap:ignore[hard-coded-secret-literals-were-found-in-source-or-config] staging only
321
+ ```
322
+
323
+ Suppressed findings are **not dropped**: they're excluded from
324
+ `--fail-on-new-high` and the console summary, but retained in
325
+ `attackmap-report.json` under `suppressed_findings` (with reasons) and marked
326
+ with a SARIF `suppressions` array so GitHub Code Scanning shows them as
327
+ suppressed. Suppression counts print in the run summary. Use `--no-suppress`
328
+ for a full unfiltered audit, or `--suppress-file PATH` to point elsewhere.
329
+
274
330
  ### Data-flow / injection detection
275
331
 
276
332
  A lightweight taint pass (Python + JS/TS) walks the import graph up to two hops
@@ -302,10 +358,27 @@ left on).
302
358
 
303
359
  Sinks that are only dangerous with attacker-controlled input (SSRF, SSTI, NoSQL,
304
360
  open redirect, `open`) are gated on a request-shaped identifier in the call — a constant URL or
305
- 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
306
366
  are evidence, not proof; confidence tapers with hop distance. Chains appear in
307
367
  `attackmap-report.json` under `scan.taint_chains`.
308
368
 
369
+ **Sanitizer awareness.** When a sink-appropriate neutralizer is present in the
370
+ sink file — `shlex.quote` / `escapeshellarg` (shell), `secure_filename` / path
371
+ allow-listers (`open`), `is_safe_url` (redirect), `markupsafe.escape` (SSTI),
372
+ driver escapers (SQL), mongo-sanitize (NoSQL) — the chain is marked
373
+ `sanitized`, its confidence is downgraded well below the finding threshold, and
374
+ the neutralizer is recorded in `sanitizer_evidence`. Sanitized chains stay in
375
+ `scan.taint_chains` for audit but don't raise a HIGH finding or earn an
376
+ exploitability score. Detection is file-granular (matching the import-walk's
377
+ granularity), so it trades a little recall for precision. To extend the table,
378
+ add a high-signal, sink-appropriate `(label, regex)` entry to
379
+ `_SANITIZER_PATTERNS` in [`taint.py`](src/attackmap/taint.py) — keep it specific
380
+ (a vague `validate(` would hide real bugs).
381
+
309
382
  Test and spec files (`tests/`, `__tests__/`, `*.test.*`, `test_*.py`, …) are
310
383
  excluded from all the heuristic passes above by default, since dangerous
311
384
  patterns in test scaffolding are rarely real exposure. Set
@@ -347,23 +420,87 @@ identifier; cipher/ECB tokens are matched case-sensitively so algorithm names
347
420
  aren't confused with prose (e.g. the French word "des"). Results appear under
348
421
  `scan.crypto_weaknesses`.
349
422
 
423
+ ### Hard-coded secret detection
424
+
425
+ Two layers, precision-first. **Typed provider-signature detectors** recognize the
426
+ distinctive shapes of leaked keys and fire at high confidence with a
427
+ provider-specific `kind`:
428
+
429
+ | Provider | Prefix / shape |
430
+ |---|---|
431
+ | AWS | `AKIA…` / `ASIA…` |
432
+ | GitHub | `ghp_` / `gho_` / `ghu_` / `ghs_` / `ghr_` |
433
+ | OpenAI | `sk-…` / `sk-proj-…` / `sk-svcacct-…` |
434
+ | Anthropic | `sk-ant-…` |
435
+ | GitLab / npm | `glpat-…` / `npm_…` |
436
+ | Stripe / Slack / Google | `sk_live_…` / `xox[bpsare]-…` / `AIza…` |
437
+ | SendGrid / Mailgun / Twilio | `SG.…` / `key-…` / `AC…` SID |
438
+ | PEM / JWT | `-----BEGIN … PRIVATE KEY-----` / `eyJ….eyJ….…` (header verified) |
439
+
440
+ Below the typed layer, a **Shannon-entropy fallback** catches high-entropy
441
+ literals that don't match a known provider. It is calibrated to stay quiet:
442
+ pure-hex hashes, URLs, version/number strings, base64/base32 alphabet constants
443
+ (#96), Subresource-Integrity / lockfile digests (`sha384-…`), and UUIDs are
444
+ suppressed, and a typed match always takes precedence over the entropy hit for
445
+ the same span. Values are redacted (`ghp_…6789`) before they reach any report —
446
+ the raw secret never appears. No liveness verification is performed. Results
447
+ appear as `scan.secret_hints`.
448
+
449
+ ### GitHub Actions / CI workflow security
450
+
451
+ CI config is a real attack surface. AttackMap parses `.github/workflows/*.yml`
452
+ and flags supply-chain and code-execution risks, each a finding with an ATT&CK
453
+ mapping:
454
+
455
+ | Kind | Catches | Severity |
456
+ |---|---|---|
457
+ | `script_injection` | attacker-controlled context (`github.event.*.{title,body,message,…}`, `github.head_ref`) interpolated into a `run:` step | HIGH |
458
+ | `pr_target_checkout` | `pull_request_target` + checkout of the PR head ref (untrusted code with secrets in scope) | HIGH |
459
+ | `unpinned_action` | `uses: org/action@tag\|branch` instead of a pinned commit SHA | branch = MEDIUM, semver tag = LOW |
460
+ | `secret_in_run` | `${{ secrets.* }}` expanded into a shell step (pass via `env:` instead) | MEDIUM |
461
+ | `broad_permissions` | `permissions: write-all` at workflow or job level | MEDIUM |
462
+ | `self_hosted_pr` | self-hosted runner on a `pull_request`/`pull_request_target` trigger | HIGH / MEDIUM |
463
+
464
+ A hardened workflow — SHA-pinned actions, scoped `permissions:`, secrets via
465
+ `env:`, no untrusted-context interpolation — produces nothing. Results appear
466
+ under `scan.workflow_issues`.
467
+
468
+ ### Unauthenticated state-changing routes
469
+
470
+ Raw `auth_hints` are signals, not conclusions — a monorepo emits hundreds. A
471
+ fusion pass turns them into one precise finding: the public, state-changing
472
+ (`POST/PUT/PATCH/DELETE`) routes with **no auth control on their own chain**. It
473
+ resolves the control *per route* rather than by file proximity:
474
+
475
+ - **Express/Koa/Fastify** — auth middleware in the route's arguments, or a global `app.use(...)` / `router.use(...)`.
476
+ - **FastAPI/Flask** — an auth decorator, `dependencies=[Depends(...)]`, `Depends(<auth>)` / `Security(...)` in the signature, or a router-level dependency.
477
+ - **Spring** — `@PreAuthorize` / `@Secured` / `@RolesAllowed` on the method or controller, or a global "authenticated" policy.
478
+
479
+ Routes behind a control produce nothing, and the sensitive categories
480
+ (webhook/admin/upload/auth) keep their own dedicated findings — so this only
481
+ adds the general mutating-endpoint case, with the resolved chain as evidence.
482
+
350
483
  ### Broken object-level authorization (BOLA / IDOR)
351
484
 
352
485
  OWASP API Security #1. AttackMap flags a route as a BOLA/IDOR candidate when it
353
486
  composes three signals it already has:
354
487
 
355
- 1. the route takes a **resource id** in the path (`/users/{id}`,
356
- `/orders/:orderId`, `/docs/<int:doc_id>`), and
357
- 2. it **reaches a datastore** — a DB hint in the same file/module, or a taint
358
- chain from the route to a SQL execute sink, and
488
+ 1. the route takes a **resource id** — via a **path** template (`/users/{id}`,
489
+ `/orders/:orderId`), an id-bearing **query parameter** (`?orderId=`), an
490
+ **RPC method** (XRPC `/xrpc/…getRecord`, tRPC `user.byId`), or a **GraphQL
491
+ field** (`user(id: ID!)`) — and
492
+ 2. it **reaches a datastore** — a DB hint in the same file/module, a taint chain
493
+ from the route to a SQL execute sink, or (for RPC/GraphQL) an object-access
494
+ operation, and
359
495
  3. **no ownership/authorization check** is visible near the handler
360
- (`current_user`, `request.user`, `authorize`, a policy/guard, or a
361
- `filter_by(user_id=…)`-style scoped query).
496
+ (`current_user`, `request.user`, `authorize`, a policy/guard, a
497
+ `filter_by(user_id=…)`-style scoped query, or a GraphQL `@auth`/`@hasRole`
498
+ schema directive on the field).
362
499
 
363
- Write routes (POST/PUT/PATCH/DELETE) are HIGH, reads MEDIUM. Candidates appear
364
- under `scan.authz_candidates`; the ownership-marker scan is the main
365
- false-positive reducer, so a well-scoped handler is not flagged. Path-template
366
- routes today; query-parameter and RPC-method ids are future work.
500
+ Write operations (POST/PUT/PATCH/DELETE, GraphQL mutations) are HIGH, reads
501
+ MEDIUM. Candidates appear under `scan.authz_candidates` (each carrying its
502
+ `surface`); the ownership-marker scan is the main false-positive reducer, so a
503
+ well-scoped handler or an `@auth`-guarded field is not flagged.
367
504
 
368
505
  ### Anomaly / outlier detection
369
506
 
@@ -430,8 +567,27 @@ of the common manifest files:
430
567
  | Composer (PHP) | `composer.json` (require + require-dev; platform reqs skipped) |
431
568
 
432
569
  Each entry appears in `attackmap-report.json` under `scan.dependencies` with
433
- `{name, version, ecosystem, file, dev}`. Version ranges are kept verbatim
434
- (`^4.16.0`, `>=2,<3`, `latest`) — this slice does not resolve lockfiles.
570
+ `{name, version, ecosystem, file, dev, resolved, direct, via}`. Manifest ranges
571
+ are kept verbatim (`^4.16.0`, `>=2,<3`, `latest`).
572
+
573
+ **Lockfile resolution (#143).** When a lockfile is present it is parsed for
574
+ *exact* resolved versions and the full **transitive** tree — where most
575
+ known-vulnerable dependencies actually live. Resolved entries set
576
+ `resolved=true`, mark `direct`/transitive, and carry a `via` resolution path
577
+ (`express > body-parser > qs`). A lockfile supersedes the range-only manifest
578
+ in its **own directory** (so a monorepo's per-service lockfiles don't shadow
579
+ each other).
580
+
581
+ | Lockfile | Ecosystem |
582
+ |---|---|
583
+ | `package-lock.json` (v1/v2/v3), `pnpm-lock.yaml` | npm |
584
+ | `poetry.lock`, `uv.lock` | PyPI |
585
+ | `Cargo.lock` | Cargo |
586
+
587
+ Go needs no lockfile pass: `go.mod` already pins exact versions and flags
588
+ `// indirect` (transitive) deps, so it's treated as resolved directly.
589
+ (`go.sum` is a checksum history, not the build list, so it is not used for
590
+ inventory.)
435
591
 
436
592
  ### CVE cross-reference (opt-in)
437
593
 
@@ -452,9 +608,12 @@ attackmap analyze . --cve
452
608
  the network.
453
609
  - **Offline-tolerant.** If the network's unavailable but the cache is warm,
454
610
  cached results still surface; only fresh queries are skipped.
455
- - **Version resolution is best-effort.** Manifest ranges (`^4.16.0`,
456
- `>=2.28,<3`) resolve to a queryable lower-bound; OSV does the range math.
457
- Unpinned specs (`*`, `latest`) are skipped.
611
+ - **Exact when a lockfile exists.** Lockfile-resolved dependencies (direct and
612
+ transitive) are queried at their pinned version, and a vulnerable transitive
613
+ dep is reported with its resolution path. For range-only manifests with no
614
+ lockfile, version resolution is best-effort — ranges (`^4.16.0`, `>=2.28,<3`)
615
+ resolve to a queryable lower-bound and OSV does the range math; unpinned specs
616
+ (`*`, `latest`) are skipped.
458
617
 
459
618
  The structured vulnerability list is available under
460
619
  `scan.vulnerabilities` in `attackmap-report.json` with
@@ -508,6 +667,14 @@ a human must verify. Uses the same auth/backend resolution as `--llm`.
508
667
  Layered on top: **MITRE ATT&CK technique mappings** on every insight and
509
668
  **detection opportunities** (Sigma/KQL/Splunk-style hints) for each weakness.
510
669
 
670
+ **6. Triage (`--triage`).** Where `--hunt` finds *new* leads, `--triage`
671
+ distills the *existing* heuristic findings: `attackmap analyze . --triage` has
672
+ the LLM cluster them by root cause, de-duplicate, and rank into a prioritized
673
+ shortlist that cites real finding IDs (it organizes, it never invents). Output
674
+ goes to `triage.md`. When no LLM backend is available it degrades to a
675
+ **deterministic**, score-ordered, clustered shortlist — reproducible enough to
676
+ diff across runs — rather than erroring.
677
+
511
678
  ---
512
679
 
513
680
  ## Supported ecosystems
@@ -556,12 +723,17 @@ attackmap analyze <path> --llm --llm-provider openai --llm-model gpt-5.5 # any
556
723
  attackmap analyze <path> --hunt # LLM vulnerability-hypothesis hunt (leads to confirm)
557
724
  attackmap analyze <path> --hunt --verify # adjudicate each lead vs. source (confirmed/refuted/review)
558
725
  attackmap analyze <path> --remediate # LLM review-first fix suggestions (remediation.md)
726
+ attackmap analyze <path> --triage # cluster/dedupe/rank existing findings (triage.md; deterministic fallback)
559
727
  attackmap analyze <path> --pr-comment pr.md # Markdown PR summary comment for CI
560
728
 
561
729
  # CI / PR diff gating
562
730
  attackmap analyze <path> --baseline prev/attackmap-report.json \
563
731
  --diff-output reports/attackmap-diff.md --fail-on-new-high
564
732
 
733
+ # Suppression (.attackmap-suppress.yaml + inline `attackmap:ignore` directives)
734
+ attackmap analyze <path> --no-suppress # full audit, ignore all suppressions
735
+ attackmap analyze <path> --suppress-file cfg.yaml # override baseline location
736
+
565
737
  # Plugin discovery
566
738
  attackmap suggest ./repo # recommend plugins for a repo shape
567
739
  attackmap suggest ./repo --install # and pip-install the missing ones
@@ -585,9 +757,10 @@ introduces a new HIGH finding.
585
757
  Dependabot go deeper on transitive resolution and lockfiles. AttackMap's
586
758
  value is folding the CVE signal into an architecture-aware narrative
587
759
  ("this public route reaches this vulnerable ORM").
588
- - **A true taint engine.** The data-flow pass is a heuristic import-graph walk
589
- (import-edge ≈ call-edge), not sound interprocedural taint analysis. It
590
- favors precision over recall; findings are evidence, not proof.
760
+ - **A true taint engine.** The data-flow pass is a heuristic call-graph-refined
761
+ import-graph walk (edges pruned to used symbols, but symbol *use* ≉ a proven
762
+ data-carrying call), not sound interprocedural taint analysis. It favors
763
+ precision over recall; findings are evidence, not proof.
591
764
  - **Exhaustive.** AttackMap is heuristic by design. Findings are confidence-tiered
592
765
  with explicit guardrails for stale signals.
593
766
 
@@ -595,7 +768,7 @@ introduces a new HIGH finding.
595
768
 
596
769
  ## Project status
597
770
 
598
- AttackMap is **beta** (v0.4.0) — published and validated on real codebases, but
771
+ AttackMap is **beta** (v0.4.15) — published and validated on real codebases, but
599
772
  pre-1.0 and heuristic.
600
773
 
601
774
  **Solid today:**
@@ -617,8 +790,9 @@ pre-1.0 and heuristic.
617
790
  explainable 0–100 "exploitable now" score that ranks route→sink combinations
618
791
  and folds in known-vulnerable dependencies on the path.
619
792
  - **`--hunt`** (LLM exploit-chain hypotheses) with **`--verify`** (adjudicate
620
- each lead CONFIRMED/REFUTED/NEEDS-REVIEW against the actual source), and
621
- **`--remediate`** (review-first fix suggestions).
793
+ each lead CONFIRMED/REFUTED/NEEDS-REVIEW against the actual source),
794
+ **`--remediate`** (review-first fix suggestions), and **`--triage`**
795
+ (cluster/dedupe/rank existing findings, with a deterministic fallback).
622
796
  - SBOM inventory (5 ecosystems) + OSV.dev CVE cross-reference (`--cve`).
623
797
  - Output: Markdown + JSON + **SARIF 2.1.0** + **Mermaid / Graphviz** diagrams;
624
798
  **diff/baseline** PR gating; a reusable **GitHub Action + PR bot**; optional
@@ -630,11 +804,16 @@ pre-1.0 and heuristic.
630
804
 
631
805
  **Still maturing:**
632
806
 
633
- - Taint is Python/JS/TS/Go/PHP and path-template scoped; query-param / RPC-method
634
- authorization and more languages are planned.
635
- - The import-graph taint walk approximates call-edges with import-edges — precision
807
+ - BOLA/IDOR authorization now covers path templates, query parameters, RPC
808
+ methods (XRPC/tRPC), and GraphQL fields; deeper resolver-level GraphQL
809
+ ownership analysis and more languages are planned.
810
+ - The taint walk is call-graph-aware for Python/JS/TS (import edges are pruned to
811
+ symbols actually used in the file) but still approximates: symbol *use* is not a
812
+ proven data-carrying call, and Go/PHP keep the plain import-graph. Precision
636
813
  over recall; findings are evidence, not proof.
637
- - CVE lookup resolves a best-effort concrete version, not full lockfile ranges.
814
+ - CVE lookup uses exact lockfile-resolved versions (direct + transitive) when a
815
+ lockfile is present; range-only manifests without a lockfile still fall back to
816
+ a best-effort concrete version.
638
817
  - Anomaly / exploitability reasoning is route-cohort and taint-chain scoped.
639
818
 
640
819
  ---