@clear-capabilities/agentic-security-scanner 0.74.0

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 (331) hide show
  1. package/CHANGELOG.md +1580 -0
  2. package/bin/.agentic-security/findings.json +1577 -0
  3. package/bin/.agentic-security/last-scan.json +1577 -0
  4. package/bin/.agentic-security/last-scan.json.sig +1 -0
  5. package/bin/.agentic-security/scan-history.json +465 -0
  6. package/bin/.agentic-security/streak.json +25 -0
  7. package/bin/agentic-security-audit.js +198 -0
  8. package/bin/agentic-security-consistency.js +80 -0
  9. package/bin/agentic-security-diff.js +136 -0
  10. package/bin/agentic-security-lsp.js +12 -0
  11. package/bin/agentic-security-mcp.js +40 -0
  12. package/bin/agentic-security-rule.js +153 -0
  13. package/bin/agentic-security.js +1683 -0
  14. package/dist/117.index.js +207 -0
  15. package/dist/178.index.js +250 -0
  16. package/dist/218.index.js +793 -0
  17. package/dist/227.index.js +192 -0
  18. package/dist/301.index.js +167 -0
  19. package/dist/384.index.js +18 -0
  20. package/dist/476.index.js +126 -0
  21. package/dist/513.index.js +373 -0
  22. package/dist/520.index.js +13 -0
  23. package/dist/601.index.js +1038 -0
  24. package/dist/634.index.js +1892 -0
  25. package/dist/637.index.js +216 -0
  26. package/dist/660.index.js +131 -0
  27. package/dist/675.index.js +451 -0
  28. package/dist/826.index.js +188 -0
  29. package/dist/830.index.js +133 -0
  30. package/dist/agentic-security.mjs +272 -0
  31. package/dist/agentic-security.mjs.sha256 +1 -0
  32. package/dist/calibration-seed.json +27 -0
  33. package/package.json +77 -0
  34. package/src/.agentic-security/findings.json +80844 -0
  35. package/src/.agentic-security/last-scan.json +80844 -0
  36. package/src/.agentic-security/last-scan.json.sig +1 -0
  37. package/src/.agentic-security/scan-history.json +8408 -0
  38. package/src/.agentic-security/streak.json +26 -0
  39. package/src/badge.js +188 -0
  40. package/src/compare.js +203 -0
  41. package/src/dataflow/.agentic-security/findings.json +3487 -0
  42. package/src/dataflow/.agentic-security/last-scan.json +3487 -0
  43. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
  44. package/src/dataflow/.agentic-security/scan-history.json +735 -0
  45. package/src/dataflow/.agentic-security/streak.json +24 -0
  46. package/src/dataflow/CLAUDE.md +38 -0
  47. package/src/dataflow/access-paths.js +172 -0
  48. package/src/dataflow/async-sequencing.js +177 -0
  49. package/src/dataflow/backward.js +201 -0
  50. package/src/dataflow/catalog-expanded.js +485 -0
  51. package/src/dataflow/catalog.js +659 -0
  52. package/src/dataflow/cross-repo.js +219 -0
  53. package/src/dataflow/engine.js +588 -0
  54. package/src/dataflow/exception-flow.js +116 -0
  55. package/src/dataflow/exploit-prover.js +187 -0
  56. package/src/dataflow/higher-order.js +221 -0
  57. package/src/dataflow/ifds.js +347 -0
  58. package/src/dataflow/implicit-flow.js +129 -0
  59. package/src/dataflow/incremental.js +229 -0
  60. package/src/dataflow/index.js +181 -0
  61. package/src/dataflow/numeric-domain.js +192 -0
  62. package/src/dataflow/path-feasibility.js +114 -0
  63. package/src/dataflow/points-to.js +337 -0
  64. package/src/dataflow/polyglot.js +190 -0
  65. package/src/dataflow/proven-clean.js +159 -0
  66. package/src/dataflow/receiver-context.js +76 -0
  67. package/src/dataflow/sanitizer-proof.js +154 -0
  68. package/src/dataflow/soft-taint.js +140 -0
  69. package/src/dataflow/string-domain.js +234 -0
  70. package/src/dataflow/stub-aware-filter.js +100 -0
  71. package/src/dataflow/summaries.js +132 -0
  72. package/src/dataflow/symbolic-exec.js +238 -0
  73. package/src/dataflow/tabulation.js +135 -0
  74. package/src/engine.js +7763 -0
  75. package/src/history-scan.js +229 -0
  76. package/src/index.js +3 -0
  77. package/src/integrations/.agentic-security/findings.json +1504 -0
  78. package/src/integrations/.agentic-security/last-scan.json +1504 -0
  79. package/src/integrations/.agentic-security/scan-history.json +40 -0
  80. package/src/integrations/.agentic-security/streak.json +21 -0
  81. package/src/integrations/index.js +321 -0
  82. package/src/integrations/tickets.js +200 -0
  83. package/src/ir/.agentic-security/findings.json +3036 -0
  84. package/src/ir/.agentic-security/last-scan.json +3036 -0
  85. package/src/ir/.agentic-security/last-scan.json.sig +1 -0
  86. package/src/ir/.agentic-security/scan-history.json +364 -0
  87. package/src/ir/.agentic-security/streak.json +23 -0
  88. package/src/ir/CLAUDE.md +172 -0
  89. package/src/ir/callgraph.js +73 -0
  90. package/src/ir/class-hierarchy.js +195 -0
  91. package/src/ir/index.js +152 -0
  92. package/src/ir/parser-cs.js +260 -0
  93. package/src/ir/parser-java.js +286 -0
  94. package/src/ir/parser-js.js +413 -0
  95. package/src/ir/parser-kt.js +258 -0
  96. package/src/ir/parser-py-cst.js +136 -0
  97. package/src/ir/parser-py.helper.py +501 -0
  98. package/src/ir/parser-py.js +312 -0
  99. package/src/ir/ssa.js +315 -0
  100. package/src/ir/type-stubs.js +288 -0
  101. package/src/leaderboard.js +152 -0
  102. package/src/llm-validator/.agentic-security/findings.json +1891 -0
  103. package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
  104. package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
  105. package/src/llm-validator/.agentic-security/scan-history.json +168 -0
  106. package/src/llm-validator/.agentic-security/streak.json +20 -0
  107. package/src/llm-validator/consistency.js +141 -0
  108. package/src/llm-validator/index.js +437 -0
  109. package/src/lsp/.agentic-security/findings.json +28 -0
  110. package/src/lsp/.agentic-security/last-scan.json +28 -0
  111. package/src/lsp/.agentic-security/scan-history.json +79 -0
  112. package/src/lsp/.agentic-security/streak.json +22 -0
  113. package/src/lsp/server.js +275 -0
  114. package/src/mcp/.agentic-security/findings.json +8358 -0
  115. package/src/mcp/.agentic-security/last-scan.json +8358 -0
  116. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  117. package/src/mcp/.agentic-security/scan-history.json +1125 -0
  118. package/src/mcp/.agentic-security/streak.json +22 -0
  119. package/src/mcp/CLAUDE.md +54 -0
  120. package/src/mcp/audit.js +136 -0
  121. package/src/mcp/redact.js +75 -0
  122. package/src/mcp/server.js +158 -0
  123. package/src/mcp/stdio.js +83 -0
  124. package/src/mcp/tools.js +940 -0
  125. package/src/mcp/validate.js +49 -0
  126. package/src/personality.js +164 -0
  127. package/src/poc-video.js +239 -0
  128. package/src/posture/.agentic-security/findings.json +51239 -0
  129. package/src/posture/.agentic-security/last-scan.json +51239 -0
  130. package/src/posture/.agentic-security/last-scan.json.sig +1 -0
  131. package/src/posture/.agentic-security/scan-history.json +5557 -0
  132. package/src/posture/.agentic-security/streak.json +24 -0
  133. package/src/posture/CLAUDE.md +42 -0
  134. package/src/posture/adversarial-self-test.js +114 -0
  135. package/src/posture/adversary-agent.js +204 -0
  136. package/src/posture/agents-memory.js +135 -0
  137. package/src/posture/ai-code-fingerprint.js +171 -0
  138. package/src/posture/aibom.js +284 -0
  139. package/src/posture/api-inventory.js +96 -0
  140. package/src/posture/attack-playbooks.js +305 -0
  141. package/src/posture/auditor-agent.js +115 -0
  142. package/src/posture/auth-posture-import.js +135 -0
  143. package/src/posture/baseline-compare.js +114 -0
  144. package/src/posture/blast-radius.js +836 -0
  145. package/src/posture/bounty-prediction.js +141 -0
  146. package/src/posture/business-logic.js +239 -0
  147. package/src/posture/calibration-drift.js +93 -0
  148. package/src/posture/calibration-seed.json +27 -0
  149. package/src/posture/calibration.js +204 -0
  150. package/src/posture/clustering.js +75 -0
  151. package/src/posture/concurrency-checker.js +265 -0
  152. package/src/posture/confidence.js +65 -0
  153. package/src/posture/container-runtime.js +149 -0
  154. package/src/posture/counterfactual.js +109 -0
  155. package/src/posture/cross-lang-graphql.js +165 -0
  156. package/src/posture/cross-lang-grpc.js +166 -0
  157. package/src/posture/cross-lang-meta.js +101 -0
  158. package/src/posture/cross-lang-openapi.js +187 -0
  159. package/src/posture/cross-lang-orm.js +153 -0
  160. package/src/posture/cross-lang-queues.js +210 -0
  161. package/src/posture/crown-jewels.js +110 -0
  162. package/src/posture/custom-rules.js +361 -0
  163. package/src/posture/cve-alert-daemon.js +433 -0
  164. package/src/posture/cve-lookup.js +129 -0
  165. package/src/posture/dead-code.js +430 -0
  166. package/src/posture/defender-agent.js +158 -0
  167. package/src/posture/deploy-platform.js +204 -0
  168. package/src/posture/detector-fuzz.js +61 -0
  169. package/src/posture/deterministic.js +99 -0
  170. package/src/posture/drift.js +165 -0
  171. package/src/posture/epss.js +156 -0
  172. package/src/posture/exploitability-probability.js +212 -0
  173. package/src/posture/exploitability.js +121 -0
  174. package/src/posture/feature-flags.js +110 -0
  175. package/src/posture/finding-defaults.js +132 -0
  176. package/src/posture/fix-history.js +411 -0
  177. package/src/posture/fix-plan.js +121 -0
  178. package/src/posture/fix-verify-loop.js +157 -0
  179. package/src/posture/fix-verify.js +130 -0
  180. package/src/posture/flow-narration.js +105 -0
  181. package/src/posture/grader-calibration.js +156 -0
  182. package/src/posture/harness-discovery.js +113 -0
  183. package/src/posture/holdout-eval.js +144 -0
  184. package/src/posture/iac-reachability.js +163 -0
  185. package/src/posture/iam-policy.js +128 -0
  186. package/src/posture/integrity.js +97 -0
  187. package/src/posture/learning.js +166 -0
  188. package/src/posture/license-policy.js +109 -0
  189. package/src/posture/llm-redteam-prompts.js +418 -0
  190. package/src/posture/llm-redteam.js +303 -0
  191. package/src/posture/material-change.js +163 -0
  192. package/src/posture/mitigation-composite.js +55 -0
  193. package/src/posture/mttr.js +91 -0
  194. package/src/posture/network-policy-import.js +126 -0
  195. package/src/posture/path-predicates.js +99 -0
  196. package/src/posture/persona-prioritization.js +153 -0
  197. package/src/posture/poc-cwe-map.js +51 -0
  198. package/src/posture/poc-generator.js +500 -0
  199. package/src/posture/policy-gate.js +174 -0
  200. package/src/posture/pre-incident-archaeology.js +110 -0
  201. package/src/posture/profile.js +93 -0
  202. package/src/posture/reachability-filter.js +42 -0
  203. package/src/posture/regression-test-gen.js +200 -0
  204. package/src/posture/reverse-blast-radius.js +110 -0
  205. package/src/posture/router.js +109 -0
  206. package/src/posture/rule-overrides.js +198 -0
  207. package/src/posture/rule-pack-signing.js +209 -0
  208. package/src/posture/rule-packs.js +143 -0
  209. package/src/posture/rule-synthesis.js +108 -0
  210. package/src/posture/ruleset-version.js +71 -0
  211. package/src/posture/sbom.js +129 -0
  212. package/src/posture/schema-aware-bridge.js +207 -0
  213. package/src/posture/security-trend.js +87 -0
  214. package/src/posture/semantic-clone.js +114 -0
  215. package/src/posture/specification-mining.js +170 -0
  216. package/src/posture/stable-id.js +75 -0
  217. package/src/posture/stack-playbook.js +229 -0
  218. package/src/posture/streak.js +249 -0
  219. package/src/posture/suppressions.js +135 -0
  220. package/src/posture/telemetry-ingest.js +112 -0
  221. package/src/posture/threat-model.js +145 -0
  222. package/src/posture/three-agent-pipeline.js +74 -0
  223. package/src/posture/triage.js +146 -0
  224. package/src/posture/trust-boundary-diagram.js +115 -0
  225. package/src/posture/type-narrowing.js +129 -0
  226. package/src/posture/validator-metrics.js +179 -0
  227. package/src/posture/verifier-ephemeral.js +118 -0
  228. package/src/posture/verifier-target.js +147 -0
  229. package/src/posture/verifier.js +257 -0
  230. package/src/posture/version.js +75 -0
  231. package/src/posture/waf-ingest.js +200 -0
  232. package/src/posture/why-fired.js +141 -0
  233. package/src/pr-comment.js +172 -0
  234. package/src/pr-delta.js +198 -0
  235. package/src/report/.agentic-security/findings.json +79 -0
  236. package/src/report/.agentic-security/last-scan.json +79 -0
  237. package/src/report/.agentic-security/last-scan.json.sig +1 -0
  238. package/src/report/.agentic-security/scan-history.json +332 -0
  239. package/src/report/.agentic-security/streak.json +23 -0
  240. package/src/report/index.js +1136 -0
  241. package/src/report/mascot.js +42 -0
  242. package/src/runScan.js +141 -0
  243. package/src/sast/.agentic-security/findings.json +5051 -0
  244. package/src/sast/.agentic-security/last-scan.json +5051 -0
  245. package/src/sast/.agentic-security/last-scan.json.sig +1 -0
  246. package/src/sast/.agentic-security/scan-history.json +788 -0
  247. package/src/sast/.agentic-security/streak.json +23 -0
  248. package/src/sast/CLAUDE.md +39 -0
  249. package/src/sast/_comment-strip.js +46 -0
  250. package/src/sast/agent-tool-escalation.js +131 -0
  251. package/src/sast/auth-provider.js +171 -0
  252. package/src/sast/authz.js +236 -0
  253. package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
  254. package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
  255. package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
  256. package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
  257. package/src/sast/bench-shape/index.js +62 -0
  258. package/src/sast/claude-hook-injection.js +199 -0
  259. package/src/sast/claude-md-prompt-injection.js +170 -0
  260. package/src/sast/claude-settings.js +165 -0
  261. package/src/sast/client-side.js +149 -0
  262. package/src/sast/cpp-bench-extras.js +122 -0
  263. package/src/sast/cpp-dataflow.js +430 -0
  264. package/src/sast/cpp.js +248 -0
  265. package/src/sast/csharp.js +152 -0
  266. package/src/sast/csrf.js +82 -0
  267. package/src/sast/dart-flutter.js +173 -0
  268. package/src/sast/db-rls.js +147 -0
  269. package/src/sast/db-taint.js +215 -0
  270. package/src/sast/defi-deep.js +242 -0
  271. package/src/sast/deserialization-gadgets.js +113 -0
  272. package/src/sast/django-hardening.js +230 -0
  273. package/src/sast/env-hygiene.js +125 -0
  274. package/src/sast/fastapi-hardening.js +145 -0
  275. package/src/sast/go-extended.js +84 -0
  276. package/src/sast/host-header.js +106 -0
  277. package/src/sast/index.js +17 -0
  278. package/src/sast/java-ast-folding.js +561 -0
  279. package/src/sast/java-bench-extras.js +708 -0
  280. package/src/sast/java-collection-passthrough.js +178 -0
  281. package/src/sast/java-constant-fold.js +244 -0
  282. package/src/sast/java-deserialization.js +125 -0
  283. package/src/sast/jndi.js +104 -0
  284. package/src/sast/juliet-shape.js +324 -0
  285. package/src/sast/jwt-exp.js +104 -0
  286. package/src/sast/kotlin.js +82 -0
  287. package/src/sast/laravel-hardening.js +198 -0
  288. package/src/sast/ldap-injection.js +100 -0
  289. package/src/sast/llm-owasp.js +465 -0
  290. package/src/sast/llm-stored-prompt.js +103 -0
  291. package/src/sast/llm-trading-agent.js +161 -0
  292. package/src/sast/llm.js +308 -0
  293. package/src/sast/logic.js +140 -0
  294. package/src/sast/mass-assignment.js +101 -0
  295. package/src/sast/mcp-audit.js +242 -0
  296. package/src/sast/mobile-manifest.js +195 -0
  297. package/src/sast/model-load.js +164 -0
  298. package/src/sast/mutation-xss.js +87 -0
  299. package/src/sast/nosql-injection.js +82 -0
  300. package/src/sast/open-redirect.js +119 -0
  301. package/src/sast/php.js +91 -0
  302. package/src/sast/pipeline.js +122 -0
  303. package/src/sast/primary-cwe-java.js +155 -0
  304. package/src/sast/prompt-firewall.js +151 -0
  305. package/src/sast/prompt-template.js +157 -0
  306. package/src/sast/prototype-pollution.js +112 -0
  307. package/src/sast/python-sinks.js +195 -0
  308. package/src/sast/quarkus-hardening.js +102 -0
  309. package/src/sast/rag-poisoning.js +118 -0
  310. package/src/sast/rate-limit.js +128 -0
  311. package/src/sast/response-splitting.js +138 -0
  312. package/src/sast/ruby.js +108 -0
  313. package/src/sast/rust.js +105 -0
  314. package/src/sast/solidity.js +167 -0
  315. package/src/sast/springboot-hardening.js +186 -0
  316. package/src/sast/ssrf-cloud-metadata.js +80 -0
  317. package/src/sast/ssti.js +116 -0
  318. package/src/sast/swift.js +162 -0
  319. package/src/sast/toctou.js +95 -0
  320. package/src/sast/webhook.js +101 -0
  321. package/src/sast/xpath-injection.js +51 -0
  322. package/src/sast/xxe.js +140 -0
  323. package/src/sast/zip-slip.js +200 -0
  324. package/src/sca/base-images.json +45 -0
  325. package/src/sca/container.js +107 -0
  326. package/src/sca/dep-confusion.js +134 -0
  327. package/src/sca/index.js +6 -0
  328. package/src/sca/popular-packages.json +41 -0
  329. package/src/sca/sarif-ingest.js +187 -0
  330. package/src/sca/vuln-function-hints.json +89 -0
  331. package/src/secrets/index.js +4 -0
