@clear-capabilities/agentic-security-scanner 0.118.2 → 0.119.1

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.
Files changed (155) hide show
  1. package/CHANGELOG.md +256 -0
  2. package/bin/agentic-security.js +78 -1
  3. package/dist/178.index.js +1 -1
  4. package/dist/384.index.js +1 -1
  5. package/dist/415.index.js +90 -0
  6. package/dist/444.index.js +89 -0
  7. package/dist/503.index.js +288 -0
  8. package/dist/637.index.js +15 -1
  9. package/dist/838.index.js +1 -1
  10. package/dist/985.index.js +5 -1
  11. package/dist/agentic-security.mjs +18 -18
  12. package/dist/agentic-security.mjs.sha256 +1 -1
  13. package/package.json +13 -8
  14. package/src/dataflow/CLAUDE.md +1 -1
  15. package/src/dataflow/catalog.js +50 -11
  16. package/src/dataflow/engine.js +76 -1
  17. package/src/dataflow/implicit-flow.js +4 -2
  18. package/src/dataflow/index.js +9 -4
  19. package/src/dataflow/proof-gate.js +5 -0
  20. package/src/dataflow/summaries.js +19 -1
  21. package/src/engine.js +95 -7
  22. package/src/ir/parser-go.js +8 -0
  23. package/src/mcp/tools.js +6 -2
  24. package/src/posture/fix-history.js +24 -0
  25. package/src/posture/provenance.js +39 -0
  26. package/src/posture/sca-verdict.js +130 -0
  27. package/src/posture/secret-history.js +72 -0
  28. package/src/pr-delta.js +13 -0
  29. package/src/report/index.js +50 -0
  30. package/src/sast/agent-untrusted-flow.js +107 -0
  31. package/src/sast/api-authz.js +71 -0
  32. package/src/sast/cross-service.js +98 -0
  33. package/src/sast/event-entrypoint.js +90 -0
  34. package/src/sast/iac-terraform.js +107 -0
  35. package/src/sast/rbac-consistency.js +113 -0
  36. package/src/sast/wrong-context-sanitizer.js +55 -0
  37. package/src/sca/image-packages.js +74 -0
  38. package/src/sca/import-reachability.js +271 -0
  39. package/src/sca/install-script-analysis.js +82 -0
  40. package/bin/.agentic-security/findings.json +0 -1907
  41. package/bin/.agentic-security/last-scan.json +0 -1907
  42. package/bin/.agentic-security/last-scan.json.sig +0 -1
  43. package/bin/.agentic-security/scan-history.json +0 -166
  44. package/bin/.agentic-security/streak.json +0 -20
  45. package/src/.agentic-security/dpia.md +0 -26
  46. package/src/.agentic-security/findings.json +0 -117732
  47. package/src/.agentic-security/last-scan.json +0 -117732
  48. package/src/.agentic-security/last-scan.json.sig +0 -1
  49. package/src/.agentic-security/pqc-migration-plan.json +0 -80
  50. package/src/.agentic-security/pqc-migration-plan.md +0 -32
  51. package/src/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +0 -6
  52. package/src/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +0 -6
  53. package/src/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +0 -6
  54. package/src/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +0 -6
  55. package/src/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +0 -6
  56. package/src/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +0 -6
  57. package/src/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +0 -6
  58. package/src/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +0 -6
  59. package/src/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +0 -6
  60. package/src/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +0 -6
  61. package/src/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +0 -6
  62. package/src/.agentic-security/scan-history.json +0 -12946
  63. package/src/.agentic-security/streak.json +0 -21
  64. package/src/.agentic-security/threat-model.json +0 -3702
  65. package/src/.agentic-security/threat-model.md +0 -118
  66. package/src/dataflow/.agentic-security/dpia.md +0 -26
  67. package/src/dataflow/.agentic-security/findings.json +0 -6086
  68. package/src/dataflow/.agentic-security/last-scan.json +0 -6086
  69. package/src/dataflow/.agentic-security/last-scan.json.sig +0 -1
  70. package/src/dataflow/.agentic-security/sbom-history/15d7c4d20f9d968ebec29445c9e9b3fc6f74e6a1.json +0 -6
  71. package/src/dataflow/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +0 -6
  72. package/src/dataflow/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +0 -6
  73. package/src/dataflow/.agentic-security/sbom-history/8869fe64035bec0152d92ece961e9369d8637076.json +0 -6
  74. package/src/dataflow/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +0 -6
  75. package/src/dataflow/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +0 -6
  76. package/src/dataflow/.agentic-security/scan-history.json +0 -250
  77. package/src/dataflow/.agentic-security/streak.json +0 -21
  78. package/src/dataflow/.agentic-security/threat-model.json +0 -161
  79. package/src/dataflow/.agentic-security/threat-model.md +0 -43
  80. package/src/ir/.agentic-security/dpia.md +0 -26
  81. package/src/ir/.agentic-security/exploit-bundles.json +0 -77
  82. package/src/ir/.agentic-security/findings.json +0 -4011
  83. package/src/ir/.agentic-security/last-scan.json +0 -4011
  84. package/src/ir/.agentic-security/last-scan.json.sig +0 -1
  85. package/src/ir/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +0 -6
  86. package/src/ir/.agentic-security/sbom-history/b5f468a158f6a145aedd8680841c106b9809f16b.json +0 -6
  87. package/src/ir/.agentic-security/scan-history.json +0 -193
  88. package/src/ir/.agentic-security/streak.json +0 -20
  89. package/src/ir/.agentic-security/threat-model.json +0 -143
  90. package/src/ir/.agentic-security/threat-model.md +0 -39
  91. package/src/mcp/.agentic-security/findings.json +0 -8632
  92. package/src/mcp/.agentic-security/last-scan.json +0 -8632
  93. package/src/mcp/.agentic-security/last-scan.json.sig +0 -1
  94. package/src/mcp/.agentic-security/scan-history.json +0 -331
  95. package/src/mcp/.agentic-security/streak.json +0 -20
  96. package/src/posture/.agentic-security/dpia.md +0 -26
  97. package/src/posture/.agentic-security/findings.json +0 -77181
  98. package/src/posture/.agentic-security/last-scan.json +0 -77181
  99. package/src/posture/.agentic-security/last-scan.json.sig +0 -1
  100. package/src/posture/.agentic-security/pqc-migration-plan.json +0 -65
  101. package/src/posture/.agentic-security/pqc-migration-plan.md +0 -30
  102. package/src/posture/.agentic-security/sbom-history/0724c133d2943c87555695b5b2adcfcfc3373a2d.json +0 -6
  103. package/src/posture/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +0 -6
  104. package/src/posture/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +0 -6
  105. package/src/posture/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +0 -6
  106. package/src/posture/.agentic-security/scan-history.json +0 -8904
  107. package/src/posture/.agentic-security/streak.json +0 -21
  108. package/src/posture/.agentic-security/threat-model.json +0 -2038
  109. package/src/posture/.agentic-security/threat-model.md +0 -73
  110. package/src/posture/compliance-frameworks/.agentic-security/findings.json +0 -80
  111. package/src/posture/compliance-frameworks/.agentic-security/last-scan.json +0 -80
  112. package/src/posture/compliance-frameworks/.agentic-security/last-scan.json.sig +0 -1
  113. package/src/posture/compliance-frameworks/.agentic-security/scan-history.json +0 -90
  114. package/src/posture/compliance-frameworks/.agentic-security/streak.json +0 -22
  115. package/src/report/.agentic-security/dpia.md +0 -26
  116. package/src/report/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +0 -6
  117. package/src/report/.agentic-security/sbom-history/f94be839aa6a1011f53da016416a561698b8f338.json +0 -6
  118. package/src/report/.agentic-security/threat-model.json +0 -7
  119. package/src/report/.agentic-security/threat-model.md +0 -22
  120. package/src/sast/.agentic-security/dpia.md +0 -26
  121. package/src/sast/.agentic-security/findings.json +0 -6154
  122. package/src/sast/.agentic-security/last-scan.json +0 -6154
  123. package/src/sast/.agentic-security/last-scan.json.sig +0 -1
  124. package/src/sast/.agentic-security/pqc-migration-plan.json +0 -65
  125. package/src/sast/.agentic-security/pqc-migration-plan.md +0 -30
  126. package/src/sast/.agentic-security/sbom-history/043a7b401c68f5a298c83d4c42e94f118ff2a89a.json +0 -6
  127. package/src/sast/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +0 -6
  128. package/src/sast/.agentic-security/sbom-history/20d77ad7363a82777d506dedbbb1ecfed52f094a.json +0 -6
  129. package/src/sast/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +0 -6
  130. package/src/sast/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +0 -6
  131. package/src/sast/.agentic-security/sbom-history/3c6b3205f04e26f7654943d465281d00e0fa395e.json +0 -6
  132. package/src/sast/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +0 -6
  133. package/src/sast/.agentic-security/sbom-history/5282502e090013219eb006f7e02d8c983bc29c8e.json +0 -6
  134. package/src/sast/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +0 -6
  135. package/src/sast/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +0 -6
  136. package/src/sast/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +0 -6
  137. package/src/sast/.agentic-security/sbom-history/73514b485d4770f3c5ee57d87338bccd1c61d8b7.json +0 -6
  138. package/src/sast/.agentic-security/sbom-history/96fa584fc3134512237d58aac63d6f3778b0cbcb.json +0 -6
  139. package/src/sast/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +0 -6
  140. package/src/sast/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +0 -6
  141. package/src/sast/.agentic-security/sbom-history/a4a34ab8b3d758de2adc8c3dc3e42d4cb0db00a3.json +0 -6
  142. package/src/sast/.agentic-security/sbom-history/ac236ec3602cfc8fb133f389ec3687e409084673.json +0 -6
  143. package/src/sast/.agentic-security/sbom-history/b57b3e1292f1f9f0de6c5a34a436e330c78008a3.json +0 -6
  144. package/src/sast/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +0 -6
  145. package/src/sast/.agentic-security/sbom-history/dcbfd4935922dd165093c08a1003c338eed937c5.json +0 -6
  146. package/src/sast/.agentic-security/sbom-history/fa017f725615b0293814344c8ef7dfeb06775ca0.json +0 -6
  147. package/src/sast/.agentic-security/scan-history.json +0 -941
  148. package/src/sast/.agentic-security/streak.json +0 -22
  149. package/src/sast/.agentic-security/threat-model.json +0 -816
  150. package/src/sast/.agentic-security/threat-model.md +0 -91
  151. package/src/sca/.agentic-security/findings.json +0 -7460
  152. package/src/sca/.agentic-security/last-scan.json +0 -7460
  153. package/src/sca/.agentic-security/last-scan.json.sig +0 -1
  154. package/src/sca/.agentic-security/scan-history.json +0 -113
  155. package/src/sca/.agentic-security/streak.json +0 -21
