@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.
- package/CHANGELOG.md +1580 -0
- package/bin/.agentic-security/findings.json +1577 -0
- package/bin/.agentic-security/last-scan.json +1577 -0
- package/bin/.agentic-security/last-scan.json.sig +1 -0
- package/bin/.agentic-security/scan-history.json +465 -0
- package/bin/.agentic-security/streak.json +25 -0
- package/bin/agentic-security-audit.js +198 -0
- package/bin/agentic-security-consistency.js +80 -0
- package/bin/agentic-security-diff.js +136 -0
- package/bin/agentic-security-lsp.js +12 -0
- package/bin/agentic-security-mcp.js +40 -0
- package/bin/agentic-security-rule.js +153 -0
- package/bin/agentic-security.js +1683 -0
- package/dist/117.index.js +207 -0
- package/dist/178.index.js +250 -0
- package/dist/218.index.js +793 -0
- package/dist/227.index.js +192 -0
- package/dist/301.index.js +167 -0
- package/dist/384.index.js +18 -0
- package/dist/476.index.js +126 -0
- package/dist/513.index.js +373 -0
- package/dist/520.index.js +13 -0
- package/dist/601.index.js +1038 -0
- package/dist/634.index.js +1892 -0
- package/dist/637.index.js +216 -0
- package/dist/660.index.js +131 -0
- package/dist/675.index.js +451 -0
- package/dist/826.index.js +188 -0
- package/dist/830.index.js +133 -0
- package/dist/agentic-security.mjs +272 -0
- package/dist/agentic-security.mjs.sha256 +1 -0
- package/dist/calibration-seed.json +27 -0
- package/package.json +77 -0
- package/src/.agentic-security/findings.json +80844 -0
- package/src/.agentic-security/last-scan.json +80844 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +8408 -0
- package/src/.agentic-security/streak.json +26 -0
- package/src/badge.js +188 -0
- package/src/compare.js +203 -0
- package/src/dataflow/.agentic-security/findings.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +735 -0
- package/src/dataflow/.agentic-security/streak.json +24 -0
- package/src/dataflow/CLAUDE.md +38 -0
- package/src/dataflow/access-paths.js +172 -0
- package/src/dataflow/async-sequencing.js +177 -0
- package/src/dataflow/backward.js +201 -0
- package/src/dataflow/catalog-expanded.js +485 -0
- package/src/dataflow/catalog.js +659 -0
- package/src/dataflow/cross-repo.js +219 -0
- package/src/dataflow/engine.js +588 -0
- package/src/dataflow/exception-flow.js +116 -0
- package/src/dataflow/exploit-prover.js +187 -0
- package/src/dataflow/higher-order.js +221 -0
- package/src/dataflow/ifds.js +347 -0
- package/src/dataflow/implicit-flow.js +129 -0
- package/src/dataflow/incremental.js +229 -0
- package/src/dataflow/index.js +181 -0
- package/src/dataflow/numeric-domain.js +192 -0
- package/src/dataflow/path-feasibility.js +114 -0
- package/src/dataflow/points-to.js +337 -0
- package/src/dataflow/polyglot.js +190 -0
- package/src/dataflow/proven-clean.js +159 -0
- package/src/dataflow/receiver-context.js +76 -0
- package/src/dataflow/sanitizer-proof.js +154 -0
- package/src/dataflow/soft-taint.js +140 -0
- package/src/dataflow/string-domain.js +234 -0
- package/src/dataflow/stub-aware-filter.js +100 -0
- package/src/dataflow/summaries.js +132 -0
- package/src/dataflow/symbolic-exec.js +238 -0
- package/src/dataflow/tabulation.js +135 -0
- package/src/engine.js +7763 -0
- package/src/history-scan.js +229 -0
- package/src/index.js +3 -0
- package/src/integrations/.agentic-security/findings.json +1504 -0
- package/src/integrations/.agentic-security/last-scan.json +1504 -0
- package/src/integrations/.agentic-security/scan-history.json +40 -0
- package/src/integrations/.agentic-security/streak.json +21 -0
- package/src/integrations/index.js +321 -0
- package/src/integrations/tickets.js +200 -0
- package/src/ir/.agentic-security/findings.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +364 -0
- package/src/ir/.agentic-security/streak.json +23 -0
- package/src/ir/CLAUDE.md +172 -0
- package/src/ir/callgraph.js +73 -0
- package/src/ir/class-hierarchy.js +195 -0
- package/src/ir/index.js +152 -0
- package/src/ir/parser-cs.js +260 -0
- package/src/ir/parser-java.js +286 -0
- package/src/ir/parser-js.js +413 -0
- package/src/ir/parser-kt.js +258 -0
- package/src/ir/parser-py-cst.js +136 -0
- package/src/ir/parser-py.helper.py +501 -0
- package/src/ir/parser-py.js +312 -0
- package/src/ir/ssa.js +315 -0
- package/src/ir/type-stubs.js +288 -0
- package/src/leaderboard.js +152 -0
- package/src/llm-validator/.agentic-security/findings.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
- package/src/llm-validator/.agentic-security/scan-history.json +168 -0
- package/src/llm-validator/.agentic-security/streak.json +20 -0
- package/src/llm-validator/consistency.js +141 -0
- package/src/llm-validator/index.js +437 -0
- package/src/lsp/.agentic-security/findings.json +28 -0
- package/src/lsp/.agentic-security/last-scan.json +28 -0
- package/src/lsp/.agentic-security/scan-history.json +79 -0
- package/src/lsp/.agentic-security/streak.json +22 -0
- package/src/lsp/server.js +275 -0
- package/src/mcp/.agentic-security/findings.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +1125 -0
- package/src/mcp/.agentic-security/streak.json +22 -0
- package/src/mcp/CLAUDE.md +54 -0
- package/src/mcp/audit.js +136 -0
- package/src/mcp/redact.js +75 -0
- package/src/mcp/server.js +158 -0
- package/src/mcp/stdio.js +83 -0
- package/src/mcp/tools.js +940 -0
- package/src/mcp/validate.js +49 -0
- package/src/personality.js +164 -0
- package/src/poc-video.js +239 -0
- package/src/posture/.agentic-security/findings.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +5557 -0
- package/src/posture/.agentic-security/streak.json +24 -0
- package/src/posture/CLAUDE.md +42 -0
- package/src/posture/adversarial-self-test.js +114 -0
- package/src/posture/adversary-agent.js +204 -0
- package/src/posture/agents-memory.js +135 -0
- package/src/posture/ai-code-fingerprint.js +171 -0
- package/src/posture/aibom.js +284 -0
- package/src/posture/api-inventory.js +96 -0
- package/src/posture/attack-playbooks.js +305 -0
- package/src/posture/auditor-agent.js +115 -0
- package/src/posture/auth-posture-import.js +135 -0
- package/src/posture/baseline-compare.js +114 -0
- package/src/posture/blast-radius.js +836 -0
- package/src/posture/bounty-prediction.js +141 -0
- package/src/posture/business-logic.js +239 -0
- package/src/posture/calibration-drift.js +93 -0
- package/src/posture/calibration-seed.json +27 -0
- package/src/posture/calibration.js +204 -0
- package/src/posture/clustering.js +75 -0
- package/src/posture/concurrency-checker.js +265 -0
- package/src/posture/confidence.js +65 -0
- package/src/posture/container-runtime.js +149 -0
- package/src/posture/counterfactual.js +109 -0
- package/src/posture/cross-lang-graphql.js +165 -0
- package/src/posture/cross-lang-grpc.js +166 -0
- package/src/posture/cross-lang-meta.js +101 -0
- package/src/posture/cross-lang-openapi.js +187 -0
- package/src/posture/cross-lang-orm.js +153 -0
- package/src/posture/cross-lang-queues.js +210 -0
- package/src/posture/crown-jewels.js +110 -0
- package/src/posture/custom-rules.js +361 -0
- package/src/posture/cve-alert-daemon.js +433 -0
- package/src/posture/cve-lookup.js +129 -0
- package/src/posture/dead-code.js +430 -0
- package/src/posture/defender-agent.js +158 -0
- package/src/posture/deploy-platform.js +204 -0
- package/src/posture/detector-fuzz.js +61 -0
- package/src/posture/deterministic.js +99 -0
- package/src/posture/drift.js +165 -0
- package/src/posture/epss.js +156 -0
- package/src/posture/exploitability-probability.js +212 -0
- package/src/posture/exploitability.js +121 -0
- package/src/posture/feature-flags.js +110 -0
- package/src/posture/finding-defaults.js +132 -0
- package/src/posture/fix-history.js +411 -0
- package/src/posture/fix-plan.js +121 -0
- package/src/posture/fix-verify-loop.js +157 -0
- package/src/posture/fix-verify.js +130 -0
- package/src/posture/flow-narration.js +105 -0
- package/src/posture/grader-calibration.js +156 -0
- package/src/posture/harness-discovery.js +113 -0
- package/src/posture/holdout-eval.js +144 -0
- package/src/posture/iac-reachability.js +163 -0
- package/src/posture/iam-policy.js +128 -0
- package/src/posture/integrity.js +97 -0
- package/src/posture/learning.js +166 -0
- package/src/posture/license-policy.js +109 -0
- package/src/posture/llm-redteam-prompts.js +418 -0
- package/src/posture/llm-redteam.js +303 -0
- package/src/posture/material-change.js +163 -0
- package/src/posture/mitigation-composite.js +55 -0
- package/src/posture/mttr.js +91 -0
- package/src/posture/network-policy-import.js +126 -0
- package/src/posture/path-predicates.js +99 -0
- package/src/posture/persona-prioritization.js +153 -0
- package/src/posture/poc-cwe-map.js +51 -0
- package/src/posture/poc-generator.js +500 -0
- package/src/posture/policy-gate.js +174 -0
- package/src/posture/pre-incident-archaeology.js +110 -0
- package/src/posture/profile.js +93 -0
- package/src/posture/reachability-filter.js +42 -0
- package/src/posture/regression-test-gen.js +200 -0
- package/src/posture/reverse-blast-radius.js +110 -0
- package/src/posture/router.js +109 -0
- package/src/posture/rule-overrides.js +198 -0
- package/src/posture/rule-pack-signing.js +209 -0
- package/src/posture/rule-packs.js +143 -0
- package/src/posture/rule-synthesis.js +108 -0
- package/src/posture/ruleset-version.js +71 -0
- package/src/posture/sbom.js +129 -0
- package/src/posture/schema-aware-bridge.js +207 -0
- package/src/posture/security-trend.js +87 -0
- package/src/posture/semantic-clone.js +114 -0
- package/src/posture/specification-mining.js +170 -0
- package/src/posture/stable-id.js +75 -0
- package/src/posture/stack-playbook.js +229 -0
- package/src/posture/streak.js +249 -0
- package/src/posture/suppressions.js +135 -0
- package/src/posture/telemetry-ingest.js +112 -0
- package/src/posture/threat-model.js +145 -0
- package/src/posture/three-agent-pipeline.js +74 -0
- package/src/posture/triage.js +146 -0
- package/src/posture/trust-boundary-diagram.js +115 -0
- package/src/posture/type-narrowing.js +129 -0
- package/src/posture/validator-metrics.js +179 -0
- package/src/posture/verifier-ephemeral.js +118 -0
- package/src/posture/verifier-target.js +147 -0
- package/src/posture/verifier.js +257 -0
- package/src/posture/version.js +75 -0
- package/src/posture/waf-ingest.js +200 -0
- package/src/posture/why-fired.js +141 -0
- package/src/pr-comment.js +172 -0
- package/src/pr-delta.js +198 -0
- package/src/report/.agentic-security/findings.json +79 -0
- package/src/report/.agentic-security/last-scan.json +79 -0
- package/src/report/.agentic-security/last-scan.json.sig +1 -0
- package/src/report/.agentic-security/scan-history.json +332 -0
- package/src/report/.agentic-security/streak.json +23 -0
- package/src/report/index.js +1136 -0
- package/src/report/mascot.js +42 -0
- package/src/runScan.js +141 -0
- package/src/sast/.agentic-security/findings.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +788 -0
- package/src/sast/.agentic-security/streak.json +23 -0
- package/src/sast/CLAUDE.md +39 -0
- package/src/sast/_comment-strip.js +46 -0
- package/src/sast/agent-tool-escalation.js +131 -0
- package/src/sast/auth-provider.js +171 -0
- package/src/sast/authz.js +236 -0
- package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
- package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
- package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
- package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
- package/src/sast/bench-shape/index.js +62 -0
- package/src/sast/claude-hook-injection.js +199 -0
- package/src/sast/claude-md-prompt-injection.js +170 -0
- package/src/sast/claude-settings.js +165 -0
- package/src/sast/client-side.js +149 -0
- package/src/sast/cpp-bench-extras.js +122 -0
- package/src/sast/cpp-dataflow.js +430 -0
- package/src/sast/cpp.js +248 -0
- package/src/sast/csharp.js +152 -0
- package/src/sast/csrf.js +82 -0
- package/src/sast/dart-flutter.js +173 -0
- package/src/sast/db-rls.js +147 -0
- package/src/sast/db-taint.js +215 -0
- package/src/sast/defi-deep.js +242 -0
- package/src/sast/deserialization-gadgets.js +113 -0
- package/src/sast/django-hardening.js +230 -0
- package/src/sast/env-hygiene.js +125 -0
- package/src/sast/fastapi-hardening.js +145 -0
- package/src/sast/go-extended.js +84 -0
- package/src/sast/host-header.js +106 -0
- package/src/sast/index.js +17 -0
- package/src/sast/java-ast-folding.js +561 -0
- package/src/sast/java-bench-extras.js +708 -0
- package/src/sast/java-collection-passthrough.js +178 -0
- package/src/sast/java-constant-fold.js +244 -0
- package/src/sast/java-deserialization.js +125 -0
- package/src/sast/jndi.js +104 -0
- package/src/sast/juliet-shape.js +324 -0
- package/src/sast/jwt-exp.js +104 -0
- package/src/sast/kotlin.js +82 -0
- package/src/sast/laravel-hardening.js +198 -0
- package/src/sast/ldap-injection.js +100 -0
- package/src/sast/llm-owasp.js +465 -0
- package/src/sast/llm-stored-prompt.js +103 -0
- package/src/sast/llm-trading-agent.js +161 -0
- package/src/sast/llm.js +308 -0
- package/src/sast/logic.js +140 -0
- package/src/sast/mass-assignment.js +101 -0
- package/src/sast/mcp-audit.js +242 -0
- package/src/sast/mobile-manifest.js +195 -0
- package/src/sast/model-load.js +164 -0
- package/src/sast/mutation-xss.js +87 -0
- package/src/sast/nosql-injection.js +82 -0
- package/src/sast/open-redirect.js +119 -0
- package/src/sast/php.js +91 -0
- package/src/sast/pipeline.js +122 -0
- package/src/sast/primary-cwe-java.js +155 -0
- package/src/sast/prompt-firewall.js +151 -0
- package/src/sast/prompt-template.js +157 -0
- package/src/sast/prototype-pollution.js +112 -0
- package/src/sast/python-sinks.js +195 -0
- package/src/sast/quarkus-hardening.js +102 -0
- package/src/sast/rag-poisoning.js +118 -0
- package/src/sast/rate-limit.js +128 -0
- package/src/sast/response-splitting.js +138 -0
- package/src/sast/ruby.js +108 -0
- package/src/sast/rust.js +105 -0
- package/src/sast/solidity.js +167 -0
- package/src/sast/springboot-hardening.js +186 -0
- package/src/sast/ssrf-cloud-metadata.js +80 -0
- package/src/sast/ssti.js +116 -0
- package/src/sast/swift.js +162 -0
- package/src/sast/toctou.js +95 -0
- package/src/sast/webhook.js +101 -0
- package/src/sast/xpath-injection.js +51 -0
- package/src/sast/xxe.js +140 -0
- package/src/sast/zip-slip.js +200 -0
- package/src/sca/base-images.json +45 -0
- package/src/sca/container.js +107 -0
- package/src/sca/dep-confusion.js +134 -0
- package/src/sca/index.js +6 -0
- package/src/sca/popular-packages.json +41 -0
- package/src/sca/sarif-ingest.js +187 -0
- package/src/sca/vuln-function-hints.json +89 -0
- package/src/secrets/index.js +4 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Probabilistic exploitability with Wilson 95% confidence intervals (v0.68).
|
|
2
|
+
//
|
|
3
|
+
// Replaces opaque severity strings with a calibrated number per finding:
|
|
4
|
+
// f.exploitProbability ∈ [0,1] — Bayesian point estimate
|
|
5
|
+
// f.exploitProbabilityCI95 [lo, hi] — Wilson interval on point
|
|
6
|
+
// f.exploitProbabilityWhy string[] — feature names that fired
|
|
7
|
+
//
|
|
8
|
+
// This is the COMPLEMENT to `exploitability.js`, which is intentionally an
|
|
9
|
+
// ordinal priority (sort key). This module is the calibrated probability
|
|
10
|
+
// you'd put on a slide or stake a budget on.
|
|
11
|
+
//
|
|
12
|
+
// Inputs (per finding + context):
|
|
13
|
+
// - finding.cwe CWE family
|
|
14
|
+
// - finding.parser, finding.family detector identity
|
|
15
|
+
// - taint signals (`trace`, `chain`, source provenance)
|
|
16
|
+
// - reachability tier from posture/reachability-filter.js (set externally)
|
|
17
|
+
// - project context: hasAuth / hasWAF / hasCSP / route_unauth
|
|
18
|
+
// - historical signal: .agentic-security/exploit-history.jsonl (operator-
|
|
19
|
+
// curated record of past confirmed exploits per CWE family).
|
|
20
|
+
//
|
|
21
|
+
// Method:
|
|
22
|
+
// 1. Start with a CWE-family prior (CISA-KEV-aggregated base rate).
|
|
23
|
+
// 2. Multiplicatively update with feature factors (reachability +,
|
|
24
|
+
// sanitizer-present −, auth/WAF/CSP −, source-from-network +).
|
|
25
|
+
// 3. Wilson CI computed from operator's historical hit rate at finer
|
|
26
|
+
// grain (CWE × language × framework) when enough samples exist; falls
|
|
27
|
+
// back to wider CI from the global CWE-family prior otherwise.
|
|
28
|
+
//
|
|
29
|
+
// Important: this is NOT yet a calibrated statistical model. It's a
|
|
30
|
+
// principled heuristic that EXPOSES its uncertainty via the CI rather than
|
|
31
|
+
// hiding it behind a severity label. The CI width is the honest signal —
|
|
32
|
+
// when n is small or factors conflict, the interval gets wide on purpose.
|
|
33
|
+
|
|
34
|
+
import * as fs from 'node:fs';
|
|
35
|
+
import * as path from 'node:path';
|
|
36
|
+
import { wilsonInterval } from './calibration.js';
|
|
37
|
+
|
|
38
|
+
// CISA KEV-derived base rate per CWE family. These are rough mid-2025
|
|
39
|
+
// observations of "actually exploited in the wild" rates among findings
|
|
40
|
+
// of the family, not academic numbers. Refresh annually.
|
|
41
|
+
const CWE_BASE_RATE = {
|
|
42
|
+
'CWE-78': 0.50, // command injection
|
|
43
|
+
'CWE-89': 0.55, // SQLi
|
|
44
|
+
'CWE-79': 0.35, // XSS
|
|
45
|
+
'CWE-22': 0.40, // path traversal
|
|
46
|
+
'CWE-918': 0.40, // SSRF
|
|
47
|
+
'CWE-502': 0.55, // deserialization
|
|
48
|
+
'CWE-94': 0.55, // code injection / SSTI
|
|
49
|
+
'CWE-611': 0.30, // XXE
|
|
50
|
+
'CWE-1321': 0.45, // prototype pollution
|
|
51
|
+
'CWE-352': 0.20, // CSRF
|
|
52
|
+
'CWE-601': 0.20, // open redirect (low impact alone, often a chain link)
|
|
53
|
+
'CWE-113': 0.30, // response splitting
|
|
54
|
+
'CWE-798': 0.65, // hardcoded secrets — when the secret leaks, exploit is immediate
|
|
55
|
+
'CWE-90': 0.40, // LDAP injection
|
|
56
|
+
'CWE-643': 0.40, // XPath injection
|
|
57
|
+
'CWE-1333': 0.25, // ReDoS — exploitation often DoS only
|
|
58
|
+
'CWE-327': 0.25, // weak crypto — usually requires chained access
|
|
59
|
+
'CWE-329': 0.30, // static IV
|
|
60
|
+
'CWE-338': 0.30, // weak RNG
|
|
61
|
+
'CWE-916': 0.40, // weak password hash
|
|
62
|
+
'CWE-1336': 0.40, // prompt injection
|
|
63
|
+
'CWE-269': 0.45, // privilege escalation
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const DEFAULT_BASE_RATE = 0.30;
|
|
67
|
+
|
|
68
|
+
// Multiplicative factors. The point estimate is base * Π(factors), clamped
|
|
69
|
+
// to (0, 1). Factor > 1 raises the probability; factor < 1 lowers it. We
|
|
70
|
+
// also collect which factors fired into `why`.
|
|
71
|
+
const FACTORS = [
|
|
72
|
+
// POSITIVE (raise probability)
|
|
73
|
+
{
|
|
74
|
+
name: 'reachable-from-public-route',
|
|
75
|
+
factor: 1.5,
|
|
76
|
+
test: (f) => f.reachabilityTier === 'reachable-public' || f.reachabilityTier === 'public-unauthed',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'source-from-network',
|
|
80
|
+
factor: 1.3,
|
|
81
|
+
test: (f) => (f.trace || f.chain || []).some(t =>
|
|
82
|
+
/http-body|url-param|header|cookie/i.test(t.provenance || '')),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'critical-severity-detector',
|
|
86
|
+
factor: 1.15,
|
|
87
|
+
test: (f) => f.severity === 'critical',
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'cwe-on-cisa-kev',
|
|
91
|
+
factor: 1.25,
|
|
92
|
+
test: (f) => !!f.kevReferenced,
|
|
93
|
+
},
|
|
94
|
+
// NEGATIVE (lower probability)
|
|
95
|
+
{
|
|
96
|
+
name: 'sanitizer-in-path',
|
|
97
|
+
factor: 0.5,
|
|
98
|
+
test: (f) => Array.isArray(f.sanitizerCallsInPath) && f.sanitizerCallsInPath.length > 0,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'auth-middleware-on-route',
|
|
102
|
+
factor: 0.6,
|
|
103
|
+
test: (f) => f.routeAuth === true,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'project-has-waf',
|
|
107
|
+
factor: 0.75,
|
|
108
|
+
test: (f, ctx) => ctx && ctx.hasWAF === true,
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'project-has-csp',
|
|
112
|
+
factor: 0.85,
|
|
113
|
+
test: (f, ctx) => ctx && ctx.hasCSP === true && /CWE-79|xss/i.test(f.cwe || f.family || ''),
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'low-severity-detector',
|
|
117
|
+
factor: 0.6,
|
|
118
|
+
test: (f) => f.severity === 'low' || f.severity === 'info',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'unreachable',
|
|
122
|
+
factor: 0.1,
|
|
123
|
+
test: (f) => f.unreachable === true,
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
function _basePrior(cwe) {
|
|
128
|
+
if (!cwe) return DEFAULT_BASE_RATE;
|
|
129
|
+
return CWE_BASE_RATE[cwe] || DEFAULT_BASE_RATE;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function _clamp01(x) { return Math.max(0.001, Math.min(0.999, x)); }
|
|
133
|
+
|
|
134
|
+
// Operator-curated exploit history. JSONL, one row per past confirmed
|
|
135
|
+
// exploit: {"cwe": "CWE-89", "language": "javascript", "framework": "express",
|
|
136
|
+
// "verdict": "true_positive_exploited" | "false_positive"}
|
|
137
|
+
// Used to refine the Wilson CI when enough samples exist at the
|
|
138
|
+
// (cwe × language × framework) grain.
|
|
139
|
+
function _loadHistory(scanRoot) {
|
|
140
|
+
if (!scanRoot) return [];
|
|
141
|
+
const fp = path.join(scanRoot, '.agentic-security', 'exploit-history.jsonl');
|
|
142
|
+
if (!fs.existsSync(fp)) return [];
|
|
143
|
+
const out = [];
|
|
144
|
+
try {
|
|
145
|
+
for (const line of fs.readFileSync(fp, 'utf8').split('\n')) {
|
|
146
|
+
const t = line.trim();
|
|
147
|
+
if (!t) continue;
|
|
148
|
+
try {
|
|
149
|
+
const o = JSON.parse(t);
|
|
150
|
+
if (o && o.cwe && o.verdict) out.push(o);
|
|
151
|
+
} catch {}
|
|
152
|
+
}
|
|
153
|
+
} catch {}
|
|
154
|
+
return out;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function _historicalCi(history, cwe, language) {
|
|
158
|
+
if (!Array.isArray(history) || history.length === 0) return null;
|
|
159
|
+
// Tier the slice: prefer (cwe × language) when n ≥ 5, fall back to cwe
|
|
160
|
+
// alone when n ≥ 10, fall back to nothing when neither slice has volume.
|
|
161
|
+
const cl = history.filter(h => h.cwe === cwe && h.language === language);
|
|
162
|
+
const c = history.filter(h => h.cwe === cwe);
|
|
163
|
+
let pool, sliceName;
|
|
164
|
+
if (cl.length >= 5) { pool = cl; sliceName = `${cwe}×${language}`; }
|
|
165
|
+
else if (c.length >= 10) { pool = c; sliceName = cwe; }
|
|
166
|
+
else return null;
|
|
167
|
+
const tp = pool.filter(h => h.verdict === 'true_positive_exploited').length;
|
|
168
|
+
const n = pool.length;
|
|
169
|
+
return { ci: wilsonInterval(tp, n), p: tp / n, n, slice: sliceName };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Public entry. Annotates each finding in `findings` with
|
|
173
|
+
// exploitProbability + exploitProbabilityCI95 + exploitProbabilityWhy.
|
|
174
|
+
//
|
|
175
|
+
// Pure function over findings — returns the same array.
|
|
176
|
+
export function annotateExploitProbability(findings, ctx = {}) {
|
|
177
|
+
if (!Array.isArray(findings) || findings.length === 0) return findings;
|
|
178
|
+
const history = _loadHistory(ctx.scanRoot);
|
|
179
|
+
for (const f of findings) {
|
|
180
|
+
const cwe = f.cwe || null;
|
|
181
|
+
const why = [];
|
|
182
|
+
let p = _basePrior(cwe);
|
|
183
|
+
for (const F of FACTORS) {
|
|
184
|
+
try {
|
|
185
|
+
if (F.test(f, ctx)) { p *= F.factor; why.push(F.name); }
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
p = _clamp01(p);
|
|
189
|
+
// Wilson CI: prefer historical CI when we have one, otherwise derive
|
|
190
|
+
// a wider CI from the prior (n=10 implied sample at base rate).
|
|
191
|
+
const hist = _historicalCi(history, cwe, f.language || (f.file || '').split('.').pop());
|
|
192
|
+
if (hist) {
|
|
193
|
+
f.exploitProbability = hist.p;
|
|
194
|
+
f.exploitProbabilityCI95 = hist.ci;
|
|
195
|
+
f.exploitProbabilitySlice = hist.slice;
|
|
196
|
+
f.exploitProbabilityN = hist.n;
|
|
197
|
+
} else {
|
|
198
|
+
// Wilson with a synthetic n=10 "prior pseudo-observations" at the
|
|
199
|
+
// updated probability — gives a wide CI when we have no data.
|
|
200
|
+
const tp = Math.round(p * 10);
|
|
201
|
+
f.exploitProbability = p;
|
|
202
|
+
f.exploitProbabilityCI95 = wilsonInterval(tp, 10);
|
|
203
|
+
f.exploitProbabilitySlice = 'prior-only';
|
|
204
|
+
f.exploitProbabilityN = 10;
|
|
205
|
+
}
|
|
206
|
+
f.exploitProbabilityWhy = why;
|
|
207
|
+
}
|
|
208
|
+
return findings;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Convenience for tests / diagnostics.
|
|
212
|
+
export const _internal = { CWE_BASE_RATE, DEFAULT_BASE_RATE, FACTORS, _basePrior };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Exploitability scoring (FR-PREC-3) — ORDINAL PRIORITY, NOT A PROBABILITY.
|
|
2
|
+
//
|
|
3
|
+
// The output is a 0–1 ordinal priority score used to rank findings, NOT a
|
|
4
|
+
// calibrated probability. The weights below are hand-picked heuristics and
|
|
5
|
+
// have not been calibrated against a labeled outcome set. Treat the number
|
|
6
|
+
// as "use this to sort, not to bet money on."
|
|
7
|
+
//
|
|
8
|
+
// In particular, do NOT:
|
|
9
|
+
// - render the score as a percentage in customer-facing UI ("95% likely
|
|
10
|
+
// to be exploited" implies a precision the score doesn't have)
|
|
11
|
+
// - feed the score into a downstream pricing / risk-acceptance decision
|
|
12
|
+
// - aggregate it across findings (average exploitability is meaningless)
|
|
13
|
+
//
|
|
14
|
+
// DO use it for:
|
|
15
|
+
// - ranking findings within a single scan ("show me the top 10")
|
|
16
|
+
// - tier labels (critical/high/medium/low) which intentionally collapse
|
|
17
|
+
// the score into coarse buckets
|
|
18
|
+
//
|
|
19
|
+
// To turn this into a calibrated probability, you need a labeled outcome
|
|
20
|
+
// dataset (PRs merged with no incident vs. PRs that became incidents) and
|
|
21
|
+
// isotonic regression. See PRD §10 / bench/README.md for the open work.
|
|
22
|
+
//
|
|
23
|
+
// Output:
|
|
24
|
+
// f.exploitability ∈ [0,1] — ordinal priority score
|
|
25
|
+
// f.priorityScore = f.exploitability — explicit alias for new callers
|
|
26
|
+
// f.exploitabilityFactors: string[] — which signals contributed
|
|
27
|
+
// f.exploitabilityTier: 'critical' | 'high' | 'medium' | 'low'
|
|
28
|
+
|
|
29
|
+
const SEVERITY_BASE = {
|
|
30
|
+
critical: 0.80,
|
|
31
|
+
high: 0.65,
|
|
32
|
+
medium: 0.40,
|
|
33
|
+
low: 0.20,
|
|
34
|
+
info: 0.10,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Detected from project signals — passed in via ctx.
|
|
38
|
+
// ctx.hasCSP — Content-Security-Policy header configured
|
|
39
|
+
// ctx.hasHelmet — helmet middleware present
|
|
40
|
+
// ctx.hasWAF — WAF / Cloudflare rules ingested
|
|
41
|
+
// ctx.hasAuth — auth middleware/guards detected somewhere
|
|
42
|
+
// ctx.unauthRoutes — count of routes that lack auth on the project
|
|
43
|
+
export function detectProjectContext(fc, routes) {
|
|
44
|
+
const ctx = {
|
|
45
|
+
hasCSP: false,
|
|
46
|
+
hasHelmet: false,
|
|
47
|
+
hasWAF: false,
|
|
48
|
+
hasAuth: false,
|
|
49
|
+
unauthRoutes: 0,
|
|
50
|
+
};
|
|
51
|
+
if (!fc) return ctx;
|
|
52
|
+
for (const [fp, c] of Object.entries(fc)) {
|
|
53
|
+
if (!c || typeof c !== 'string') continue;
|
|
54
|
+
if (!ctx.hasCSP && /content[-_]security[-_]policy|Content-Security-Policy/i.test(c)) ctx.hasCSP = true;
|
|
55
|
+
if (!ctx.hasHelmet && /\bhelmet\s*\(/.test(c)) ctx.hasHelmet = true;
|
|
56
|
+
if (!ctx.hasWAF && /(cloudflare|aws[-_]?waf|akamai[-_]?waf|fastly[-_]?waf)/i.test(c)) ctx.hasWAF = true;
|
|
57
|
+
if (!ctx.hasAuth && /(passport\.|express-jwt|requireAuth|requires?_login|@login_required|isAuthenticated|verifyJWT)/i.test(c)) ctx.hasAuth = true;
|
|
58
|
+
void fp;
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(routes)) ctx.unauthRoutes = routes.filter(r => r && r.unauthed).length;
|
|
61
|
+
return ctx;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function scoreOne(f, ctx) {
|
|
65
|
+
const factors = [];
|
|
66
|
+
let s = SEVERITY_BASE[f.severity] ?? 0.30;
|
|
67
|
+
factors.push(`sev:${f.severity}`);
|
|
68
|
+
|
|
69
|
+
// Reachability from entry points raises exploitability sharply.
|
|
70
|
+
if (f.routeRooted) { s += 0.20; factors.push('route-rooted'); }
|
|
71
|
+
else if (f.reachable === true) { s += 0.10; factors.push('reachable'); }
|
|
72
|
+
else if (f.reachable === false) { s -= 0.20; factors.push('unreachable'); }
|
|
73
|
+
|
|
74
|
+
// Auth gating on the path lowers it.
|
|
75
|
+
if (f.guards && f.guards.length) { s -= 0.15; factors.push(`guards:${f.guards.length}`); }
|
|
76
|
+
|
|
77
|
+
// Sanitized paths are very low.
|
|
78
|
+
if (f.isSanitized) { s = Math.min(s, 0.10); factors.push('sanitized'); }
|
|
79
|
+
if (f.sanitizerMismatch) { s += 0.10; factors.push('sanitizer-mismatch'); }
|
|
80
|
+
|
|
81
|
+
// SCA enrichment: KEV-listed vulns are by definition actively exploited;
|
|
82
|
+
// EPSS percentile ≥ 0.95 puts the CVE in the "exploited now" cohort.
|
|
83
|
+
if (f.kev || f.kevListed) { s = Math.max(s, 0.92); factors.push('kev'); }
|
|
84
|
+
if (typeof f.epssPercentile === 'number' && f.epssPercentile >= 0.95) {
|
|
85
|
+
s = Math.max(s, 0.85); factors.push('epss>=p95');
|
|
86
|
+
}
|
|
87
|
+
if (f.exploitedNow) { s = Math.max(s, 0.85); factors.push('exploited-now'); }
|
|
88
|
+
|
|
89
|
+
// Project-wide mitigations reduce exploitability.
|
|
90
|
+
if (ctx) {
|
|
91
|
+
if (ctx.hasCSP && /xss/i.test(f.family || f.vuln || '')) { s -= 0.10; factors.push('csp-present'); }
|
|
92
|
+
if (ctx.hasHelmet) { s -= 0.05; factors.push('helmet'); }
|
|
93
|
+
if (ctx.hasWAF) { s -= 0.05; factors.push('waf'); }
|
|
94
|
+
if (!ctx.hasAuth && f.routeRooted) { s += 0.10; factors.push('no-auth-middleware'); }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
s = Math.max(0, Math.min(1, s));
|
|
98
|
+
// Round to 2 decimals — the score is ordinal, not calibrated. Three
|
|
99
|
+
// decimals of precision suggested precision the model doesn't have.
|
|
100
|
+
return { score: Math.round(s * 100) / 100, factors };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function annotateExploitability(findings, ctx) {
|
|
104
|
+
if (!Array.isArray(findings)) return;
|
|
105
|
+
for (const f of findings) {
|
|
106
|
+
if (!f || typeof f !== 'object') continue;
|
|
107
|
+
try {
|
|
108
|
+
const { score, factors } = scoreOne(f, ctx);
|
|
109
|
+
f.exploitability = score;
|
|
110
|
+
f.priorityScore = score; // ordinal-priority alias; new code should use this name
|
|
111
|
+
f.exploitabilityFactors = factors;
|
|
112
|
+
f._scoreIsOrdinal = true; // marker so downstream code knows not to treat as probability
|
|
113
|
+
if (score >= 0.80) f.exploitabilityTier = 'critical';
|
|
114
|
+
else if (score >= 0.60) f.exploitabilityTier = 'high';
|
|
115
|
+
else if (score >= 0.35) f.exploitabilityTier = 'medium';
|
|
116
|
+
else f.exploitabilityTier = 'low';
|
|
117
|
+
} catch {
|
|
118
|
+
f.exploitability = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// FR-PROD-6 — Feature-flag awareness.
|
|
2
|
+
//
|
|
3
|
+
// When a security finding lives behind a feature-flag check, its real-world
|
|
4
|
+
// exposure depends on the flag's rollout state, not on the code itself.
|
|
5
|
+
// A flag at 0% rollout is `info`; the same code at 100% is `critical`.
|
|
6
|
+
//
|
|
7
|
+
// This module recognizes flag-gated code regions for the major providers:
|
|
8
|
+
// - LaunchDarkly (`ldClient.variation`, `useFlag`)
|
|
9
|
+
// - Statsig (`Statsig.checkGate`, `useGate`)
|
|
10
|
+
// - ConfigCat (`configCatClient.getValue`)
|
|
11
|
+
// - Unleash (`unleash.isEnabled`)
|
|
12
|
+
// - OpenFeature (`client.getBooleanValue`)
|
|
13
|
+
// - Vercel Flags (`@vercel/flags`)
|
|
14
|
+
// - Custom env-var flags (`process.env.FEATURE_X === 'true'`)
|
|
15
|
+
//
|
|
16
|
+
// We tag findings with the gating flag name when detected. Rollout percentage
|
|
17
|
+
// can be supplied via `.agentic-security/feature-flag-rollouts.json` (a map
|
|
18
|
+
// of flag name → percentage 0..100); if absent, default to 100% (assume the
|
|
19
|
+
// flag is on) — fail-open against the security side.
|
|
20
|
+
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
|
|
24
|
+
const FLAG_PATTERNS = [
|
|
25
|
+
// LaunchDarkly
|
|
26
|
+
[/\bldClient\.variation\s*\(\s*['"`]([^'"`]+)['"`]/g, 'launchdarkly'],
|
|
27
|
+
[/\buseFlag\s*\(\s*['"`]([^'"`]+)['"`]/g, 'launchdarkly'],
|
|
28
|
+
[/\bvariation\s*\(\s*['"`]([^'"`]+)['"`]/g, 'launchdarkly'],
|
|
29
|
+
// Statsig
|
|
30
|
+
[/\bStatsig\.(?:checkGate|getExperiment|getConfig)\s*\(\s*['"`]([^'"`]+)['"`]/g, 'statsig'],
|
|
31
|
+
[/\buseGate\s*\(\s*['"`]([^'"`]+)['"`]/g, 'statsig'],
|
|
32
|
+
// ConfigCat
|
|
33
|
+
[/\bconfigCatClient\.getValue\s*\(\s*['"`]([^'"`]+)['"`]/g, 'configcat'],
|
|
34
|
+
// Unleash
|
|
35
|
+
[/\bunleash\.isEnabled\s*\(\s*['"`]([^'"`]+)['"`]/g, 'unleash'],
|
|
36
|
+
// OpenFeature
|
|
37
|
+
[/\b(?:client|of)\.get(?:Boolean|String|Number)Value\s*\(\s*['"`]([^'"`]+)['"`]/g, 'openfeature'],
|
|
38
|
+
// Vercel
|
|
39
|
+
[/\b(?:flag|getFlag|flags)\s*\(\s*['"`]([^'"`]+)['"`]/g, 'vercel-or-generic'],
|
|
40
|
+
// env-var flags
|
|
41
|
+
[/process\.env\.(FEATURE_[A-Z0-9_]+)\s*===?\s*['"`]?(?:true|1)/g, 'env-var'],
|
|
42
|
+
[/process\.env\.(FF_[A-Z0-9_]+)\s*===?\s*['"`]?(?:true|1)/g, 'env-var'],
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function loadRollouts(scanRoot) {
|
|
46
|
+
const candidates = [
|
|
47
|
+
'.agentic-security/feature-flag-rollouts.json',
|
|
48
|
+
'.agentic-security/feature-flags.json',
|
|
49
|
+
];
|
|
50
|
+
for (const rel of candidates) {
|
|
51
|
+
const fp = path.join(scanRoot || process.cwd(), rel);
|
|
52
|
+
try {
|
|
53
|
+
if (fs.existsSync(fp)) {
|
|
54
|
+
const data = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
55
|
+
if (data && typeof data === 'object') return data;
|
|
56
|
+
}
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Walk every file once and build a map: file → list of { flagName, line, vendor }.
|
|
63
|
+
// Cheap regex scan, suitable to call per-scan.
|
|
64
|
+
export function detectFlagSites(fileContents) {
|
|
65
|
+
const out = {};
|
|
66
|
+
if (!fileContents || typeof fileContents !== 'object') return out;
|
|
67
|
+
for (const [fp, text] of Object.entries(fileContents)) {
|
|
68
|
+
if (!text || typeof text !== 'string') continue;
|
|
69
|
+
const sample = text.length > 100_000 ? text.slice(0, 100_000) : text;
|
|
70
|
+
const hits = [];
|
|
71
|
+
for (const [re, vendor] of FLAG_PATTERNS) {
|
|
72
|
+
re.lastIndex = 0;
|
|
73
|
+
let m;
|
|
74
|
+
while ((m = re.exec(sample))) {
|
|
75
|
+
const flagName = m[1];
|
|
76
|
+
const line = sample.slice(0, m.index).split('\n').length;
|
|
77
|
+
hits.push({ flagName, vendor, line });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (hits.length) out[fp] = hits;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// For each finding, search ±20 lines around the finding location in its file
|
|
86
|
+
// for a flag site. If found, tag with the controlling flag and rollout %.
|
|
87
|
+
export function annotateFeatureFlagGating(findings, fileContents, opts = {}) {
|
|
88
|
+
if (!Array.isArray(findings)) return findings;
|
|
89
|
+
const rollouts = opts.rollouts || loadRollouts(opts.scanRoot);
|
|
90
|
+
const sites = detectFlagSites(fileContents || {});
|
|
91
|
+
for (const f of findings) {
|
|
92
|
+
if (!f || typeof f !== 'object') continue;
|
|
93
|
+
const fp = f.file;
|
|
94
|
+
const ln = f.line || 0;
|
|
95
|
+
if (!fp || !sites[fp]) continue;
|
|
96
|
+
const window = 20;
|
|
97
|
+
const nearby = sites[fp].find(s => Math.abs(s.line - ln) <= window);
|
|
98
|
+
if (!nearby) continue;
|
|
99
|
+
f.featureFlag = nearby.flagName;
|
|
100
|
+
f.featureFlagVendor = nearby.vendor;
|
|
101
|
+
const rollout = rollouts && Object.prototype.hasOwnProperty.call(rollouts, nearby.flagName)
|
|
102
|
+
? Number(rollouts[nearby.flagName])
|
|
103
|
+
: 100;
|
|
104
|
+
f.featureFlagRollout = Number.isFinite(rollout) ? Math.max(0, Math.min(100, rollout)) : 100;
|
|
105
|
+
if (f.featureFlagRollout === 0) f.featureFlagState = 'gated-off';
|
|
106
|
+
else if (f.featureFlagRollout < 100) f.featureFlagState = 'partial-rollout';
|
|
107
|
+
else f.featureFlagState = 'fully-rolled-out';
|
|
108
|
+
}
|
|
109
|
+
return findings;
|
|
110
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Premortem #8: backfill the `parser` and `family` fields on every finding.
|
|
2
|
+
//
|
|
3
|
+
// Symptom this fixes: a smoke run on test/fixtures/vulnerable-js reported 31
|
|
4
|
+
// findings, all with `parser: null` — which silenced the PARSER_PRIOR boost in
|
|
5
|
+
// confidence.js, AND 20/31 had `family: null` — which silenced the entire
|
|
6
|
+
// calibration table in calibration.js. The annotation pipeline downstream
|
|
7
|
+
// expected these fields to be set by every detector, but most regex-style
|
|
8
|
+
// detectors emit a plain Finding shape without them.
|
|
9
|
+
//
|
|
10
|
+
// We backfill, never overwrite. Detector-set values win.
|
|
11
|
+
|
|
12
|
+
// Lightweight family inference. CWE-based first, then title/vuln keyword.
|
|
13
|
+
const _CWE_FAMILY = {
|
|
14
|
+
'CWE-78': 'command-injection',
|
|
15
|
+
'CWE-79': 'xss',
|
|
16
|
+
'CWE-80': 'xss',
|
|
17
|
+
'CWE-87': 'xss',
|
|
18
|
+
'CWE-89': 'sql-injection',
|
|
19
|
+
'CWE-90': 'ldap-injection',
|
|
20
|
+
'CWE-91': 'xpath-injection',
|
|
21
|
+
'CWE-94': 'code-injection',
|
|
22
|
+
'CWE-22': 'path-traversal',
|
|
23
|
+
'CWE-23': 'path-traversal',
|
|
24
|
+
'CWE-36': 'path-traversal',
|
|
25
|
+
'CWE-200': 'info-disclosure',
|
|
26
|
+
'CWE-209': 'info-disclosure',
|
|
27
|
+
'CWE-256': 'hardcoded-secret',
|
|
28
|
+
'CWE-259': 'hardcoded-secret',
|
|
29
|
+
'CWE-287': 'broken-auth',
|
|
30
|
+
'CWE-295': 'cert-validation',
|
|
31
|
+
'CWE-306': 'broken-auth',
|
|
32
|
+
'CWE-307': 'rate-limit',
|
|
33
|
+
'CWE-326': 'weak-crypto',
|
|
34
|
+
'CWE-327': 'weak-crypto',
|
|
35
|
+
'CWE-328': 'weak-crypto',
|
|
36
|
+
'CWE-329': 'weak-crypto',
|
|
37
|
+
'CWE-330': 'weak-rng',
|
|
38
|
+
'CWE-338': 'weak-rng',
|
|
39
|
+
'CWE-345': 'integrity',
|
|
40
|
+
'CWE-352': 'csrf',
|
|
41
|
+
'CWE-384': 'session-fixation',
|
|
42
|
+
'CWE-434': 'unrestricted-upload',
|
|
43
|
+
'CWE-502': 'insecure-deserialization',
|
|
44
|
+
'CWE-601': 'open-redirect',
|
|
45
|
+
'CWE-611': 'xxe',
|
|
46
|
+
'CWE-639': 'idor',
|
|
47
|
+
'CWE-640': 'broken-auth',
|
|
48
|
+
'CWE-732': 'permissions',
|
|
49
|
+
'CWE-770': 'rate-limit',
|
|
50
|
+
'CWE-776': 'xxe',
|
|
51
|
+
'CWE-798': 'hardcoded-secret',
|
|
52
|
+
'CWE-829': 'supply-chain',
|
|
53
|
+
'CWE-862': 'broken-authz',
|
|
54
|
+
'CWE-863': 'broken-authz',
|
|
55
|
+
'CWE-915': 'mass-assignment',
|
|
56
|
+
'CWE-918': 'ssrf',
|
|
57
|
+
'CWE-1004': 'cookie-flag',
|
|
58
|
+
'CWE-1021': 'clickjacking',
|
|
59
|
+
'CWE-1287': 'idor',
|
|
60
|
+
'CWE-1321': 'prototype-pollution',
|
|
61
|
+
'CWE-1333': 'redos',
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const _KEYWORD_FAMILY = [
|
|
65
|
+
[/sql\s*injection/i, 'sql-injection'],
|
|
66
|
+
[/command\s*injection/i, 'command-injection'],
|
|
67
|
+
[/code\s*injection|eval|insecure\s*eval/i, 'code-injection'],
|
|
68
|
+
[/cross[-\s]?site\s*scripting|\bxss\b/i, 'xss'],
|
|
69
|
+
[/path\s*traversal|directory\s*traversal/i, 'path-traversal'],
|
|
70
|
+
[/server[-\s]?side\s*request\s*forgery|\bssrf\b/i, 'ssrf'],
|
|
71
|
+
[/cross[-\s]?site\s*request\s*forgery|\bcsrf\b/i, 'csrf'],
|
|
72
|
+
[/open\s*redirect/i, 'open-redirect'],
|
|
73
|
+
[/\bxxe\b|external\s*entity/i, 'xxe'],
|
|
74
|
+
[/\bidor\b|insecure\s*direct\s*object/i, 'idor'],
|
|
75
|
+
[/mass[-\s]?assignment|over[-\s]?posting/i, 'mass-assignment'],
|
|
76
|
+
[/prototype\s*pollution/i, 'prototype-pollution'],
|
|
77
|
+
[/deserialization|deserialize/i, 'insecure-deserialization'],
|
|
78
|
+
[/weak\s*(?:crypto|hash|cipher)|\bmd5\b|\bsha1\b|\brc4\b/i, 'weak-crypto'],
|
|
79
|
+
[/weak\s*rng|insecure\s*random|math\.random/i, 'weak-rng'],
|
|
80
|
+
[/hardcoded\s*(?:secret|credential|key|password|token)|secret\s*in\s*code/i, 'hardcoded-secret'],
|
|
81
|
+
[/jwt|json\s*web\s*token/i, 'broken-auth'],
|
|
82
|
+
[/jndi/i, 'jndi-injection'],
|
|
83
|
+
[/log\s*injection|log4shell/i, 'log-injection'],
|
|
84
|
+
[/insecure\s*http|http\s*without\s*tls|missing\s*https/i, 'insecure-http'],
|
|
85
|
+
[/host\s*header/i, 'host-header'],
|
|
86
|
+
[/rate[-\s]?limit/i, 'rate-limit'],
|
|
87
|
+
[/vulnerable\s*dependency|cve-\d{4}-\d+|known\s*vulnerable\s*component/i, 'vulnerable-dep'],
|
|
88
|
+
[/prompt\s*injection|llm\s*injection/i, 'prompt-injection'],
|
|
89
|
+
[/clickjacking|x-frame-options/i, 'clickjacking'],
|
|
90
|
+
[/zip[-\s]?slip/i, 'path-traversal'],
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
function _inferFamily(f) {
|
|
94
|
+
if (!f || typeof f !== 'object') return null;
|
|
95
|
+
if (typeof f.family === 'string' && f.family.length) return f.family;
|
|
96
|
+
if (typeof f.cwe === 'string' && _CWE_FAMILY[f.cwe]) return _CWE_FAMILY[f.cwe];
|
|
97
|
+
const hay = `${f.vuln || ''} ${f.title || ''} ${f.description || ''}`.slice(0, 600);
|
|
98
|
+
for (const [re, fam] of _KEYWORD_FAMILY) {
|
|
99
|
+
if (re.test(hay)) return fam;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _inferParser(f) {
|
|
105
|
+
if (!f || typeof f !== 'object') return 'REGEX';
|
|
106
|
+
if (typeof f.parser === 'string' && f.parser.length) return f.parser;
|
|
107
|
+
// SCA findings carry pkg/component/purl; tag them so triage and validator
|
|
108
|
+
// can short-circuit (LLM validator already special-cases parser SCA).
|
|
109
|
+
if (f.parser === 'SCA' || f.kind === 'sca' ||
|
|
110
|
+
typeof f.pkg === 'string' || typeof f.component === 'string' ||
|
|
111
|
+
typeof f.purl === 'string') return 'SCA';
|
|
112
|
+
if (f.custom === true) return 'CUSTOM_RULE';
|
|
113
|
+
// Findings carrying a source/sink chain are layer-2 taint outputs.
|
|
114
|
+
if (Array.isArray(f.chain) && f.chain.length && f.source && f.sink) return 'IR-TAINT';
|
|
115
|
+
// AST-driven detectors mark themselves; fall back to REGEX otherwise.
|
|
116
|
+
return 'REGEX';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function backfillFindingDefaults(findings) {
|
|
120
|
+
if (!Array.isArray(findings)) return;
|
|
121
|
+
for (const f of findings) {
|
|
122
|
+
if (!f || typeof f !== 'object') continue;
|
|
123
|
+
if (!f.parser) f.parser = _inferParser(f);
|
|
124
|
+
if (!f.family) {
|
|
125
|
+
const fam = _inferFamily(f);
|
|
126
|
+
if (fam) f.family = fam;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Exported for tests.
|
|
132
|
+
export const _internals = { _CWE_FAMILY, _KEYWORD_FAMILY, _inferFamily, _inferParser };
|