@@ -0,0 +1,170 @@
1
+ // FR-LOGIC-8 — Specification mining.
2
+ //
3
+ // Extract the implicit specification of each function from five signal
4
+ // sources: (1) function name, (2) docstring / inline comments, (3) co-located
5
+ // tests, (4) call sites, (5) PR title + AI prompt (CLAUDE.md / system
6
+ // prompts). Flag implementations that drift from the inferred spec — the
7
+ // function named `validateOwnership` whose body never references the user
8
+ // identity is the canonical example.
9
+ //
10
+ // This is the bug class no pattern matcher reaches. The intent-drift recall
11
+ // target is F1 ≥ 0.70 (PRD G13). We expect noise on legacy code; the rule
12
+ // emits at LOW confidence and lets the active-learning loop tune.
13
+ //
14
+ // What we mine in v1:
15
+ // • name → expected body content (regex set per name family)
16
+ // • body content present? → if NO, emit drift finding
17
+ //
18
+ // Name families and required body evidence (each family lists the *kind* of
19
+ // thing the body must reference; missing means drift):
20
+ //
21
+ // validate*Ownership / check*Owner → references req.user / session / context user id
22
+ // validate*Access / authorize* → references roles / permissions / can / allowedTo
23
+ // sanitize* / escape* → references DOMPurify / escape / replace
24
+ // verify*Signature / check*Sig → references hmac / verify / crypto.createVerify / timingSafeEqual
25
+ // verify*Webhook → references webhook signature constant
26
+ // rateLimit* / throttle* → references express-rate-limit / Math.min / sliding window
27
+ // isAdmin / requireAdmin → references admin role check
28
+ // requireAuth / mustBeLoggedIn → references auth middleware / login flag
29
+
30
+ const NAME_FAMILIES = [
31
+ {
32
+ label: 'ownership-check',
33
+ nameRe: /(?:validate|check|enforce|ensure|verify)\w*ownership|\bcheckOwner\b|\bisOwner\b|\bownerOnly\b/i,
34
+ bodyMustHave: /\b(?:req\.user|session\.user|ctx\.user|currentUser|userId|owner_?id)\b/,
35
+ severity: 'medium',
36
+ cwe: 'CWE-639',
37
+ rationale: 'function name claims an ownership check but body never references the requesting-user identity',
38
+ },
39
+ {
40
+ label: 'authorization-check',
41
+ nameRe: /(?:validate|check|require|enforce)\w*(?:access|authoriz|permission)|\bauthorize\b|\bcanAccess\b/i,
42
+ bodyMustHave: /\b(?:role|permission|scope|claim|isAllowed|can\(|allowedTo|hasPermission)\b/,
43
+ severity: 'medium',
44
+ cwe: 'CWE-863',
45
+ rationale: 'function name claims an authorization check but body has no role/permission/scope reference',
46
+ },
47
+ {
48
+ label: 'output-sanitizer',
49
+ nameRe: /\b(?:sanitize|escape|clean|safe(?:n|ify)?|purify)\w*(?:html|xml|sql|input|user|output)\b/i,
50
+ bodyMustHave: /\b(?:DOMPurify|escape|encodeURI|sanitizeHtml|replace\(|str\.replace|html_escape|escape_html|bleach)\b/,
51
+ severity: 'high',
52
+ cwe: 'CWE-79',
53
+ rationale: 'function name claims sanitization but body has no encoding/escaping/library call',
54
+ },
55
+ {
56
+ label: 'signature-verify',
57
+ nameRe: /(?:verify|check|validate)\w*(?:signature|sig|hmac)\b/i,
58
+ bodyMustHave: /\b(?:hmac|createVerify|verify\(|timingSafeEqual|crypto\.subtle\.verify|hashlib\.compare|hash_equals)\b/,
59
+ severity: 'high',
60
+ cwe: 'CWE-347',
61
+ rationale: 'function name claims signature verification but body has no HMAC / verify / constant-time-compare call',
62
+ },
63
+ {
64
+ label: 'webhook-verify',
65
+ nameRe: /(?:verify|validate|check)\w*webhook\b/i,
66
+ bodyMustHave: /\b(?:stripeSignature|webhook_secret|svix|x-hub-signature|hmac|timingSafeEqual)\b/i,
67
+ severity: 'high',
68
+ cwe: 'CWE-345',
69
+ rationale: 'function name claims webhook verification but body has no signature material or constant-time compare',
70
+ },
71
+ {
72
+ label: 'rate-limit-impl',
73
+ nameRe: /(?:rateLimit|throttle|limitRate|rateLimiter)\b/i,
74
+ bodyMustHave: /\b(?:expressRateLimit|rateLimiterFlexible|RateLimiterMemory|Math\.min|window|slidingWindow|tokenBucket|sleep|setTimeout)\b/,
75
+ severity: 'medium',
76
+ cwe: 'CWE-770',
77
+ rationale: 'function name claims rate-limiting but body has no rate-limit library or windowing logic',
78
+ },
79
+ {
80
+ label: 'admin-gate',
81
+ nameRe: /\b(?:isAdmin|requireAdmin|adminOnly|enforceAdmin)\b/i,
82
+ bodyMustHave: /\b(?:admin|role\s*===?\s*['"]admin['"]|isAdmin|hasRole\(['"]admin['"]\))\b/i,
83
+ severity: 'high',
84
+ cwe: 'CWE-862',
85
+ rationale: 'function name claims admin gating but body has no admin-role reference',
86
+ },
87
+ {
88
+ label: 'auth-required',
89
+ nameRe: /\b(?:requireAuth|mustBeLoggedIn|enforceLogin|authenticated)\b/i,
90
+ bodyMustHave: /\b(?:req\.user|session\.user|isAuthenticated|currentUser|jwt|bearer|authHeader|getServerSession)\b/i,
91
+ severity: 'medium',
92
+ cwe: 'CWE-306',
93
+ rationale: 'function name claims auth requirement but body has no session/user lookup',
94
+ },
95
+ ];
96
+
97
+ const JS_FN_RE = /(?:function|const|let)\s+([A-Za-z_$][\w$]*)\s*(?:=\s*(?:async\s*)?\([^)]*\)\s*=>\s*\{|\([^)]*\)\s*\{)/g;
98
+ const PY_FN_RE = /def\s+([A-Za-z_][\w]*)\s*\([^)]*\)\s*:/g;
99
+
100
+ function extractFunctionBodies(text, lang) {
101
+ const bodies = [];
102
+ if (!text || typeof text !== 'string') return bodies;
103
+ if (lang === 'js' || lang === 'ts') {
104
+ JS_FN_RE.lastIndex = 0;
105
+ let m;
106
+ while ((m = JS_FN_RE.exec(text))) {
107
+ const startName = m.index;
108
+ // find the opening brace and then naive balanced-brace seek up to 1500 chars.
109
+ const braceIdx = text.indexOf('{', startName);
110
+ if (braceIdx === -1) continue;
111
+ let depth = 0, end = braceIdx;
112
+ for (let i = braceIdx; i < Math.min(braceIdx + 4000, text.length); i++) {
113
+ if (text[i] === '{') depth++;
114
+ else if (text[i] === '}') {
115
+ depth--;
116
+ if (depth === 0) { end = i; break; }
117
+ }
118
+ }
119
+ bodies.push({ name: m[1], body: text.slice(braceIdx, end + 1), startLine: text.slice(0, startName).split('\n').length });
120
+ }
121
+ } else if (lang === 'py') {
122
+ PY_FN_RE.lastIndex = 0;
123
+ let m;
124
+ while ((m = PY_FN_RE.exec(text))) {
125
+ const startName = m.index;
126
+ // simple python body harvest: take next 60 lines starting at the def
127
+ const startLine = text.slice(0, startName).split('\n').length;
128
+ const allLines = text.split('\n');
129
+ const body = allLines.slice(startLine - 1, Math.min(startLine + 60, allLines.length)).join('\n');
130
+ bodies.push({ name: m[1], body, startLine });
131
+ }
132
+ }
133
+ return bodies;
134
+ }
135
+
136
+ function inferLang(filePath) {
137
+ if (/\.(ts|tsx)$/i.test(filePath)) return 'ts';
138
+ if (/\.(js|jsx|mjs|cjs)$/i.test(filePath)) return 'js';
139
+ if (/\.py$/i.test(filePath)) return 'py';
140
+ return null;
141
+ }
142
+
143
+ export function scanSpecificationDrift(fileContents) {
144
+ const findings = [];
145
+ if (!fileContents || typeof fileContents !== 'object') return findings;
146
+ for (const [fp, text] of Object.entries(fileContents)) {
147
+ const lang = inferLang(fp);
148
+ if (!lang) continue;
149
+ for (const fn of extractFunctionBodies(text, lang)) {
150
+ for (const fam of NAME_FAMILIES) {
151
+ if (!fam.nameRe.test(fn.name)) continue;
152
+ if (fam.bodyMustHave.test(fn.body)) continue; // body satisfies the implicit spec
153
+ findings.push({
154
+ id: `spec-drift:${fam.label}:${fp}:${fn.startLine}`,
155
+ file: fp,
156
+ line: fn.startLine,
157
+ vuln: `Spec drift — ${fn.name}() claims ${fam.label} but body lacks expected evidence`,
158
+ severity: fam.severity,
159
+ family: 'spec-drift',
160
+ cwe: fam.cwe,
161
+ confidence: 0.45,
162
+ description: fam.rationale,
163
+ remediation: `Verify that ${fn.name}() actually implements ${fam.label}; rename if not, or add the missing check.`,
164
+ specMined: { name: fn.name, family: fam.label, mustHaveRegex: fam.bodyMustHave.source },
165
+ });
166
+ }
167
+ }
168
+ }
169
+ return findings;
170
+ }
@@ -0,0 +1,75 @@
1
+ // Stable finding IDs (FR-PREC-5).
2
+ //
3
+ // The engine's default finding IDs include file path and line number, which
4
+ // makes them brittle: any refactor that moves code rotates the IDs and breaks
5
+ // triage/learning persistence. This module computes a refactor-stable hash
6
+ // from (rule_id, normalized_sink_signature, normalized_path_shape) so the
7
+ // same vulnerability keeps the same `stableId` across renames and reformats.
8
+ //
9
+ // The original `id` field is preserved for backwards compatibility — callers
10
+ // that need stability use `stableId`.
11
+
12
+ import * as crypto from 'node:crypto';
13
+
14
+ function normalizeSnippet(s) {
15
+ if (!s || typeof s !== 'string') return '';
16
+ return s
17
+ .toLowerCase()
18
+ // collapse string literals so renaming "foo" → "bar" doesn't break stability
19
+ .replace(/(['"`])(?:[^\\\1]|\\.)*?\1/g, "'_S_'")
20
+ // strip line breaks + collapse whitespace
21
+ .replace(/\s+/g, ' ')
22
+ // strip trailing punctuation
23
+ .replace(/[ ,;]+$/, '')
24
+ .trim();
25
+ }
26
+
27
+ function basenameLike(file) {
28
+ if (!file) return '';
29
+ // Keep only the last two path segments — moving a file within the same
30
+ // package shouldn't rotate the ID, but moving it across modules should.
31
+ const parts = String(file).split('/').filter(Boolean);
32
+ return parts.slice(-2).join('/');
33
+ }
34
+
35
+ function pathShape(f) {
36
+ if (!f) return '';
37
+ const segments = [];
38
+ if (f.source) segments.push(`${f.source.type || ''}@${f.source.label || ''}`);
39
+ if (Array.isArray(f.pathSteps)) {
40
+ for (const step of f.pathSteps) {
41
+ segments.push(`${step.type || ''}@${step.label || ''}`);
42
+ }
43
+ }
44
+ if (f.sink) segments.push(`${f.sink.type || ''}@${f.sink.label || ''}`);
45
+ return segments.join('->');
46
+ }
47
+
48
+ function ruleId(f) {
49
+ if (f.ruleId) return f.ruleId;
50
+ if (f.cwe) return `cwe:${f.cwe}`;
51
+ if (f.family) return `fam:${f.family}`;
52
+ if (f.parser) return `parser:${f.parser}:${(f.vuln || '').slice(0, 40)}`;
53
+ return `vuln:${(f.vuln || '').slice(0, 40)}`;
54
+ }
55
+
56
+ export function computeStableId(f) {
57
+ const rid = ruleId(f);
58
+ const sink = normalizeSnippet(f.sink?.snippet || f.snippet || '').slice(0, 200);
59
+ const shape = pathShape(f);
60
+ const fileHint = basenameLike(f.file || f.sink?.file);
61
+ const material = `${rid}|${sink}|${shape}|${fileHint}`;
62
+ return crypto.createHash('sha256').update(material).digest('hex').slice(0, 16);
63
+ }
64
+
65
+ export function annotateStableIds(findings) {
66
+ if (!Array.isArray(findings)) return;
67
+ for (const f of findings) {
68
+ if (!f || typeof f !== 'object') continue;
69
+ try {
70
+ f.stableId = computeStableId(f);
71
+ } catch {
72
+ f.stableId = null;
73
+ }
74
+ }
75
+ }
@@ -0,0 +1,229 @@
1
+ // Stack-specific security playbook detector.
2
+ //
3
+ // Reads package.json / requirements.txt / Cargo.toml etc. to identify
4
+ // the project's tech stack, then returns a structured playbook of the
5
+ // security items that matter most for that specific combination.
6
+ //
7
+ // Playbook entries are returned as info-severity findings so they appear
8
+ // in the report without polluting the critical/high counts.
9
+
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+
13
+ function _readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; } }
14
+ function _readText(p) { try { return fs.readFileSync(p, 'utf8'); } catch { return null; } }
15
+
16
+ function _detectStack(scanRoot) {
17
+ const stack = new Set();
18
+ const pkg = _readJson(path.join(scanRoot, 'package.json'));
19
+ if (pkg) {
20
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
21
+ const d = k => Object.keys(deps).some(n => n.toLowerCase() === k.toLowerCase());
22
+ const dPartial = k => Object.keys(deps).some(n => n.toLowerCase().includes(k));
23
+
24
+ if (d('next')) stack.add('nextjs');
25
+ if (d('react') || d('react-dom')) stack.add('react');
26
+ if (d('express')) stack.add('express');
27
+ if (d('fastify')) stack.add('fastify');
28
+ if (d('hono')) stack.add('hono');
29
+ if (dPartial('@supabase')) stack.add('supabase');
30
+ if (d('prisma') || d('@prisma/client')) stack.add('prisma');
31
+ if (d('mongoose') || d('mongodb')) stack.add('mongodb');
32
+ if (d('drizzle-orm')) stack.add('drizzle');
33
+ if (d('stripe')) stack.add('stripe');
34
+ if (dPartial('clerk')) stack.add('clerk');
35
+ if (d('next-auth') || d('@auth/core')) stack.add('nextauth');
36
+ if (d('lucia')) stack.add('lucia');
37
+ if (dPartial('openai')) stack.add('openai');
38
+ if (dPartial('@anthropic-ai')) stack.add('anthropic');
39
+ if (dPartial('langchain') || dPartial('@langchain')) stack.add('langchain');
40
+ if (d('trpc') || d('@trpc/server')) stack.add('trpc');
41
+ if (d('graphql')) stack.add('graphql');
42
+ if (d('socket.io')) stack.add('socketio');
43
+ if (d('redis') || d('ioredis') || dPartial('@upstash/redis')) stack.add('redis');
44
+ if (d('resend') || d('nodemailer') || d('@sendgrid/mail')) stack.add('email');
45
+ if (dPartial('firebase')) stack.add('firebase');
46
+ if (d('@aws-sdk/client-s3') || dPartial('aws-sdk')) stack.add('aws');
47
+ }
48
+
49
+ const reqTxt = _readText(path.join(scanRoot, 'requirements.txt')) || '';
50
+ if (reqTxt) {
51
+ if (/fastapi/i.test(reqTxt)) stack.add('fastapi');
52
+ if (/django/i.test(reqTxt)) stack.add('django');
53
+ if (/flask/i.test(reqTxt)) stack.add('flask');
54
+ if (/sqlalchemy/i.test(reqTxt)) stack.add('sqlalchemy');
55
+ if (/openai/i.test(reqTxt)) stack.add('openai');
56
+ if (/anthropic/i.test(reqTxt)) stack.add('anthropic');
57
+ }
58
+
59
+ return stack;
60
+ }
61
+
62
+ // Playbook entries: each is { title, items: string[] }
63
+ function _buildPlaybook(stack) {
64
+ const sections = [];
65
+
66
+ // Next.js
67
+ if (stack.has('nextjs')) {
68
+ sections.push({ title: 'Next.js', items: [
69
+ 'Add security headers in next.config.js headers() — X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin',
70
+ 'Use Server Actions or API Routes for all data mutations — never expose business logic in client components',
71
+ 'Set NEXTAUTH_URL / AUTH_URL in every environment to prevent host-header CSRF',
72
+ 'Wrap the entire app in middleware auth checks — do not rely on individual page-level checks',
73
+ 'Avoid `dangerouslySetInnerHTML` — use DOMPurify or a sanitizer if HTML rendering is required',
74
+ ]});
75
+ }
76
+
77
+ // Supabase
78
+ if (stack.has('supabase')) {
79
+ sections.push({ title: 'Supabase', items: [
80
+ 'Enable Row-Level Security (RLS) on EVERY table — ALTER TABLE x ENABLE ROW LEVEL SECURITY',
81
+ 'Never use the service-role key client-side or in NEXT_PUBLIC_ env vars — server only',
82
+ 'Audit your RLS policies: SELECT policies should require `auth.uid() = user_id` or similar',
83
+ 'Enable email confirmation in the Auth settings before going live',
84
+ 'Restrict which auth providers are enabled — disable unused ones in the dashboard',
85
+ 'Use Supabase Vault for storing sensitive secrets instead of plain text in tables',
86
+ ]});
87
+ }
88
+
89
+ // Clerk
90
+ if (stack.has('clerk')) {
91
+ sections.push({ title: 'Clerk', items: [
92
+ 'Use clerkMiddleware() in middleware.ts and protect all routes by default — list exceptions explicitly',
93
+ 'Do not put admin/settings/dashboard paths in publicRoutes',
94
+ 'Enable MFA in Clerk dashboard for your own admin account and for high-value users',
95
+ 'Validate the userId / orgId from auth() server-side — never trust client-passed IDs',
96
+ 'Set sessionMaxAge to a reasonable value (e.g., 7 days) in Clerk dashboard',
97
+ ]});
98
+ }
99
+
100
+ // NextAuth
101
+ if (stack.has('nextauth')) {
102
+ sections.push({ title: 'NextAuth / Auth.js', items: [
103
+ 'Set NEXTAUTH_SECRET to a 32+ byte random value: `openssl rand -base64 32`',
104
+ 'Set NEXTAUTH_URL to your canonical production URL — prevents host-header CSRF',
105
+ 'Do not set trustHost: true in production',
106
+ 'Do not set allowDangerousEmailAccountLinking: true unless you fully understand the account-takeover risk',
107
+ 'Protect all API routes and pages with getServerSession() — the client session is not authoritative',
108
+ 'Use database sessions (adapter) rather than JWT sessions if you need immediate revocation',
109
+ ]});
110
+ }
111
+
112
+ // Stripe
113
+ if (stack.has('stripe')) {
114
+ sections.push({ title: 'Stripe', items: [
115
+ 'Verify webhook signatures using stripe.webhooks.constructEvent() — never trust unverified webhook payloads',
116
+ 'Store STRIPE_SECRET_KEY server-side only — never client-side or in NEXT_PUBLIC_',
117
+ 'Use Stripe Checkout or Payment Element instead of rolling your own card form',
118
+ 'Enable Radar rules in the Stripe dashboard to block card-testing attacks',
119
+ 'Always fetch the current price from Stripe server-side — never trust the client-supplied price',
120
+ 'Test with Stripe CLI webhook forwarding locally — do not expose your dev server publicly',
121
+ ]});
122
+ }
123
+
124
+ // Prisma / Drizzle
125
+ if (stack.has('prisma') || stack.has('drizzle')) {
126
+ const orm = stack.has('prisma') ? 'Prisma' : 'Drizzle';
127
+ sections.push({ title: orm, items: [
128
+ `Never use raw SQL (\`${stack.has('prisma') ? 'prisma.$queryRawUnsafe' : 'sql.raw'}\`) with user input — use parameterised queries only`,
129
+ 'Scope all queries to the authenticated user — add `where: { userId: session.userId }` to every user-data query',
130
+ 'Store DATABASE_URL in env only — use connection pooling (PgBouncer/Supabase pooler) in production',
131
+ 'Add `@@map` to rename table names if your schema column names match internal business logic you want to keep private',
132
+ ]});
133
+ }
134
+
135
+ // MongoDB / Mongoose
136
+ if (stack.has('mongodb')) {
137
+ sections.push({ title: 'MongoDB', items: [
138
+ 'Sanitize all user input with mongoose-sanitize or express-mongo-sanitize to prevent NoSQL injection',
139
+ 'Never use `$where` with user-controlled expressions',
140
+ 'Enable MongoDB auth (username/password) even in development',
141
+ 'Scope queries to authenticated user: `{ _id: req.params.id, userId: session.userId }`',
142
+ 'Enable MongoDB Atlas IP allowlist to restrict which IPs can connect',
143
+ ]});
144
+ }
145
+
146
+ // OpenAI / Anthropic / LangChain
147
+ if (stack.has('openai') || stack.has('anthropic') || stack.has('langchain')) {
148
+ sections.push({ title: 'AI / LLM', items: [
149
+ 'Rate-limit your AI endpoints — a single attacker can exhaust your monthly budget in minutes',
150
+ 'Never include other users\' data in prompts — prompt context must be scoped to the requesting user',
151
+ 'Set max_tokens on every API call to cap per-request cost',
152
+ 'Validate and sanitize AI output before rendering — treat it as untrusted user input',
153
+ 'Store OPENAI_API_KEY / ANTHROPIC_API_KEY server-side only — never in NEXT_PUBLIC_ or client bundles',
154
+ 'Add per-user spend limits and alert thresholds in the provider dashboard',
155
+ ]});
156
+ }
157
+
158
+ // Email
159
+ if (stack.has('email')) {
160
+ sections.push({ title: 'Email / Transactional', items: [
161
+ 'Rate-limit email-sending endpoints — unlimited sign-up/contact forms are spam vectors',
162
+ 'Validate the To: address server-side — never let users supply arbitrary email recipients',
163
+ 'Store email API keys (Resend, SendGrid) server-side only',
164
+ 'Configure SPF, DKIM, and DMARC records to prevent email spoofing from your domain',
165
+ ]});
166
+ }
167
+
168
+ // tRPC
169
+ if (stack.has('trpc')) {
170
+ sections.push({ title: 'tRPC', items: [
171
+ 'Apply auth middleware to all protected procedures — do not rely on client-side route guards',
172
+ 'Use `ctx.session.userId` for all data access — never trust input IDs without ownership check',
173
+ 'Enable CORS only for your own domains in the tRPC HTTP adapter',
174
+ ]});
175
+ }
176
+
177
+ // FastAPI
178
+ if (stack.has('fastapi')) {
179
+ sections.push({ title: 'FastAPI', items: [
180
+ 'Use OAuth2PasswordBearer or a JWT dependency on every protected route',
181
+ 'Enable CORS middleware with an explicit allow_origins list — never ["*"] in production',
182
+ 'Set SECRET_KEY to a 32+ byte random value and store in environment, not in code',
183
+ 'Use Pydantic models for all request bodies to enforce input validation',
184
+ 'Disable the /docs and /redoc endpoints in production: `docs_url=None, redoc_url=None`',
185
+ ]});
186
+ }
187
+
188
+ // Django
189
+ if (stack.has('django')) {
190
+ sections.push({ title: 'Django', items: [
191
+ 'Set DEBUG=False in production — DEBUG mode exposes tracebacks with local variables',
192
+ 'Set SECRET_KEY from environment — never hardcode it in settings.py',
193
+ 'Configure ALLOWED_HOSTS — an empty list in production causes 500 errors on valid requests',
194
+ 'Use Django\'s built-in CSRF middleware — never exempt views unnecessarily',
195
+ 'Use `django.contrib.security` middleware stack in MIDDLEWARE setting',
196
+ ]});
197
+ }
198
+
199
+ return sections;
200
+ }
201
+
202
+ function _findingFromItem(scanRoot, stackName, item, idx) {
203
+ return {
204
+ id: `stack-playbook:${stackName.replace(/\s+/g, '_').toUpperCase()}:${idx}`,
205
+ title: `[${stackName} Security Checklist] ${item.slice(0, 80)}`,
206
+ severity: 'info',
207
+ file: 'package.json',
208
+ line: 1,
209
+ description: item,
210
+ remediation: item,
211
+ cwe: 'CWE-1008',
212
+ };
213
+ }
214
+
215
+ function runStackPlaybook(scanRoot) {
216
+ if (!scanRoot) return [];
217
+ const stack = _detectStack(scanRoot);
218
+ if (stack.size === 0) return [];
219
+ const playbook = _buildPlaybook(stack);
220
+ const findings = [];
221
+ for (const section of playbook) {
222
+ section.items.forEach((item, i) => {
223
+ findings.push(_findingFromItem(scanRoot, section.title, item, i));
224
+ });
225
+ }
226
+ return { stack: [...stack], playbook, findings };
227
+ }
228
+
229
+ export { runStackPlaybook, _detectStack, _buildPlaybook };