@@ -1 +1 @@
1
- 7625811bf6f85d6a9d7dd8b96cded3aff2a57426d1816b1591c3a410d87df8b5 agentic-security.mjs
1
+ 40d6882dc7b5dbd62ac41f10cb01ec4e43fa42081dbcc0427553e0282748fedb agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.118.2",
3
+ "version": "0.119.1",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin — SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, SARIF ingest, pipeline integrity, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -27,7 +27,9 @@
27
27
  "src/",
28
28
  "bin/",
29
29
  "dist/",
30
- "CHANGELOG.md"
30
+ "CHANGELOG.md",
31
+ "!**/.agentic-security",
32
+ "!**/.agentic-security/**"
31
33
  ],
32
34
  "engines": {
33
35
  "node": ">=24.0.0"
@@ -55,16 +57,19 @@
55
57
  },
56
58
  "scripts": {
57
59
  "build": "ncc build bin/agentic-security.js -o dist --minify -e web-tree-sitter -e tree-sitter-wasms && mv dist/index.js dist/agentic-security.mjs && rm -f dist/package.json && chmod +x dist/agentic-security.mjs && node -e \"const fs=require('fs');const p='dist/agentic-security.mjs';const c=fs.readFileSync(p,'utf8');if(!c.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+c);\" && node -e \"const fs=require('fs');const c=require('crypto');const h=c.createHash('sha256').update(fs.readFileSync('dist/agentic-security.mjs')).digest('hex');fs.writeFileSync('dist/agentic-security.mjs.sha256',h+' agentic-security.mjs\\n');\"",
58
- "prepublishOnly": "npm run build && node -e \"const fs=require('fs');const cur=fs.existsSync('CHANGELOG.md')?fs.readFileSync('CHANGELOG.md','utf8'):'';const src=fs.readFileSync('../CHANGELOG.md','utf8');if(cur && cur!==src){console.error('scanner/CHANGELOG.md has local edits that differ from ../CHANGELOG.md.');console.error('Refusing to overwrite. Either commit the edit upstream first or rm scanner/CHANGELOG.md to accept the upstream copy.');process.exit(1);}fs.writeFileSync('CHANGELOG.md',src);\"",
59
- "test": "npm run test:smoke && npm run test:sast && npm run test:posture && npm run test:dataflow && npm run test:mcp && npm run test:report && npm run test:bench-modules && npm run test:lifecycle && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js",
60
+ "prepublishOnly": "npm run build && node ../scripts/sync-scanner-changelog.mjs",
61
+ "test": "npm run test:smoke && npm run test:sast && npm run test:posture && npm run test:dataflow && npm run test:mcp && npm run test:report && npm run test:bench-modules && npm run test:lifecycle && npm run test:eval && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js",
60
62
  "test:smoke": "node --test test/smoke.test.js",
61
- "test:sast": "node --test test/llm.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/wrong-context-sanitizer.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/guard-recognition.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js",
62
- "test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/sca-coverage.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/license-graph.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/router.test.js test/legacy-alias-redirect.test.js test/coverage-report.test.js test/corpus-status.test.js",
63
- "test:dataflow": "node --test test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/parser-go.test.js test/parser-php-rb.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/kcfa-context.test.js test/callgraph-resolve.test.js",
63
+ "test:sast": "node --test test/llm.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/guard-recognition.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js",
64
+ "test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/license-graph.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/router.test.js test/legacy-alias-redirect.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js",
65
+ "test:dataflow": "node --test test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/parser-go.test.js test/parser-php-rb.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js",
64
66
  "test:mcp": "node --test test/mcp.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js",
65
- "test:report": "node --test test/sarif-ingest.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/grader-calibration.test.js",
67
+ "test:report": "node --test test/sarif-ingest.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js",
66
68
  "test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js",
67
69
  "test:lifecycle": "node --test test/dead-code.test.js test/no-dead-modules.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js && node ../scripts/lint-command-descriptions.mjs",
70
+ "test:eval": "node --test test/independent-eval.test.js",
71
+ "eval:independent": "node ../bench/independent-eval/runner.mjs",
72
+ "eval:independent:gate": "node ../bench/independent-eval/runner.mjs --gate default",
68
73
  "smoke": "node bin/agentic-security.js scan test/fixtures/vulnerable-js",
69
74
  "prebench": "npm run build",
70
75
  "bench": "node test/benchmark/bench.js",
@@ -28,7 +28,7 @@ Layer-2 taint engine. Walks the Layer-1 IR (`../ir/`) with field-sensitive forwa
28
28
 
29
29
  ## Entry points
30
30
 
31
- - `runTaintEngine(perFileIR, callGraph, opts)` — the public entry. Called from `engine.js` when `AGENTIC_SECURITY_DEEP=1` (or auto-enabled outside CI).
31
+ - `runTaintEngine(perFileIR, callGraph, opts)` — the public entry. Runs from `engine.js` when `AGENTIC_SECURITY_DEEP=1`. **R1 (PRD §5):** the CLI entry (`bin/agentic-security.js#cmdScan`) sets that env var by default for local/interactive scans (not in CI, and not when `--no-deep`/`AGENTIC_SECURITY_DEEP=0`), so the default `/scan --all` runs deep. In-process callers (tests, the cve-replay corpus) invoke `runScan()` directly without the CLI default and therefore stay deep-off unless they set the env var themselves (e.g. `test/deep-taint.test.js`).
32
32
  - `applyPathFeasibility` — constant-fold pass that runs before the worklist.
33
33
  - `annotateBackwardSlices` — backward-slice annotation for already-emitted findings.
34
34
  - `annotateProvenClean(findings, perFileIR)` (`proven-clean.js`) — proves a SQL sink is reached only through a parameterizer; sets `provenClean`. Wired by default in `runDeepAnalysis` (opt out: `AGENTIC_SECURITY_NO_PROOF_GATE=1`).
@@ -166,6 +166,21 @@ export const CATALOG = [
166
166
  { kind: 'source', id: 'go-buffalo-request',language:'go', framework:'buffalo',match: { type: 'member', object: 'c', prop: 'Request' }, label: 'c.Request (buffalo)' },
167
167
  { kind: 'source', id: 'go-gorilla-vars', language: 'go', framework: 'gorilla',match: { type: 'call', callee: 'Vars' }, label: 'mux.Vars (gorilla)' },
168
168
 
169
+ // ─── SINKS (Go — database/sql) — R3 (PRD §5) ──────────────────────────────
170
+ // callee 'Query' is also a net/http SOURCE (r.URL.Query). The engine
171
+ // disambiguates by position — source at the assignment RHS, sink at a call
172
+ // with a tainted query argument — so coexistence is benign (a source call
173
+ // like r.URL.Query() has no tainted arg, so it never fires the sink).
174
+ { kind: 'sink', id: 'go-sql-query', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'Query' }, argIndex: 0,
175
+ vuln: { name: 'SQL Injection (db.Query — Go)', severity: 'critical', cwe: 'CWE-89',
176
+ remediation: 'Use parameterized queries: db.Query("SELECT … WHERE id = $1", id). Never concatenate untrusted input into the SQL string.' } },
177
+ { kind: 'sink', id: 'go-sql-queryrow', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'QueryRow' }, argIndex: 0,
178
+ vuln: { name: 'SQL Injection (db.QueryRow — Go)', severity: 'critical', cwe: 'CWE-89',
179
+ remediation: 'Use placeholders ($1 / ?) and pass args separately.' } },
180
+ { kind: 'sink', id: 'go-sql-exec', language: 'go', framework: 'database/sql', match: { type: 'call', callee: 'Exec' }, argIndex: 0,
181
+ vuln: { name: 'SQL Injection (db.Exec — Go)', severity: 'critical', cwe: 'CWE-89',
182
+ remediation: 'Use parameterized statements: db.Exec("UPDATE t SET x=$1 WHERE id=$2", x, id).' } },
183
+
169
184
  // ─── SOURCES (Ruby — Rails / Sinatra) ─────────────────────────────────────
170
185
  { kind: 'source', id: 'rb-rails-params', language: 'rb', framework: 'rails', match: { type: 'global', name: 'params' }, label: 'params (Rails)' },
171
186
  { kind: 'source', id: 'rb-rails-cookies', language: 'rb', framework: 'rails', match: { type: 'global', name: 'cookies' }, label: 'cookies (Rails)' },
@@ -747,22 +762,46 @@ function filterByProvenance(entries) {
747
762
  return list;
748
763
  }
749
764
 
750
- export function matchSource(memberExpr) {
751
- // memberExpr is exprDesc: { kind: 'member', object: {kind:'ident',name}, prop }
752
- if (!memberExpr || memberExpr.kind !== 'member') return null;
753
- if (memberExpr.object?.kind !== 'ident') return null;
754
- const k = `${memberExpr.object.name}.${memberExpr.prop}`;
755
- const raw = MEMBER_INDEX.get(k);
756
- if (!raw) return null;
757
- const hits = filterByProvenance(raw);
758
- if (!hits.length) return null;
759
- return hits.find(h => h.kind === 'source') || null;
765
+ export function matchSource(expr) {
766
+ if (!expr) return null;
767
+ // Member sources (req.query): the original path — unchanged.
768
+ if (expr.kind === 'member' && expr.object?.kind === 'ident') {
769
+ const raw = MEMBER_INDEX.get(`${expr.object.name}.${expr.prop}`);
770
+ if (raw) {
771
+ const hits = filterByProvenance(raw);
772
+ const s = hits.find(h => h.kind === 'source');
773
+ if (s) return s;
774
+ }
775
+ }
776
+ // R3 (PRD §5): CALL sources (r.FormValue(), r.URL.Query(), c.QueryParam()).
777
+ // matchSource previously handled only member reads, so Go's call-shaped
778
+ // sources were never recognized at the assignment RHS. Match by callee last
779
+ // segment (Go gives a dotted string; JS/Py a member/ident expr).
780
+ if (expr.kind === 'call') {
781
+ let cn = null;
782
+ if (typeof expr.callee === 'string') cn = expr.callee.includes('.') ? expr.callee.slice(expr.callee.lastIndexOf('.') + 1) : expr.callee;
783
+ else if (expr.callee && expr.callee.kind === 'member') cn = expr.callee.prop;
784
+ else if (expr.callee && expr.callee.kind === 'ident') cn = expr.callee.name;
785
+ if (cn) {
786
+ const raw = CALLEE_INDEX.get(cn);
787
+ if (raw) {
788
+ const hits = filterByProvenance(raw);
789
+ const s = hits.find(h => h.kind === 'source');
790
+ if (s) return s;
791
+ }
792
+ }
793
+ }
794
+ return null;
760
795
  }
761
796
 
762
797
  export function matchSinkOrSanitizer(calleeExpr) {
763
798
  if (!calleeExpr) return null;
764
799
  let calleeName = null;
765
- if (calleeExpr.kind === 'ident') calleeName = calleeExpr.name;
800
+ // R3 (PRD §5): the Go IR (and other string-callee parsers) represent a call
801
+ // target as a dotted STRING ("db.Query") rather than a member expr. Match on
802
+ // the last segment so those languages' sinks/sanitizers are recognized too.
803
+ if (typeof calleeExpr === 'string') calleeName = calleeExpr.includes('.') ? calleeExpr.slice(calleeExpr.lastIndexOf('.') + 1) : calleeExpr;
804
+ else if (calleeExpr.kind === 'ident') calleeName = calleeExpr.name;
766
805
  else if (calleeExpr.kind === 'member') calleeName = calleeExpr.prop;
767
806
  if (!calleeName) return null;
768
807
  const raw = CALLEE_INDEX.get(calleeName);
@@ -39,6 +39,7 @@ import { aliasesForVar } from './points-to.js';
39
39
  import { higherOrderTaintFlow } from './higher-order.js';
40
40
  import { SummaryCache, entryStateFromCall } from './summaries.js';
41
41
  import { lookupBuiltinSummary } from './builtin-summaries.js';
42
+ import { isImplicitFlowEnabled, buildImplicitContext, implicitAssignTarget, markImplicitTaint, createImplicitFinding } from './implicit-flow.js';
42
43
 
43
44
  // v0.70 #2 — addPath that also taints every alias of the variable.
44
45
  // When `target` is a dotted path like "a.x" and the root `a` has aliases
@@ -157,6 +158,13 @@ function exprIsSource(expr) {
157
158
  const hit = matchSource(expr);
158
159
  if (hit) return hit;
159
160
  }
161
+ // R3 (PRD §5): call-shaped sources (r.FormValue(), r.URL.Query(), c.Query()).
162
+ // Previously only member reads were recognized, so Go's call-style sources
163
+ // never tainted the assignment target. matchSource now resolves call sources.
164
+ if (expr.kind === 'call') {
165
+ const hit = matchSource(expr);
166
+ if (hit) return hit;
167
+ }
160
168
  if (expr.kind === 'member' && expr.object) {
161
169
  return exprIsSource(expr.object);
162
170
  }
@@ -383,6 +391,20 @@ function step(node, stateIn, callContext) {
383
391
  });
384
392
  }
385
393
  }
394
+ // R4 (PRD §5): array-element taint. A mutating array method (push/unshift/
395
+ // splice/fill/copyWithin) called with a tainted argument taints the
396
+ // receiver array; an index read (a[0] → access path "a.0") is then covered
397
+ // by the receiver prefix. Object-property taint already flows via the
398
+ // access-path lattice — this closes the array case.
399
+ if (node.callee && node.callee.kind === 'member' && typeof node.callee.prop === 'string'
400
+ && /^(?:push|unshift|splice|fill|copyWithin)$/.test(node.callee.prop)
401
+ && Array.isArray(argTaints) && argTaints.some(Boolean)) {
402
+ // Mutate the state Set IN PLACE (the binding is const; the call case
403
+ // returns this same Set ref). Avoids touching the unrelated mutated-param
404
+ // paths in this case, keeping the blast radius to array-element taint only.
405
+ const _arrRecv = accessPathOf(node.callee.object);
406
+ if (_arrRecv) state.add(_arrRecv);
407
+ }
386
408
  if (cat) {
387
409
  for (const e of cat) {
388
410
  if (e.kind === 'sink' && (
@@ -493,6 +515,11 @@ function step(node, stateIn, callContext) {
493
515
  // control flow) can otherwise hold past the global timeout.
494
516
  function analyzeFunction(fn, entryState, callContext) {
495
517
  const nodes = fn.cfg.nodes;
518
+ // R2 (PRD §5): set the call-string caller context to THIS function while its
519
+ // worklist computes callee summaries (so a callee is keyed by its caller).
520
+ // No-op for the key unless AGENTIC_SECURITY_KCFA_CALLSTRING=1. Restored below.
521
+ const _prevCallerCtx = (callContext && callContext._summaryCache && callContext._summaryCache.setCallerContext)
522
+ ? callContext._summaryCache.setCallerContext(fn.qid) : undefined;
496
523
  const work = [];
497
524
  const inStates = new Map();
498
525
  const outStates = new Map();
@@ -535,6 +562,47 @@ function analyzeFunction(fn, entryState, callContext) {
535
562
  }
536
563
  }
537
564
 
565
+ // R4 (PRD §5) — implicit / control-dependence flow. OPT-IN (default OFF, see
566
+ // isImplicitFlowEnabled). Post-pass over the converged CFG: a sink reached
567
+ // INSIDE a tainted-condition branch can leak information even when its
568
+ // argument is constant or only implicitly tainted (a var assigned in that
569
+ // branch). Findings carry implicit:true + capped confidence.
570
+ if (isImplicitFlowEnabled() && fn.cfg) {
571
+ try {
572
+ const union = new Set();
573
+ for (const s of inStates.values()) for (const p of s) union.add(p);
574
+ const ictx = buildImplicitContext(fn.cfg, (expr) => exprTaint(expr, union));
575
+ // Mark vars assigned inside a tainted branch as implicit-tainted.
576
+ let implicitState = new Set();
577
+ for (const [nid, ctx] of ictx) {
578
+ const t = implicitAssignTarget(nodes[nid], ctx);
579
+ if (t) implicitState = markImplicitTaint(implicitState, t);
580
+ }
581
+ // A sink in a tainted branch whose arg is implicit-tainted (or constant)
582
+ // — and NOT already explicitly tainted (the normal pass covers that).
583
+ for (const [nid, ctx] of ictx) {
584
+ const node = nodes[nid];
585
+ if (!node || node.kind !== 'call') continue;
586
+ const cat = matchSinkOrSanitizer(node.callee);
587
+ const sink = cat && cat.find((e) => e.kind === 'sink');
588
+ if (!sink) continue;
589
+ const inS = inStates.get(nid) || new Set();
590
+ if ((node.args || []).some((a) => exprTaint(a, inS))) continue;
591
+ const argRefsImplicit = (node.args || []).some((a) => {
592
+ const ap = accessPathOf(a); return ap && isCoveredBy(implicitState, `implicit:${ap}`);
593
+ });
594
+ const allConst = (node.args || []).length > 0 && (node.args || []).every((a) => a && a.kind === 'literal');
595
+ if (argRefsImplicit || allConst) {
596
+ callContext._findings.push({
597
+ ...createImplicitFinding(node, ctx.conditionLabel),
598
+ _funcQid: fn.qid, sinkId: sink.id,
599
+ cwe: (sink.vuln && sink.vuln.cwe) || 'CWE-200',
600
+ });
601
+ }
602
+ }
603
+ } catch { /* implicit flow is best-effort + opt-in */ }
604
+ }
605
+
538
606
  const exit = outStates.get(fn.cfg.exit) || new Set();
539
607
  // v0.66 — record which params are tainted at function exit so the
540
608
  // caller's applyAtCallSite can propagate that mutated taint back. We
@@ -546,6 +614,10 @@ function analyzeFunction(fn, entryState, callContext) {
546
614
  if (isCoveredBy(exit, p)) callContext._mutatedParamsOut.add(p);
547
615
  }
548
616
  }
617
+ // R2: restore the caller context for the enclosing function's analysis.
618
+ if (_prevCallerCtx !== undefined && callContext && callContext._summaryCache && callContext._summaryCache.setCallerContext) {
619
+ callContext._summaryCache.setCallerContext(_prevCallerCtx);
620
+ }
549
621
  return exit;
550
622
  }
551
623
 
@@ -749,7 +821,10 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
749
821
  cwe: f.cwe,
750
822
  remediation: f.remediation,
751
823
  parser: 'IR-TAINT',
752
- confidence: 0.75,
824
+ // R4 implicit-flow: preserve the implicit flag + its capped confidence
825
+ // (a control-dependence finding, not an explicit data-flow one).
826
+ confidence: (f.implicit && typeof f.confidence === 'number') ? f.confidence : 0.75,
827
+ ...(f.implicit === true ? { implicit: true } : {}),
753
828
  source: f.trace && f.trace.length ? {
754
829
  file: fn.file,
755
830
  line: f.trace[0].line,
@@ -34,8 +34,10 @@
34
34
  import { addPath } from './access-paths.js';
35
35
 
36
36
  export function isImplicitFlowEnabled() {
37
- if (process.env.AGENTIC_SECURITY_IMPLICIT_FLOW === '0') return false;
38
- return true;
37
+ // OPT-IN (default OFF): implicit/control-dependence flow is famously noisy, so
38
+ // it must be explicitly requested. Findings carry implicit:true + capped
39
+ // confidence so the standard pipeline ranks them below explicit flows.
40
+ return process.env.AGENTIC_SECURITY_IMPLICIT_FLOW === '1';
39
41
  }
40
42
 
41
43
  /**
@@ -48,12 +48,17 @@ export function runDeepAnalysis(perFileIR, callGraph, opts = {}) {
48
48
  scanner: opts.scannerVersion || 'unknown',
49
49
  rules: opts.rulesDigest || `catalog:${_catalogSize()}`,
50
50
  };
51
+ // Compute current file hashes UNCONDITIONALLY — the first scan (and any
52
+ // scan after a version/rule invalidation) must persist a baseline so the
53
+ // NEXT scan has something to reuse. This previously lived inside the
54
+ // valid-state branch, so a cold cache never committed and incremental was
55
+ // a permanent no-op (the commit guard at the bottom keys on currentFileHashes).
56
+ currentFileHashes = {};
57
+ for (const [fp, content] of Object.entries(opts.fileContents)) {
58
+ currentFileHashes[fp] = hashFileContent(content);
59
+ }
51
60
  const valid = validateIncrementalState(priorState, currentVersion);
52
61
  if (valid.valid) {
53
- currentFileHashes = {};
54
- for (const [fp, content] of Object.entries(opts.fileContents)) {
55
- currentFileHashes[fp] = hashFileContent(content);
56
- }
57
62
  const diff = diffFileHashes(priorState.files || {}, currentFileHashes);
58
63
  const changedQids = new Set();
59
64
  // Map a changed file to the qids it owns. perFileIR exposes file→fns.
@@ -76,6 +76,11 @@ export function annotateProofGate(findings, opts = {}) {
76
76
 
77
77
  // proven-clean | proven-infeasible → recall-preserving demotion.
78
78
  if (verdict === 'proven-clean') stats.provenClean++; else stats.provenInfeasible++;
79
+ // R13 (PRD §5): make "provably safe" a first-class, queryable verdict — not
80
+ // just a quiet confidence demotion. "We proved this can't fire" is a
81
+ // stronger, rarer claim than any confidence score, and lets the report
82
+ // optionally hide discharged findings (--hide-proven-safe).
83
+ f.provablySafe = true;
79
84
 
80
85
  if (typeof f.confidence === 'number') {
81
86
  f._confidenceBeforeProofGate = f.confidence;
@@ -64,12 +64,30 @@ export class SummaryCache {
64
64
  const envCap = Number(process.env.AGENTIC_SECURITY_KCFA_MAX_CONTEXTS);
65
65
  this._maxContextsPerFn = Number.isFinite(envCap) && envCap >= 0 ? envCap : 16;
66
66
  this._contextCapHits = 0;
67
+ // R2 (PRD §5): bounded k=1 call-string sensitivity. OPT-IN (default OFF).
68
+ // When on, the cache key is extended with the immediate caller's qid, so two
69
+ // call paths reaching a helper with the SAME tainted-arg shape get DISTINCT
70
+ // summaries instead of sharing one (removes the over-merge that the pure
71
+ // value-context cache produces). Off by default → keys are byte-identical to
72
+ // before, so no behavior change for the shipped engine.
73
+ this._callString = process.env.AGENTIC_SECURITY_KCFA_CALLSTRING === '1';
74
+ this._callerCtx = null;
75
+ }
76
+
77
+ // Set the immediate-caller qid for call-string keying; returns the previous
78
+ // value so the engine can save/restore it around nested callee analysis.
79
+ setCallerContext(callerQid) {
80
+ const prev = this._callerCtx;
81
+ this._callerCtx = callerQid || null;
82
+ return prev;
67
83
  }
68
84
 
69
85
  _key(qid, taintedParams, receiverType) {
70
86
  // P1.2: when a receiver type is provided, extend the cache key with
71
87
  // its hash. Backward-compatible: no receiverType → same key as before.
72
- const base = `${qid}::${_hashState(taintedParams)}`;
88
+ let base = `${qid}::${_hashState(taintedParams)}`;
89
+ // R2: append the caller qid when call-string sensitivity is enabled.
90
+ if (this._callString && this._callerCtx) base = `${base}@${this._callerCtx}`;
73
91
  if (!receiverType) return base;
74
92
  return `${base}::${hashReceiverType(receiverType)}`;
75
93
  }
package/src/engine.js CHANGED
@@ -35,6 +35,10 @@ import { scanMobileManifest } from './sast/mobile-manifest.js';
35
35
  import { scanQuarkusHardening } from './sast/quarkus-hardening.js';
36
36
  import { scanFastapiHardening } from './sast/fastapi-hardening.js';
37
37
  import { scanAuthZ } from './sast/authz.js';
38
+ import { scanApiBrokenAuthz } from './sast/api-authz.js';
39
+ import { scanTerraform } from './sast/iac-terraform.js';
40
+ import { scanCrossService } from './sast/cross-service.js';
41
+ import { scanRbacConsistency } from './sast/rbac-consistency.js';
38
42
  import { scanModelLoad } from './sast/model-load.js';
39
43
  import { scanPromptTemplate } from './sast/prompt-template.js';
40
44
  import { scanXXE } from './sast/xxe.js';
@@ -81,7 +85,7 @@ import { scanLDAPInjection } from './sast/ldap-injection.js';
81
85
  import { scanXPathInjection } from './sast/xpath-injection.js';
82
86
  import { scanSSTI } from './sast/ssti.js';
83
87
  import { scanOpenRedirect } from './sast/open-redirect.js';
84
- import { scanWrongContextSanitizer } from './sast/wrong-context-sanitizer.js';
88
+ import { scanWrongContextSanitizer, scanSanitizerContextMismatch } from './sast/wrong-context-sanitizer.js';
85
89
  import { scanFrontendHygiene } from './sast/frontend-hygiene.js';
86
90
  import { scanCsvInjection } from './sast/csv-injection.js';
87
91
  import { scanStoredTaint } from './sast/stored-taint.js';
@@ -99,6 +103,9 @@ import { scanResponseSplitting } from './sast/response-splitting.js';
99
103
  import { scanStoredPromptInjection, scanStoredPromptInjectionCrossFile } from './sast/llm-stored-prompt.js';
100
104
  import { scanRAGPoisoning } from './sast/rag-poisoning.js';
101
105
  import { scanAgentToolEscalation } from './sast/agent-tool-escalation.js';
106
+ import { scanAgentUntrustedFlow } from './sast/agent-untrusted-flow.js';
107
+ import { scanInstallScripts } from './sca/install-script-analysis.js';
108
+ import { scanEventEntrypoints } from './sast/event-entrypoint.js';
102
109
  import { scanDbTaint, scanDbTaintCrossFile } from './sast/db-taint.js';
103
110
  import { scanSSRFCloudMetadata } from './sast/ssrf-cloud-metadata.js';
104
111
  import { scanMutationXSS } from './sast/mutation-xss.js';
@@ -143,6 +150,8 @@ import { annotateFeatureFlagGating } from './posture/feature-flags.js';
143
150
  import { annotatePersonaScores } from './posture/persona-prioritization.js';
144
151
  import { annotateMitigationComposite } from './posture/mitigation-composite.js';
145
152
  import { annotateCompositeRisk } from './posture/composite-risk.js';
153
+ import { annotateScaVerdicts } from './posture/sca-verdict.js';
154
+ import { annotateFindingProvenance } from './posture/provenance.js';
146
155
 
147
156
  // ── Integration block: world-class scaffolded modules ──────────────────────
148
157
  // Each module is opt-in via its own env var so partial adoption is safe.
@@ -6691,6 +6700,42 @@ function _parseMavenDependencyTree(text, filePath){
6691
6700
  return out;
6692
6701
  }
6693
6702
 
6703
+ // R10 (PRD §5): Gradle transitive graph. `gradle dependencies` (or
6704
+ // `gradle :app:dependencies`) prints the FULL resolved tree; build.gradle alone
6705
+ // only declares direct deps. Canonical committed path: `gradle-dependencies.txt`.
6706
+ // Format (tree-drawing prefixes we strip), version may be conflict-resolved:
6707
+ // +--- group:artifact:1.0
6708
+ // | \--- group:artifact:1.0 -> 1.2 (resolved to 1.2)
6709
+ // Config headers ("compileClasspath - …"), project deps, and (*)/(c) markers skipped.
6710
+ function _parseGradleDependencies(text, filePath) {
6711
+ const out = [];
6712
+ const seen = new Set();
6713
+ for (const rawLine of String(text).split('\n')) {
6714
+ if (!rawLine.trim()) continue;
6715
+ const indent = rawLine.length - rawLine.replace(/^[\s|+\\-]+/, '').length;
6716
+ const stripped = rawLine.replace(/^[\s|+\\-]+/, '').trim();
6717
+ if (!stripped || stripped.startsWith('project ') || /\bproject\s+[':]/.test(stripped)) continue;
6718
+ // group:artifact:version, with optional `-> resolved` and trailing (*)/(c)/(n).
6719
+ const m = stripped.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+):([^\s(]+?)(?:\s*->\s*([^\s(]+))?\s*(?:\([*cn]\))?\s*$/);
6720
+ if (!m) continue;
6721
+ const group = m[1], artifact = m[2];
6722
+ let ver = (m[4] || m[3] || '').replace(/[{}]/g, '');
6723
+ ver = ver.replace(/^(?:strictly|require|prefer)\s+/, '');
6724
+ if (!group || !artifact || !ver) continue;
6725
+ const key = `${group}:${artifact}:${ver}`;
6726
+ if (seen.has(key)) continue;
6727
+ seen.add(key);
6728
+ out.push({
6729
+ name: `${group}:${artifact}`, version: ver, group, scope: 'required',
6730
+ purl: _makePurl('maven', artifact, ver, group), ecosystem: 'maven', filePath,
6731
+ isUnpinned: false,
6732
+ isTransitive: indent > 6,
6733
+ pomSource: 'gradle-dependencies',
6734
+ });
6735
+ }
6736
+ return out;
6737
+ }
6738
+
6694
6739
  function _parseBuildGradle(text,filePath){
6695
6740
  const out=[];
6696
6741
  const pat=/(?:implementation|api|compile|runtimeOnly|testImplementation|testCompile|compileOnly|classpath)\s*(?:\(|['"])\s*['"]?([a-zA-Z0-9._\-]+):([a-zA-Z0-9._\-]+):([^'")\s,]+)/g;
@@ -6969,6 +7014,8 @@ function parseManifests(allFileContents){
6969
7014
  // `target/dependency-tree.txt` (per `mvn dependency:tree -DoutputFile=...`)
6970
7015
  // but users sometimes commit it as `dependency-tree.txt` at repo root.
6971
7016
  'dependency-tree.txt':_parseMavenDependencyTree,
7017
+ // R10: Gradle resolved transitive graph — `gradle dependencies > gradle-dependencies.txt`.
7018
+ 'gradle-dependencies.txt':_parseGradleDependencies,
6972
7019
  };
6973
7020
  const out=[],seen=new Set();
6974
7021
  for(const[fp,content]of Object.entries(allFileContents)){
@@ -7346,12 +7393,13 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7346
7393
  const _perFileTimeoutMs = parseInt(process.env.AGENTIC_SECURITY_PER_FILE_TIMEOUT_MS || '10000', 10);
7347
7394
  const _fileTimings = [];
7348
7395
  let _filesSkipped = 0, _filesTimedOut = 0, _filesDenseSkipped = 0;
7349
- const files=Object.keys(fileContents).filter(f=>shouldScan(f) && !_isPathIgnored(f));const fc={},pfr={};const aR=[],aF=[],aSrc=[],aSink=[],aSan=[],aLogic=[],aSupply=[],aSecrets=[],aCiphersRest=[],aCiphersTransit=[];let i=0;for(const p of files){i++;const _ft0=Date.now();setProgress({current:i,total:files.length,file:p.split("/").pop(),phase:"Scanning"});try{const c=fileContents[p];if(!c||c.length>500000){_filesSkipped++;continue;}const _avgLine=c.length/Math.max(c.split('\n').length,1);if(_avgLine>400&&c.length>10000){_filesDenseSkipped++;continue;}fc[p]=c;aR.push(...scanRoutes(p,c));const ta=performAnalysis(p,c);pfr[p]=ta;aF.push(...ta.findings);aSrc.push(...ta.sources);aSink.push(...ta.sinks);aSan.push(...ta.sanitizers);aLogic.push(...scanLogicVulns(p,c));aSecrets.push(...scanCredentials(p,c));aF.push(...scanStructuralVulns(p,c));aF.push(...scanExtraStructural(p,c));aF.push(...scanAliasedSinks(p,c));aF.push(...scanJavaSAST(p,c));aF.push(...scanJavaBenchExtras(p,c));aLogic.push(...scanMiddlewareOrdering(p,c));aLogic.push(...scanReDoS(p,c));if(/\.(?:java|cs|kt|py|php|phtml)$/i.test(p)){try{aLogic.push(...scanRegexReDoS(p,c));}catch(_){}}aLogic.push(...scanTodosNearSecurity(p,c));aSecrets.push(...scanEntropySecrets(p,c));const cp=scanCiphers(p,c);aCiphersRest.push(...cp.atRest);aCiphersTransit.push(...cp.inTransit);if(/\.(graphql|gql)$/i.test(p))aF.push(...scanGraphQL(p,c));aF.push(...scanIaC(p,c));
7396
+ const files=Object.keys(fileContents).filter(f=>shouldScan(f) && !_isPathIgnored(f));const fc={},pfr={};const aR=[],aF=[],aSrc=[],aSink=[],aSan=[],aLogic=[],aSupply=[],aSecrets=[],aCiphersRest=[],aCiphersTransit=[];let i=0;for(const p of files){i++;const _ft0=Date.now();setProgress({current:i,total:files.length,file:p.split("/").pop(),phase:"Scanning"});try{const c=fileContents[p];if(!c||c.length>500000){_filesSkipped++;continue;}const _avgLine=c.length/Math.max(c.split('\n').length,1);if(_avgLine>400&&c.length>10000){_filesDenseSkipped++;continue;}fc[p]=c;aR.push(...scanRoutes(p,c));const ta=performAnalysis(p,c);pfr[p]=ta;aF.push(...ta.findings);aSrc.push(...ta.sources);aSink.push(...ta.sinks);aSan.push(...ta.sanitizers);aLogic.push(...scanLogicVulns(p,c));aSecrets.push(...scanCredentials(p,c));aF.push(...scanStructuralVulns(p,c));aF.push(...scanExtraStructural(p,c));aF.push(...scanAliasedSinks(p,c));aF.push(...scanJavaSAST(p,c));aF.push(...scanJavaBenchExtras(p,c));aLogic.push(...scanMiddlewareOrdering(p,c));aLogic.push(...scanReDoS(p,c));if(/\.(?:java|cs|kt|py|php|phtml)$/i.test(p)){try{aLogic.push(...scanRegexReDoS(p,c));}catch(_){}}aLogic.push(...scanTodosNearSecurity(p,c));aSecrets.push(...scanEntropySecrets(p,c));const cp=scanCiphers(p,c);aCiphersRest.push(...cp.atRest);aCiphersTransit.push(...cp.inTransit);if(/\.(graphql|gql)$/i.test(p))aF.push(...scanGraphQL(p,c));aF.push(...scanIaC(p,c));aF.push(...scanTerraform(p,c));
7350
7397
  aF.push(...scanLLM(p,c));
7351
7398
  aF.push(...scanLLMOwasp(p,c));
7352
7399
  aLogic.push(...scanBusinessLogic(p,c));
7353
7400
  aF.push(...scanPipeline(p,c));
7354
7401
  aF.push(...scanContainer(p,c));
7402
+ aF.push(...scanInstallScripts(p,c));
7355
7403
  aF.push(...scanMCP(p,c));
7356
7404
  aF.push(...scanClaudeSettings(p,c));
7357
7405
  aF.push(...scanClaudeMdPromptInjection(p,c));
@@ -7409,6 +7457,7 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7409
7457
  aF.push(...scanSSTI(p,c));
7410
7458
  aF.push(...scanOpenRedirect(p,c));
7411
7459
  aF.push(...scanWrongContextSanitizer(p,c));
7460
+ aF.push(...scanSanitizerContextMismatch(p,c));
7412
7461
  aF.push(...scanFrontendHygiene(p,c));
7413
7462
  aF.push(...scanCsvInjection(p,c));
7414
7463
  aF.push(...scanStoredTaint(p,c));
@@ -7424,6 +7473,8 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7424
7473
  aF.push(...scanStoredPromptInjection(p,c));
7425
7474
  aF.push(...scanRAGPoisoning(p,c));
7426
7475
  aF.push(...scanAgentToolEscalation(p,c));
7476
+ aF.push(...scanAgentUntrustedFlow(p,c));
7477
+ aF.push(...scanEventEntrypoints(p,c));
7427
7478
  aF.push(...scanDbTaint(p,c));
7428
7479
  aF.push(...scanSSRFCloudMetadata(p,c));
7429
7480
  aF.push(...scanMutationXSS(p,c));
@@ -7585,6 +7636,12 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7585
7636
  setProgress({current:i,total:files.length,file:"Stored taint...",phase:"Linking"});const storedRegistry=buildStoredTaintRegistry(fc);const stf=crossStoredTaint(fc,storedRegistry);aF.push(...stf);
7586
7637
  setProgress({current:i,total:files.length,file:"Session taint...",phase:"Linking"});const sess=crossSessionTaint(fc);aF.push(...sess);
7587
7638
  setProgress({current:i,total:files.length,file:"Call graph...",phase:"Linking"});const callGraph=buildCallGraph(fc);
7639
+ // R19 (PRD §5): cross-route BOLA/BFLA over the aggregated route inventory.
7640
+ try{aF.push(...scanApiBrokenAuthz(aR));}catch(_){}
7641
+ // R22 (PRD §5): cross-service edges inferred from code (client call → matched route).
7642
+ try{aF.push(...scanCrossService(aR,fc));}catch(_){}
7643
+ // R21 (PRD §5): RBAC role-tier consistency over the route inventory.
7644
+ try{aF.push(...scanRbacConsistency(aR,fc));}catch(_){}
7588
7645
  setProgress({current:i,total:files.length,file:"Reachability + guards...",phase:"Linking"});annotateReachability(aF,aR,callGraph,fc);aF.forEach(f=>detectGuardsForFinding(f,fc));
7589
7646
  setProgress({current:i,total:files.length,file:"Inferring sanitizers...",phase:"Linking"});const learned=inferSanitizers(fc);applyLearnedSanitizers(aF,learned,fc);
7590
7647
  setProgress({current:i,total:files.length,file:"Sanitizer effectiveness...",phase:"Linking"});applySanitizerEffectiveness(aF);
@@ -7593,6 +7650,9 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7593
7650
  setProgress({current:i,total:files.length,file:"OSV vulnerability database...",phase:"SCA"});
7594
7651
  const allFileContents={...fc, ...depFileContents};
7595
7652
  const components=parseManifests(allFileContents);
7653
+ // R8 (PRD §5): OS packages from an extracted container image's package DBs
7654
+ // (dpkg/apk) — baked-in deps the Dockerfile never names. Feed the OSV/SBOM pipeline.
7655
+ try{const{extractImagePackages}=await import('./sca/image-packages.js');for(const ip of extractImagePackages(allFileContents)){if(!components.some(c=>c.ecosystem===ip.ecosystem&&c.name===ip.name&&c.version===ip.version))components.push(ip);}}catch(_){}
7596
7656
  try{const{detectVendoredLibraries}=await import('./sca/vendor-detect.js');const vendored=detectVendoredLibraries(fc);for(const v of vendored){const key=`${v.ecosystem}:${v.name}:${v.version}`;if(!components.some(c=>`${c.ecosystem}:${c.name}:${c.version}`===key))components.push({...v,group:'',purl:`pkg:${v.ecosystem}/${v.name}@${v.version}`,filePath:v.file,isUnpinned:false,reachable:true});}}catch(_){}
7597
7657
  const reach=buildReachabilitySet(fc);
7598
7658
  const reachabilitySet=reach.imported;
@@ -7607,6 +7667,14 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7607
7667
  // 0.10.0: enrich SCA findings with CISA KEV (CISA KEV catalog)
7608
7668
  try{supplyChain=await _enrichWithKEV(supplyChain);}catch(_){}
7609
7669
  try{markUsedVulnFunctions(supplyChain,fc);}catch(_){}
7670
+ // R7 (PRD §5): import-aware call-site augmentation for npm/pypi deps. The regex
7671
+ // pass above misses aliased / namespace imports (import {merge as deepMerge};
7672
+ // deepMerge()) and over-matches bare names in files that don't import the
7673
+ // package. This parses each JS/TS + Python file's imports, resolves vuln-fn
7674
+ // calls THROUGH the alias map, and gates them to files that actually import the
7675
+ // package — then ADDS the recovered sites so the route-reachability classifier
7676
+ // below (which decides reachable/unreachable/unknown) sees them. Additive only.
7677
+ try{const{augmentReachabilityViaImports}=await import('./sca/import-reachability.js');augmentReachabilityViaImports(supplyChain,fc,VULN_FUNCTION_HINTS);}catch(_){}
7610
7678
  // Python AST-based function validation (deep mode only)
7611
7679
  if(process.env.AGENTIC_SECURITY_DEEP==='1'){try{const{validateOsvFunctionsExist}=await import('./sca/py-package-functions.js');for(const sc of supplyChain){if(sc.type!=='vulnerable_dep'||sc.ecosystem!=='pypi')continue;if(!sc.osvVulnFunctions||!sc.osvVulnFunctions.length)continue;const{validated,missing}=validateOsvFunctionsExist(sc.name,sc.osvVulnFunctions,scanRoot);if(validated.length)sc._pyAstValidated=validated;if(missing.length)sc._pyAstMissing=missing;}}catch(_){}}
7612
7680
  // LLM-assisted function extraction for CVEs without hints (opt-in: AGENTIC_SECURITY_LLM_SCA=1)
@@ -8050,10 +8118,14 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
8050
8118
  try { await annotateNarration(finalFindings); }
8051
8119
  catch (e) { _annotatorErrors.push({ phase: 'annotateNarration', err: String((e && e.message) || e) }); }
8052
8120
  // Phase 3 (Sentinel-parity FR-L1, FR-L2) — IR + interprocedural taint.
8053
- // Opt-in via AGENTIC_SECURITY_DEEP=1 because it's currently breadth-first,
8054
- // not benchmark-tuned. Findings ride through the standard dedup/cluster/
8055
- // confidence pipeline below; the LLM-validator stage above already ran but
8056
- // any deep-mode finding emitted here will be unvalidated.
8121
+ // R1 (PRD §5): the CLI entry (bin/agentic-security.js#cmdScan) now sets
8122
+ // AGENTIC_SECURITY_DEEP=1 by default for local/interactive scans, so deep mode
8123
+ // runs on the default `/scan --all` path. This gate is the enforcement +
8124
+ // CI-safety point: it honors an explicit opt-out (DEEP=0) and keeps deep off in
8125
+ // CI unless DEEP_IN_CI=1. In-process callers (tests, the cve-replay corpus) invoke
8126
+ // runScan()/runFullScan() directly without the CLI default, so they stay deep-off
8127
+ // and remain deterministic regression gates. Findings ride through the standard
8128
+ // dedup/cluster/confidence pipeline below and the LLM-validator stage that follows.
8057
8129
  //
8058
8130
  // SAFETY: Deep mode is gated for CI safety:
8059
8131
  // - Global timeout via AGENTIC_SECURITY_DEEP_TIMEOUT_MS (default 300_000 = 5 min)
@@ -8195,13 +8267,29 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
8195
8267
  // apply accept-risk / SLA / major-version-freeze rules. supplyChain
8196
8268
  // findings get suppressed/tagged in place.
8197
8269
  try { const sp = loadScaPolicy(scanRoot); if (sp && !sp._error) applyScaPolicy(supplyChain, sp); } catch (_) {}
8270
+ // R12 (PRD §5): deterministic decision-first verdict per dep, derived from the
8271
+ // signals enriched above (composite risk × KEV × EPSS × reachability × policy).
8272
+ // testsDetected is read from the UNFILTERED fileContents because shouldScan()
8273
+ // strips test files out of `fc`.
8274
+ try {
8275
+ const _testsDetected = Object.keys(fileContents).some(f =>
8276
+ /(?:^|\/)(?:tests?|spec|__tests__)\//i.test(f) ||
8277
+ /\.(?:test|spec)\.[cm]?[jt]sx?$/i.test(f) ||
8278
+ /(?:^|\/)test_[^/]*\.py$/i.test(f) || /_test\.(?:py|go)$/i.test(f));
8279
+ annotateScaVerdicts(supplyChain, { testsDetected: _testsDetected });
8280
+ } catch (_) {}
8198
8281
  // 0.9.0 Feat-15: dep confusion
8199
8282
  try{const dc=detectDepConfusion(annotatedComponents,scanRoot);aF.push(...dc);}catch(_){}
8200
8283
  // Deployment-platform security checklist
8201
8284
  try{const dpf=scanDeployPlatform(scanRoot);aLogic.push(...dpf);}catch(_){}
8202
8285
  // Stack-specific security playbook
8203
8286
  try{const sp=runStackPlaybook(scanRoot);if(sp&&sp.findings)aLogic.push(...sp.findings);}catch(_){}
8204
- finalFindings.sort((a,b)=>(b.toxicityScore||0)-(a.toxicityScore||0)||(b.triageScore||0)-(a.triageScore||0)||({critical:0,high:1,medium:2,low:3}[a.severity]??4)-({critical:0,high:1,medium:2,low:3}[b.severity]??4));
8287
+ // R17 (PRD §5): annotate per-finding corroboration ("one issue, many signals")
8288
+ // after evidence + validator verdicts are populated, then use it as a final
8289
+ // tiebreaker so multi-signal findings rank above single-signal ones at equal
8290
+ // severity. Does not alter calibrated confidence.
8291
+ try { annotateFindingProvenance(finalFindings); } catch (_) {}
8292
+ finalFindings.sort((a,b)=>(b.toxicityScore||0)-(a.toxicityScore||0)||(b.triageScore||0)-(a.triageScore||0)||({critical:0,high:1,medium:2,low:3}[a.severity]??4)-({critical:0,high:1,medium:2,low:3}[b.severity]??4)||((b.corroborationCount||1)-(a.corroborationCount||1)));
8205
8293
  // Auto-PoC filter: tag whether a concrete payload+test can be derived. When
8206
8294
  // AGENTIC_SECURITY_POC=1, demote ≥medium findings that fail this check.
8207
8295
  // Used as a precision lever for users who want to ship clean reports —
@@ -79,6 +79,14 @@ function _lowerExpr(text) {
79
79
  const parts = _splitTopLevelCommas(inner).map(_lowerExpr);
80
80
  return { kind: 'tpl', parts };
81
81
  }
82
+ // R3 (PRD §5): string concat with `+` MUST be checked before the bare-literal
83
+ // rule below — otherwise `"SELECT " + q` (starts with `"`) is captured whole as
84
+ // a single literal and taint cannot flow through the concatenation. Only fires
85
+ // when there's a top-level `+` (a pure literal still falls through to literal).
86
+ if (/["'`]/.test(s) && s.includes('+')) {
87
+ const plusParts = _splitTopLevelPlus(s);
88
+ if (plusParts.length > 1) return { kind: 'tpl', parts: plusParts.map(_lowerExpr) };
89
+ }
82
90
  if (/^"/.test(s) || /^`/.test(s)) return { kind: 'literal', value: s };
83
91
  if (/^\d/.test(s)) return { kind: 'literal', value: s };
84
92
  if (/^(true|false|nil)\b/.test(s)) return { kind: 'literal', value: s };
package/src/mcp/tools.js CHANGED
@@ -16,7 +16,7 @@ import * as fs from 'node:fs';
16
16
  import * as fsp from 'node:fs/promises';
17
17
  import * as path from 'node:path';
18
18
  import * as crypto from 'node:crypto';
19
- import { applyFix as applyFixHistory } from '../posture/fix-history.js';
19
+ import { applyFix as applyFixHistory, fixAcceptanceRate } from '../posture/fix-history.js';
20
20
  import { verifyLastScan } from '../posture/integrity.js';
21
21
  import { redactString, redactFinding } from './redact.js';
22
22
 
@@ -578,7 +578,11 @@ export const apply_fix = {
578
578
  }
579
579
  throw e;
580
580
  }
581
- return { _meta: META, applied: true, historyId: entry.id, file: f.file, backupPath: entry.backupPath, integrity: status, attemptOrdinal: entry.attemptOrdinal };
581
+ // R25 (PRD §5): surface the running auto-fix acceptance rate after each
582
+ // applied fix, so the closed loop reports its own success metric.
583
+ let acceptance = null;
584
+ try { acceptance = fixAcceptanceRate(ctx.sessionRoot); } catch { /* metric is best-effort */ }
585
+ return { _meta: META, applied: true, historyId: entry.id, file: f.file, backupPath: entry.backupPath, integrity: status, attemptOrdinal: entry.attemptOrdinal, acceptance };
582
586
  },
583
587
  };
584
588