@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,836 @@
|
|
|
1
|
+
// Blast-radius and cost framing — world-class breach cost estimation.
|
|
2
|
+
//
|
|
3
|
+
// Empirical data sources (all citations are public):
|
|
4
|
+
// - IBM Cost of a Data Breach Report 2024 (per-industry, per-record costs)
|
|
5
|
+
// - NetDiligence Cyber Claims Study 2024 (SMB incident cost distributions)
|
|
6
|
+
// - Ponemon Institute studies 2023-2024 (cost components)
|
|
7
|
+
// - Verizon DBIR 2024 (breach pattern frequencies)
|
|
8
|
+
// - HHS OCR HIPAA enforcement records (Tier 1-4 fine bands)
|
|
9
|
+
// - GDPR Enforcement Tracker (EU fine medians and maxes)
|
|
10
|
+
// - Public settlements: Equifax $1.4B, Capital One $190M, T-Mobile $350M,
|
|
11
|
+
// Anthem $115M, Marriott $52M+$124M, Optus AU$140M,
|
|
12
|
+
// Uber $148M, Yahoo $117.5M
|
|
13
|
+
// - PCI-DSS fee schedules (Visa/MC published)
|
|
14
|
+
//
|
|
15
|
+
// Cost model (component sum with three-point estimates):
|
|
16
|
+
// incidentResponse + legal + crisisPR + notification + creditMonitoring +
|
|
17
|
+
// regulatoryFines + directDamage + classAction + lostBusiness
|
|
18
|
+
//
|
|
19
|
+
// Each component is computed for best (P5) / likely (P50) / worst (P95) and
|
|
20
|
+
// summed independently. Scale factors applied per component, not globally.
|
|
21
|
+
//
|
|
22
|
+
// Decoration shape:
|
|
23
|
+
// blastRadius: {
|
|
24
|
+
// scope, dataAtRisk, userCount, industry, jurisdictions,
|
|
25
|
+
// dollarBest, dollarLikely, dollarWorst,
|
|
26
|
+
// dollarLow, dollarHigh, // backward compat (= best / worst)
|
|
27
|
+
// components: { ... per-component low/likely/high ... },
|
|
28
|
+
// controlsApplied: [...], // discount factors detected
|
|
29
|
+
// dominantDriver, comparable, confidence,
|
|
30
|
+
// narrative
|
|
31
|
+
// }
|
|
32
|
+
|
|
33
|
+
import * as fs from 'node:fs';
|
|
34
|
+
import * as path from 'node:path';
|
|
35
|
+
|
|
36
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
37
|
+
// EMPIRICAL DATA TABLES
|
|
38
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
39
|
+
|
|
40
|
+
// Per-record breach cost by data class.
|
|
41
|
+
// Sources: IBM 2024 ($169 avg PII), Ponemon, NetDiligence by-data-class study.
|
|
42
|
+
// Format: { low, likely, high } — interpreted as P5 / P50 / P95.
|
|
43
|
+
const PER_RECORD_COST = {
|
|
44
|
+
'anonymous': { low: 0.50, likely: 2, high: 8 }, // email-only list
|
|
45
|
+
'email': { low: 2, likely: 8, high: 25 }, // email + name
|
|
46
|
+
'pii': { low: 50, likely: 165, high: 400 }, // IBM 2024 baseline
|
|
47
|
+
'pii-sensitive':{ low: 150, likely: 350, high: 900 }, // SSN/DOB/govt-id
|
|
48
|
+
'payment': { low: 100, likely: 220, high: 500 }, // PCI + replacement
|
|
49
|
+
'phi': { low: 250, likely: 500, high: 1500 }, // HIPAA tier 3-4
|
|
50
|
+
'credentials': { low: 20, likely: 60, high: 250 }, // base + reuse risk
|
|
51
|
+
'auth-tokens': { low: 30, likely: 90, high: 300 }, // session theft
|
|
52
|
+
'children': { low: 500, likely: 2000, high: 42530 }, // COPPA max fine
|
|
53
|
+
'rce': { low: 200, likely: 800, high: 5000 }, // infra rebuild + IR
|
|
54
|
+
'config': { low: 5, likely: 25, high: 200 }, // misc internal data
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Industry cost multipliers (IBM 2024 Cost of a Data Breach Report).
|
|
58
|
+
// "Average" baseline = 1.00. Healthcare highest at 1.84×, public sector lowest at 0.45×.
|
|
59
|
+
const INDUSTRY_PROFILES = {
|
|
60
|
+
'healthcare': { mult: 1.84, label: 'healthcare', defaultRegs: ['HIPAA','GDPR','CCPA'] },
|
|
61
|
+
'financial': { mult: 1.16, label: 'financial services', defaultRegs: ['PCI-DSS','SOX','GLBA','GDPR'] },
|
|
62
|
+
'industrial': { mult: 1.10, label: 'industrial', defaultRegs: ['NIS2'] },
|
|
63
|
+
'tech': { mult: 1.05, label: 'technology', defaultRegs: ['GDPR','CCPA'] },
|
|
64
|
+
'pharma': { mult: 1.05, label: 'pharma', defaultRegs: ['HIPAA','GDPR','FDA'] },
|
|
65
|
+
'energy': { mult: 0.96, label: 'energy', defaultRegs: ['NERC-CIP'] },
|
|
66
|
+
'services': { mult: 0.88, label: 'professional services', defaultRegs: ['GDPR'] },
|
|
67
|
+
'manufacturing':{ mult: 0.84, label: 'manufacturing', defaultRegs: [] },
|
|
68
|
+
'retail': { mult: 0.83, label: 'retail / e-commerce', defaultRegs: ['PCI-DSS','GDPR','CCPA'] },
|
|
69
|
+
'education': { mult: 0.74, label: 'education', defaultRegs: ['FERPA','GDPR'] },
|
|
70
|
+
'media': { mult: 0.71, label: 'media', defaultRegs: ['GDPR','CCPA'] },
|
|
71
|
+
'hospitality': { mult: 0.69, label: 'hospitality', defaultRegs: ['PCI-DSS','GDPR'] },
|
|
72
|
+
'consumer': { mult: 0.65, label: 'consumer / B2C', defaultRegs: ['GDPR','CCPA'] },
|
|
73
|
+
'public': { mult: 0.45, label: 'public sector', defaultRegs: ['FISMA','FedRAMP'] },
|
|
74
|
+
'generic': { mult: 1.00, label: 'general SaaS', defaultRegs: [] },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Jurisdiction regulatory fine bands.
|
|
78
|
+
// Realistic SMB-likely outcomes, not theoretical maxes.
|
|
79
|
+
// Sources: GDPR Enforcement Tracker median for <500-employee orgs,
|
|
80
|
+
// HHS OCR settlement records, state AG actions, PCI fee schedules.
|
|
81
|
+
const JURISDICTION_FINES = {
|
|
82
|
+
'GDPR': { low: 5_000, likely: 50_000, high: 500_000 }, // SMB median; theoretical max 4% revenue
|
|
83
|
+
'CCPA': { low: 2_500, likely: 25_000, high: 750_000 }, // $100-$750/CA resident in class
|
|
84
|
+
'HIPAA': { low: 25_000, likely: 250_000, high: 1_900_000 }, // Tier 1-3 typical; max $1.9M/year
|
|
85
|
+
'PCI-DSS': { low: 5_000, likely: 80_000, high: 500_000 }, // $5k-$100k/mo + per-card fines
|
|
86
|
+
'SOX': { low: 50_000, likely: 250_000, high: 5_000_000 }, // SEC enforcement, exec liability
|
|
87
|
+
'GLBA': { low: 25_000, likely: 200_000, high: 1_000_000 }, // FTC/CFPB enforcement
|
|
88
|
+
'COPPA': { low: 50_000, likely: 500_000, high: 5_000_000 }, // YouTube $170M, TikTok $5.7M
|
|
89
|
+
'FERPA': { low: 0, likely: 100_000, high: 5_000_000 }, // worst case: federal funding loss
|
|
90
|
+
'NIS2': { low: 10_000, likely: 100_000, high: 10_000_000 }, // EU critical infra; max 2% revenue
|
|
91
|
+
'NERC-CIP':{ low: 25_000, likely: 500_000, high: 1_000_000 }, // per-day penalty per violation
|
|
92
|
+
'FDA': { low: 50_000, likely: 500_000, high: 5_000_000 }, // 21 CFR Part 11 enforcement
|
|
93
|
+
'FedRAMP': { low: 100_000, likely: 500_000, high: 10_000_000 }, // contract loss + remediation
|
|
94
|
+
'FISMA': { low: 50_000, likely: 250_000, high: 5_000_000 }, // federal contract penalty
|
|
95
|
+
'LGPD': { low: 5_000, likely: 50_000, high: 1_000_000 }, // Brazil; max 2% revenue or R$50M
|
|
96
|
+
'PIPEDA': { low: 5_000, likely: 50_000, high: 100_000 }, // Canada; max C$100k
|
|
97
|
+
'UK-GDPR': { low: 5_000, likely: 50_000, high: 500_000 }, // post-Brexit equivalent
|
|
98
|
+
'STATE': { low: 5_000, likely: 25_000, high: 250_000 }, // US state breach laws stack
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// CWE-to-vuln-name mapping for findings without explicit CWE field.
|
|
102
|
+
const VULN_TO_CWE = {
|
|
103
|
+
'sql injection': 'CWE-89',
|
|
104
|
+
'nosql injection': 'CWE-89',
|
|
105
|
+
'command injection': 'CWE-78',
|
|
106
|
+
'os command': 'CWE-78',
|
|
107
|
+
'rce': 'CWE-78',
|
|
108
|
+
'remote code execution': 'CWE-78',
|
|
109
|
+
'code execution': 'CWE-78',
|
|
110
|
+
'sandbox escape': 'CWE-78',
|
|
111
|
+
'vm sandbox': 'CWE-78',
|
|
112
|
+
'sandbox execution': 'CWE-78',
|
|
113
|
+
'arbitrary code': 'CWE-78',
|
|
114
|
+
'eval injection': 'CWE-78',
|
|
115
|
+
'path traversal': 'CWE-22',
|
|
116
|
+
'zip slip': 'CWE-22',
|
|
117
|
+
'directory traversal': 'CWE-22',
|
|
118
|
+
'ssrf': 'CWE-918',
|
|
119
|
+
'server-side request': 'CWE-918',
|
|
120
|
+
'xss': 'CWE-79',
|
|
121
|
+
'cross-site scripting': 'CWE-79',
|
|
122
|
+
'dangerouslysetinnerhtml': 'CWE-79',
|
|
123
|
+
'idor': 'CWE-639',
|
|
124
|
+
'insecure direct object': 'CWE-639',
|
|
125
|
+
'csrf': 'CWE-352',
|
|
126
|
+
'mass assignment': 'CWE-915',
|
|
127
|
+
'authentication bypass': 'CWE-287',
|
|
128
|
+
'broken authentication': 'CWE-287',
|
|
129
|
+
'jwt': 'CWE-287',
|
|
130
|
+
'webhook': 'CWE-345',
|
|
131
|
+
'signature missing': 'CWE-345',
|
|
132
|
+
'signature verification': 'CWE-345',
|
|
133
|
+
'deserialization': 'CWE-502',
|
|
134
|
+
'prototype pollution': 'CWE-1321',
|
|
135
|
+
'hardcoded': 'CWE-798',
|
|
136
|
+
'password hashing': 'CWE-916',
|
|
137
|
+
'md5': 'CWE-916',
|
|
138
|
+
'sha1': 'CWE-916',
|
|
139
|
+
'weak crypto': 'CWE-327',
|
|
140
|
+
'open redirect': 'CWE-601',
|
|
141
|
+
'xxe': 'CWE-611',
|
|
142
|
+
'xml external': 'CWE-611',
|
|
143
|
+
'missing authorization': 'CWE-862',
|
|
144
|
+
'broken access control': 'CWE-862',
|
|
145
|
+
'missing auth': 'CWE-862',
|
|
146
|
+
'access control': 'CWE-862',
|
|
147
|
+
'session fixation': 'CWE-613',
|
|
148
|
+
'error message': 'CWE-209',
|
|
149
|
+
'stack trace': 'CWE-209',
|
|
150
|
+
'information disclosure': 'CWE-209',
|
|
151
|
+
'file upload': 'CWE-434',
|
|
152
|
+
'unrestricted upload': 'CWE-434',
|
|
153
|
+
'rate limit': 'CWE-400',
|
|
154
|
+
'prompt injection': 'LLM01',
|
|
155
|
+
'llm output': 'LLM02',
|
|
156
|
+
'max_tokens': 'LLM10',
|
|
157
|
+
'unbounded consumption': 'LLM10',
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Comparable real-world incidents to cite in narratives.
|
|
161
|
+
const COMPARABLE_INCIDENTS = {
|
|
162
|
+
'CWE-89': 'Equifax 2017 SQLi → $1.4B settlement (147M records, $9.50/rec)',
|
|
163
|
+
'CWE-78': 'Capital One 2019 SSRF → $190M settlement; full server access typical',
|
|
164
|
+
'CWE-94': 'Log4Shell 2021 code injection → individual orgs $100k–$10M cleanup; CVSS 10.0',
|
|
165
|
+
'CWE-22': 'Snyk 2022 path-traversal disclosure → CDN cache poisoning + .env exfil',
|
|
166
|
+
'CWE-918': 'Capital One 2019 SSRF → $190M settlement (100M records, $1.90/rec)',
|
|
167
|
+
'CWE-79': 'British Airways 2018 XSS skimmer → £20M ICO fine (380k cards)',
|
|
168
|
+
'CWE-639': 'T-Mobile 2023 API IDOR → $350M class settlement (37M records)',
|
|
169
|
+
'CWE-352': 'Numerous; CSRF on payment endpoints chains to direct fraud',
|
|
170
|
+
'CWE-915': 'GitHub mass-assignment 2012 → public ridicule + emergency rebuild',
|
|
171
|
+
'CWE-287': 'Anthem 2015 → $115M settlement (78.8M records); credential stuffing class',
|
|
172
|
+
'CWE-345': 'Stripe webhook spoofing chains to fake "payment succeeded" → plan upgrades',
|
|
173
|
+
'CWE-347': 'JWT forging incidents — auth bypass typically chains to full account takeover',
|
|
174
|
+
'CWE-502': 'Log4Shell 2021 → industry-wide $90B+ remediation; individual orgs $100k–$10M',
|
|
175
|
+
'CWE-798': 'AWS keys leaked: avg $200k crypto-mining bill in <48h; OpenAI keys: $30–100k overnight',
|
|
176
|
+
'CWE-916': 'MD5/SHA1 hash: rainbow tables crack 8-char passwords in <60s on a GPU',
|
|
177
|
+
'CWE-327': 'Adobe 2013 weak ECB encryption → 38M user passwords reverse-engineered',
|
|
178
|
+
'CWE-200': 'Multiple incidents: error messages leak DB schema, secrets, internal paths',
|
|
179
|
+
'CWE-209': 'Stack-trace disclosure: SAP CVE-2022-22536 chained to RCE via internal paths',
|
|
180
|
+
'CWE-601': 'OAuth open-redirect → full token theft (Slack 2018, Stripe 2020)',
|
|
181
|
+
'CWE-611': 'BlackHat 2015 XXE demos → AWS metadata exfil',
|
|
182
|
+
'CWE-862': 'Optus 2022 missing-authz API → AU$140M class settlement (10M records)',
|
|
183
|
+
'CWE-613': 'Adobe 2013 session fixation → 38M credentials',
|
|
184
|
+
'CWE-434': 'Imgur 2014 file upload → RCE via processed image metadata',
|
|
185
|
+
'CWE-400': 'Air Canada 2024 LLM chatbot DoS → court-ordered refunds + reputational damage',
|
|
186
|
+
'CWE-922': 'Client-side storage of session tokens enables XSS → permanent session theft',
|
|
187
|
+
'CWE-1333': 'ReDoS in Express: single regex pin a Node process for 100% CPU',
|
|
188
|
+
'CWE-1321': 'Lodash/Mongoose prototype pollution → auth bypass + RCE (CVE-2019-10744)',
|
|
189
|
+
'CWE-1336': 'Server-side template injection (Pug/Jinja2/Twig) → routine path to RCE',
|
|
190
|
+
'CWE-943': 'NoSQL injection via $-operators → auth bypass (MongoDB queries)',
|
|
191
|
+
'LLM01': 'Air Canada 2024 chatbot prompt injection → binding refund obligations',
|
|
192
|
+
'LLM02': 'LangChain/AutoGPT prompt → SQL chain incidents; LLM output trusted as code',
|
|
193
|
+
'LLM10': 'Reported 2024: single prompt-injection attack ran $40k OpenAI bill in 6 hours',
|
|
194
|
+
'DEFAULT': 'Generic finding — likely cost driven by user count + jurisdiction stack',
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
198
|
+
// PROJECT SIGNAL COLLECTION
|
|
199
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
200
|
+
|
|
201
|
+
function readSafe(p) { try { return fs.readFileSync(p, 'utf8'); } catch { return ''; } }
|
|
202
|
+
function existsAny(root, files) { return files.some(f => fs.existsSync(path.join(root, f))); }
|
|
203
|
+
|
|
204
|
+
export function collectProjectSignals(scanRoot) {
|
|
205
|
+
const sig = {
|
|
206
|
+
// Stack signals
|
|
207
|
+
hasStripe: false, hasAuth: false, hasS3: false, hasAnalytics: false,
|
|
208
|
+
hasUserTable: false, hasPII: false, hasPHI: false, hasSecrets: false,
|
|
209
|
+
hasChildrenData: false, hasFinancialData: false, hasEducationData: false,
|
|
210
|
+
|
|
211
|
+
// Industry classification
|
|
212
|
+
industry: 'generic',
|
|
213
|
+
industryConfidence: 'low',
|
|
214
|
+
|
|
215
|
+
// Jurisdiction exposure
|
|
216
|
+
jurisdictions: new Set(),
|
|
217
|
+
|
|
218
|
+
// Existing controls (discount factors)
|
|
219
|
+
controls: new Set(),
|
|
220
|
+
|
|
221
|
+
// Scale signals
|
|
222
|
+
hasB2BSignals: false,
|
|
223
|
+
hasEnterpriseSignals: false,
|
|
224
|
+
hasGlobalDeployment: false,
|
|
225
|
+
estimatedUsers: null,
|
|
226
|
+
revenueIndicator: 'pre-revenue',
|
|
227
|
+
|
|
228
|
+
dependencies: new Set(),
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// ── 1. Manifest sweep ─────────────────────────────────────────────────────
|
|
232
|
+
const manifests = [
|
|
233
|
+
'package.json', 'requirements.txt', 'pyproject.toml',
|
|
234
|
+
'Gemfile', 'go.mod', 'composer.json', 'Cargo.toml',
|
|
235
|
+
];
|
|
236
|
+
let allManifestText = '';
|
|
237
|
+
for (const m of manifests) {
|
|
238
|
+
const text = readSafe(path.join(scanRoot, m));
|
|
239
|
+
if (!text) continue;
|
|
240
|
+
allManifestText += text + '\n';
|
|
241
|
+
for (const m2 of text.matchAll(/"([\w@/-]+)"\s*:/g)) sig.dependencies.add(m2[1]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Stack detection
|
|
245
|
+
if (/\b(stripe|paddle|chargebee|square|braintree|lemonsqueezy|adyen)\b/i.test(allManifestText)) sig.hasStripe = true;
|
|
246
|
+
if (/\b(clerk|next-auth|@auth\/|lucia-auth|passport|firebase-auth|auth0|supabase|better-auth|workos|okta|saml)\b/i.test(allManifestText)) sig.hasAuth = true;
|
|
247
|
+
if (/\b(aws-sdk|@aws-sdk\/client-s3|boto3|google-cloud-storage|@google-cloud\/storage|azure-storage)\b/i.test(allManifestText)) sig.hasS3 = true;
|
|
248
|
+
if (/\b(posthog|mixpanel|amplitude|segment|ga4|@analytics|heap|hotjar|fullstory)\b/i.test(allManifestText)) sig.hasAnalytics = true;
|
|
249
|
+
if (/\b(linear|jira|salesforce|hubspot|zendesk|intercom|drift|mailgun|sendgrid)\b/i.test(allManifestText)) sig.hasB2BSignals = true;
|
|
250
|
+
if (/\b(workos|okta|saml|scim|onelogin|pingidentity|enterprise-sso)\b/i.test(allManifestText)) sig.hasEnterpriseSignals = true;
|
|
251
|
+
|
|
252
|
+
// ── 2. Industry classification ────────────────────────────────────────────
|
|
253
|
+
const industrySignals = {
|
|
254
|
+
healthcare: /\b(drchrono|healthie|@medplum|fhir|hl7|athena|redox|epic-fhir|cerner|hipaa)\b/i,
|
|
255
|
+
financial: /\b(plaid|dwolla|alpaca|stripe-issuing|stripe-treasury|broker|kyc|aml|sanction-list|finicity|teller)\b/i,
|
|
256
|
+
pharma: /\b(clinical-?trial|fda-cfr|gxp|adverse-event|pharmacovigilance)\b/i,
|
|
257
|
+
education: /\b(canvas-lms|blackboard|moodle|gradebook|edx-platform|student-information)\b/i,
|
|
258
|
+
retail: /\b(shopify|woocommerce|@bigcommerce|magento|medusa|saleor)\b/i,
|
|
259
|
+
hospitality: /\b(opera-pms|cloudbeds|mews|sabre|amadeus|booking-api)\b/i,
|
|
260
|
+
energy: /\b(scada|modbus|opc-ua|iec-61850|dnp3|energy-grid)\b/i,
|
|
261
|
+
media: /\b(brightcove|mux|cloudinary-video|jwplayer|wowza)\b/i,
|
|
262
|
+
public: /\b(\.gov|fedramp|fisma|nist-800-53|gov-cloud)\b/i,
|
|
263
|
+
};
|
|
264
|
+
for (const [ind, re] of Object.entries(industrySignals)) {
|
|
265
|
+
if (re.test(allManifestText)) {
|
|
266
|
+
sig.industry = ind;
|
|
267
|
+
sig.industryConfidence = 'medium';
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// If multiple industry-defining deps found, treat as 'tech' (multi-vertical SaaS)
|
|
272
|
+
if (sig.industry === 'generic' && sig.hasStripe && sig.hasAuth && sig.hasAnalytics) {
|
|
273
|
+
sig.industry = 'tech';
|
|
274
|
+
sig.industryConfidence = 'medium';
|
|
275
|
+
} else if (sig.industry === 'generic' && sig.hasStripe) {
|
|
276
|
+
sig.industry = 'consumer';
|
|
277
|
+
sig.industryConfidence = 'low';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── 3. Schema heuristic — refines industry + data classification ──────────
|
|
281
|
+
const schemaCandidates = [
|
|
282
|
+
'schema.prisma', 'prisma/schema.prisma', 'db/schema.ts', 'db/schema.js',
|
|
283
|
+
'supabase/migrations', 'migrations', 'drizzle/schema.ts',
|
|
284
|
+
'app/models', 'src/models',
|
|
285
|
+
];
|
|
286
|
+
let schemaText = '';
|
|
287
|
+
for (const f of schemaCandidates) {
|
|
288
|
+
const fp = path.join(scanRoot, f);
|
|
289
|
+
if (!fs.existsSync(fp)) continue;
|
|
290
|
+
try {
|
|
291
|
+
const stat = fs.statSync(fp);
|
|
292
|
+
if (stat.isDirectory()) {
|
|
293
|
+
const files = fs.readdirSync(fp).slice(0, 30);
|
|
294
|
+
for (const sub of files) schemaText += readSafe(path.join(fp, sub));
|
|
295
|
+
} else schemaText = readSafe(fp);
|
|
296
|
+
} catch {}
|
|
297
|
+
}
|
|
298
|
+
if (schemaText) {
|
|
299
|
+
if (/\b(?:User|users|profiles?|accounts?|members?)\b/.test(schemaText)) sig.hasUserTable = true;
|
|
300
|
+
if (/\b(?:email|phone|address|first_name|last_name|full_name)\b/i.test(schemaText)) sig.hasPII = true;
|
|
301
|
+
if (/\b(?:ssn|date_of_birth|dob|national_id|passport|government_id|tax_id)\b/i.test(schemaText)) {
|
|
302
|
+
sig.hasPII = true;
|
|
303
|
+
sig.hasFinancialData = true; // sensitive PII triggers financial-class fines
|
|
304
|
+
}
|
|
305
|
+
if (/\b(?:diagnosis|medical|patient|prescription|health_record|mrn|icd|cpt|hipaa)\b/i.test(schemaText)) {
|
|
306
|
+
sig.hasPHI = true;
|
|
307
|
+
if (sig.industry === 'generic') { sig.industry = 'healthcare'; sig.industryConfidence = 'high'; }
|
|
308
|
+
}
|
|
309
|
+
if (/\b(?:card|payment|invoice|charge|subscription|stripe_customer|account_number|routing)\b/i.test(schemaText)) {
|
|
310
|
+
sig.hasStripe = true;
|
|
311
|
+
sig.hasFinancialData = true;
|
|
312
|
+
}
|
|
313
|
+
if (/\b(?:parent_consent|coppa|child_age|guardian|kid_)\b/i.test(schemaText)) {
|
|
314
|
+
sig.hasChildrenData = true;
|
|
315
|
+
}
|
|
316
|
+
if (/\b(?:student|course|grade|transcript|enrollment|gpa)\b/i.test(schemaText)) {
|
|
317
|
+
sig.hasEducationData = true;
|
|
318
|
+
if (sig.industry === 'generic') { sig.industry = 'education'; sig.industryConfidence = 'medium'; }
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ── 4. Jurisdiction detection ─────────────────────────────────────────────
|
|
323
|
+
// Industry-driven jurisdictions
|
|
324
|
+
const indProfile = INDUSTRY_PROFILES[sig.industry] || INDUSTRY_PROFILES.generic;
|
|
325
|
+
for (const j of indProfile.defaultRegs) sig.jurisdictions.add(j);
|
|
326
|
+
|
|
327
|
+
// GDPR (EU presence)
|
|
328
|
+
const i18nDirs = ['locales', 'i18n', 'translations', 'src/locales', 'public/locales'];
|
|
329
|
+
let hasEuLocale = false;
|
|
330
|
+
for (const d of i18nDirs) {
|
|
331
|
+
const dp = path.join(scanRoot, d);
|
|
332
|
+
if (fs.existsSync(dp)) {
|
|
333
|
+
try {
|
|
334
|
+
const entries = fs.readdirSync(dp).join(' ');
|
|
335
|
+
if (/\b(de|fr|es|it|nl|pl|sv|da|fi|cs|hu|pt|el|ro|bg|hr|sk|sl|lt|lv|et)[-_.]/i.test(entries)) hasEuLocale = true;
|
|
336
|
+
} catch {}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (hasEuLocale || /\b(react-cookie-consent|cookieconsent|@osano|@cookiebot|tarteaucitron)\b/i.test(allManifestText)) {
|
|
340
|
+
sig.jurisdictions.add('GDPR');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// CCPA — almost always applies if you have US users and any signal of one
|
|
344
|
+
if (sig.hasPII || sig.hasStripe) sig.jurisdictions.add('CCPA');
|
|
345
|
+
|
|
346
|
+
// HIPAA / PCI-DSS / COPPA / FERPA from data-class detection
|
|
347
|
+
if (sig.hasPHI) sig.jurisdictions.add('HIPAA');
|
|
348
|
+
if (sig.hasStripe || sig.hasFinancialData) sig.jurisdictions.add('PCI-DSS');
|
|
349
|
+
if (sig.hasChildrenData) sig.jurisdictions.add('COPPA');
|
|
350
|
+
if (sig.hasEducationData) sig.jurisdictions.add('FERPA');
|
|
351
|
+
// State breach laws apply to anyone with US users
|
|
352
|
+
if (sig.hasPII || sig.hasStripe) sig.jurisdictions.add('STATE');
|
|
353
|
+
|
|
354
|
+
// UK GDPR — detect en-GB, .co.uk, GBP currency
|
|
355
|
+
if (/\b(en-GB|GBP|VATSchema|\.co\.uk)\b/.test(allManifestText)) sig.jurisdictions.add('UK-GDPR');
|
|
356
|
+
|
|
357
|
+
// ── 5. Existing controls detection (discount factors) ─────────────────────
|
|
358
|
+
// Each control reduces breach cost.
|
|
359
|
+
// WAF — Cloudflare, AWS WAF, Fastly
|
|
360
|
+
if (/\b(cloudflare|@cloudflare|aws-wafv?2?|@fastly\/|imperva)\b/i.test(allManifestText)) sig.controls.add('waf');
|
|
361
|
+
// MFA / 2FA
|
|
362
|
+
if (/\b(speakeasy|otplib|@simplewebauthn|webauthn|notp|twilio-authy|totp)\b/i.test(allManifestText)) sig.controls.add('mfa');
|
|
363
|
+
if (/\b(workos|okta|saml|enterprise-sso)\b/i.test(allManifestText)) sig.controls.add('mfa');
|
|
364
|
+
// SIEM / monitoring
|
|
365
|
+
if (/\b(@sentry|datadog|newrelic|@bugsnag|@logtail|@rollbar|honeycomb)\b/i.test(allManifestText)) sig.controls.add('monitoring');
|
|
366
|
+
// Encryption at rest
|
|
367
|
+
if (/\b(@aws-crypto|@google-cloud\/kms|@azure\/keyvault|node-vault|encrypt-rest)\b/i.test(allManifestText)) sig.controls.add('encryption');
|
|
368
|
+
// Bug bounty program
|
|
369
|
+
if (fs.existsSync(path.join(scanRoot, '.well-known/security.txt')) ||
|
|
370
|
+
fs.existsSync(path.join(scanRoot, 'public/.well-known/security.txt'))) {
|
|
371
|
+
sig.controls.add('bug-bounty');
|
|
372
|
+
}
|
|
373
|
+
// Incident response plan
|
|
374
|
+
if (existsAny(scanRoot, ['DISASTER.md', 'RUNBOOK.md', 'INCIDENT_RESPONSE.md', 'docs/incident-response.md'])) {
|
|
375
|
+
sig.controls.add('ir-plan');
|
|
376
|
+
}
|
|
377
|
+
// Backups
|
|
378
|
+
if (/\bbackup|@aws-sdk\/client-backup|@google-cloud\/backup-dr\b/i.test(allManifestText)) sig.controls.add('backups');
|
|
379
|
+
// SOC2 / ISO 27001 mentions in compliance docs
|
|
380
|
+
const docFiles = ['SECURITY.md', 'COMPLIANCE.md', 'docs/security', 'docs/compliance'];
|
|
381
|
+
let docText = '';
|
|
382
|
+
for (const d of docFiles) { docText += readSafe(path.join(scanRoot, d)); }
|
|
383
|
+
if (/\b(SOC ?2|SOC2|ISO ?27001|ISO27001|PCI ?DSS|HITRUST)\b/i.test(docText)) sig.controls.add('compliance-program');
|
|
384
|
+
|
|
385
|
+
// ── 6. Env files → has production credentials → is live ───────────────────
|
|
386
|
+
for (const f of ['.env', '.env.local', '.env.production']) {
|
|
387
|
+
if (fs.existsSync(path.join(scanRoot, f))) sig.hasSecrets = true;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ── 7. User count + revenue tier estimation ───────────────────────────────
|
|
391
|
+
sig.estimatedUsers = estimateUserCount(sig);
|
|
392
|
+
sig.revenueIndicator = estimateRevenueTier(sig);
|
|
393
|
+
|
|
394
|
+
return sig;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function estimateUserCount(sig) {
|
|
398
|
+
// Enterprise SSO + B2B signals → small but high-value customer base
|
|
399
|
+
if (sig.hasEnterpriseSignals) return 1_000;
|
|
400
|
+
// B2B SaaS without enterprise tier
|
|
401
|
+
if (sig.hasB2BSignals && sig.hasStripe) return 2_500;
|
|
402
|
+
// Consumer app at scale (multiple analytics tools = mature product)
|
|
403
|
+
if (sig.hasAnalytics && sig.hasAuth && sig.hasStripe) return 25_000;
|
|
404
|
+
// Consumer app post-launch
|
|
405
|
+
if (sig.hasAnalytics && sig.hasAuth) return 5_000;
|
|
406
|
+
// Has Stripe + auth → small SaaS
|
|
407
|
+
if (sig.hasStripe && sig.hasAuth) return 1_000;
|
|
408
|
+
// Has auth + DB → launched but small
|
|
409
|
+
if (sig.hasAuth && sig.hasUserTable) return 500;
|
|
410
|
+
// Has auth only — likely pre-launch
|
|
411
|
+
if (sig.hasAuth) return 100;
|
|
412
|
+
// API-only or no auth — internal tool or pre-launch
|
|
413
|
+
return 50;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function estimateRevenueTier(sig) {
|
|
417
|
+
if (sig.hasEnterpriseSignals && sig.hasStripe) return 'scale'; // >$10M ARR
|
|
418
|
+
if (sig.hasB2BSignals && sig.hasStripe) return 'growth'; // $1M-$10M
|
|
419
|
+
if (sig.hasStripe && sig.hasAnalytics) return 'growth';
|
|
420
|
+
if (sig.hasStripe) return 'startup'; // $0-$1M
|
|
421
|
+
if (sig.hasAuth) return 'side-project'; // pre-revenue with users
|
|
422
|
+
return 'pre-revenue';
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
426
|
+
// CWE LOOKUP + SCOPE INFERENCE
|
|
427
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
428
|
+
|
|
429
|
+
function getCwe(finding) {
|
|
430
|
+
if (finding.cwe && typeof finding.cwe === 'string') return finding.cwe;
|
|
431
|
+
const vuln = (finding.vuln || finding.title || '').toLowerCase();
|
|
432
|
+
for (const [kw, cwe] of Object.entries(VULN_TO_CWE)) {
|
|
433
|
+
if (vuln.includes(kw)) return cwe;
|
|
434
|
+
}
|
|
435
|
+
return 'DEFAULT';
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function inferDataClass(finding, signals, cwe) {
|
|
439
|
+
const vuln = (finding.vuln || finding.title || '').toLowerCase();
|
|
440
|
+
const dataAtRisk = [];
|
|
441
|
+
|
|
442
|
+
// CWE/vuln-driven classification
|
|
443
|
+
if (/sql|nosql|injection|idor|mass.assign|deserial|authoriz/i.test(vuln)) {
|
|
444
|
+
if (signals.hasPHI) dataAtRisk.push('phi');
|
|
445
|
+
if (signals.hasFinancialData) dataAtRisk.push('pii-sensitive');
|
|
446
|
+
if (signals.hasPII) dataAtRisk.push('pii');
|
|
447
|
+
if (signals.hasStripe) dataAtRisk.push('payment');
|
|
448
|
+
}
|
|
449
|
+
if (/command|rce|deserial|sandbox|eval/i.test(vuln)) {
|
|
450
|
+
dataAtRisk.push('rce');
|
|
451
|
+
if (signals.hasPHI) dataAtRisk.push('phi');
|
|
452
|
+
if (signals.hasStripe) dataAtRisk.push('payment');
|
|
453
|
+
else if (signals.hasPII) dataAtRisk.push('pii');
|
|
454
|
+
}
|
|
455
|
+
if (/hardcoded|secret|credential/i.test(vuln)) dataAtRisk.push('credentials');
|
|
456
|
+
if (/auth.token|session|jwt|cookie/i.test(vuln)) dataAtRisk.push('auth-tokens');
|
|
457
|
+
if (/webhook|signature/i.test(vuln) && signals.hasStripe) dataAtRisk.push('payment');
|
|
458
|
+
if (/llm|prompt/i.test(vuln)) dataAtRisk.push('config'); // unbounded spend = config-class
|
|
459
|
+
if (/xss/i.test(vuln)) dataAtRisk.push('auth-tokens');
|
|
460
|
+
if (/ssrf/i.test(vuln)) {
|
|
461
|
+
dataAtRisk.push('credentials'); // typically credentials via cloud-metadata
|
|
462
|
+
if (signals.hasPII) dataAtRisk.push('pii');
|
|
463
|
+
}
|
|
464
|
+
if (signals.hasChildrenData && /pii|injection|idor|authoriz/i.test(vuln)) dataAtRisk.push('children');
|
|
465
|
+
|
|
466
|
+
// Fallback
|
|
467
|
+
if (dataAtRisk.length === 0) dataAtRisk.push('config');
|
|
468
|
+
|
|
469
|
+
// Dedupe
|
|
470
|
+
return [...new Set(dataAtRisk)];
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function inferScope(finding, signals) {
|
|
474
|
+
const file = (finding.file || '').toLowerCase();
|
|
475
|
+
if (/\b(admin|internal|backoffice|staff)\b/.test(file)) return 'admin-only';
|
|
476
|
+
if (/\b(payment|billing|checkout|invoice)\b/.test(file)) return 'paying-users';
|
|
477
|
+
return 'all-users';
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
481
|
+
// COST COMPONENTS
|
|
482
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
483
|
+
|
|
484
|
+
// 1. Incident Response — forensics + IR retainer + initial legal opinion.
|
|
485
|
+
// NetDiligence 2024: median crisis services $50k, mean $190k.
|
|
486
|
+
function componentIR(signals, scope) {
|
|
487
|
+
let base = { low: 8_000, likely: 50_000, high: 250_000 };
|
|
488
|
+
if (signals.hasPHI) base = { low: 25_000, likely: 100_000, high: 500_000 };
|
|
489
|
+
else if (signals.hasFinancialData) base = { low: 15_000, likely: 75_000, high: 400_000 };
|
|
490
|
+
if (signals.controls.has('ir-plan')) base = scaleComp(base, 0.65);
|
|
491
|
+
if (signals.controls.has('monitoring')) base = scaleComp(base, 0.80);
|
|
492
|
+
// Admin-only breaches typically faster to contain
|
|
493
|
+
if (scope === 'admin-only') base = scaleComp(base, 0.50);
|
|
494
|
+
return base;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 2. Legal counsel — class-action defense excluded (separate component).
|
|
498
|
+
// Includes breach-counsel, regulatory defense advisory.
|
|
499
|
+
function componentLegal(signals) {
|
|
500
|
+
const base = { low: 10_000, likely: 75_000, high: 500_000 };
|
|
501
|
+
// Each additional jurisdiction adds counsel cost
|
|
502
|
+
const jcount = signals.jurisdictions.size;
|
|
503
|
+
const mult = 1 + Math.min(jcount * 0.25, 2); // cap at 3× for 8+ jurisdictions
|
|
504
|
+
return scaleComp(base, mult);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// 3. PR / crisis management — skipped for sub-100 user side projects.
|
|
508
|
+
function componentPR(signals, userCount, revenueTier) {
|
|
509
|
+
if (userCount < 100 && revenueTier === 'pre-revenue') return zero();
|
|
510
|
+
if (userCount < 1000) return { low: 0, likely: 15_000, high: 75_000 };
|
|
511
|
+
if (userCount < 10000) return { low: 10_000, likely: 50_000, high: 200_000 };
|
|
512
|
+
if (userCount < 100000) return { low: 25_000, likely: 150_000, high: 750_000 };
|
|
513
|
+
return { low: 100_000, likely: 500_000, high: 3_000_000 };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// 4. Notification — FTC benchmark ~$5/user for first-class mail; legal-reviewed
|
|
517
|
+
// template floor of $15k regardless of count.
|
|
518
|
+
function componentNotification(userCount, scope) {
|
|
519
|
+
const affected = scope === 'admin-only' ? Math.min(userCount, 50) : userCount;
|
|
520
|
+
const perUserLow = 2, perUserLikely = 5, perUserHigh = 15;
|
|
521
|
+
const floor = 5_000;
|
|
522
|
+
return {
|
|
523
|
+
low: Math.max(floor, Math.round(affected * perUserLow)),
|
|
524
|
+
likely: Math.max(floor * 2, Math.round(affected * perUserLikely)),
|
|
525
|
+
high: Math.max(floor * 3, Math.round(affected * perUserHigh)),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// 5. Credit monitoring — 1 year offered, only triggered when sensitive PII at risk.
|
|
530
|
+
// Industry standard: $20-$30/user/year. ~30% take-up rate.
|
|
531
|
+
function componentCreditMonitoring(signals, userCount, dataAtRisk, scope) {
|
|
532
|
+
if (!dataAtRisk.some(d => ['pii','pii-sensitive','phi','payment'].includes(d))) return zero();
|
|
533
|
+
if (scope === 'admin-only') userCount = Math.min(userCount, 50);
|
|
534
|
+
const takeUp = 0.3;
|
|
535
|
+
const perYearLow = 12, perYearLikely = 25, perYearHigh = 40;
|
|
536
|
+
return {
|
|
537
|
+
low: Math.round(userCount * takeUp * perYearLow),
|
|
538
|
+
likely: Math.round(userCount * takeUp * perYearLikely),
|
|
539
|
+
high: Math.round(userCount * takeUp * perYearHigh * 2), // 2 years if sensitive
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// 6. Regulatory fines — sum over detected jurisdictions, scaled by user count.
|
|
544
|
+
function componentRegulatory(signals, userCount, dataAtRisk) {
|
|
545
|
+
let sum = zero();
|
|
546
|
+
// Skip regulatory fines entirely for pre-revenue, sub-100-user side projects
|
|
547
|
+
if (signals.revenueIndicator === 'pre-revenue' && userCount < 100) return zero();
|
|
548
|
+
// Need actual user data at risk
|
|
549
|
+
const hasRegulatedData = dataAtRisk.some(d =>
|
|
550
|
+
['pii','pii-sensitive','phi','payment','children','auth-tokens'].includes(d));
|
|
551
|
+
if (!hasRegulatedData) {
|
|
552
|
+
// RCE still triggers minimal regulatory if any user data exists in the system
|
|
553
|
+
if (!dataAtRisk.includes('rce') || !signals.hasUserTable) return zero();
|
|
554
|
+
}
|
|
555
|
+
// Scale band by user count — small breaches get reduced fine bands
|
|
556
|
+
const scale = userCountScale(userCount);
|
|
557
|
+
for (const j of signals.jurisdictions) {
|
|
558
|
+
const band = JURISDICTION_FINES[j];
|
|
559
|
+
if (!band) continue;
|
|
560
|
+
sum = addComp(sum, scaleComp(band, scale.regMult));
|
|
561
|
+
}
|
|
562
|
+
return sum;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// 7. Direct damage — per-record cost × records × industry multiplier × controls discount.
|
|
566
|
+
function componentDirectDamage(signals, userCount, dataAtRisk, scope) {
|
|
567
|
+
// Aggregate per-record cost across all data classes (use max — worst data class drives cost)
|
|
568
|
+
let perRecord = zero();
|
|
569
|
+
for (const d of dataAtRisk) {
|
|
570
|
+
const cost = PER_RECORD_COST[d];
|
|
571
|
+
if (!cost) continue;
|
|
572
|
+
if (cost.likely > perRecord.likely) perRecord = { ...cost };
|
|
573
|
+
}
|
|
574
|
+
if (perRecord.likely === 0) return zero();
|
|
575
|
+
|
|
576
|
+
const industryMult = (INDUSTRY_PROFILES[signals.industry] || INDUSTRY_PROFILES.generic).mult;
|
|
577
|
+
|
|
578
|
+
// Records affected — admin-only contains spread
|
|
579
|
+
const affected = scope === 'admin-only' ? Math.min(userCount, 50) :
|
|
580
|
+
scope === 'paying-users' ? userCount :
|
|
581
|
+
userCount;
|
|
582
|
+
|
|
583
|
+
let comp = {
|
|
584
|
+
low: Math.round(perRecord.low * affected * industryMult),
|
|
585
|
+
likely: Math.round(perRecord.likely * affected * industryMult),
|
|
586
|
+
high: Math.round(perRecord.high * affected * industryMult),
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// Apply controls discounts
|
|
590
|
+
if (signals.controls.has('waf')) comp = scaleComp(comp, 0.85);
|
|
591
|
+
if (signals.controls.has('mfa') && dataAtRisk.some(d => ['auth-tokens','credentials'].includes(d))) {
|
|
592
|
+
comp = scaleComp(comp, 0.70);
|
|
593
|
+
}
|
|
594
|
+
if (signals.controls.has('encryption')) comp = scaleComp(comp, 0.80);
|
|
595
|
+
if (signals.controls.has('compliance-program')) comp = scaleComp(comp, 0.85);
|
|
596
|
+
|
|
597
|
+
return comp;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// 8. Class action exposure — US-specific, only when meaningful affected user count + revenue.
|
|
601
|
+
function componentClassAction(signals, userCount, dataAtRisk) {
|
|
602
|
+
// No class action against pre-revenue or sub-1k breaches
|
|
603
|
+
if (signals.revenueIndicator === 'pre-revenue') return zero();
|
|
604
|
+
if (userCount < 1000) return zero();
|
|
605
|
+
if (!dataAtRisk.some(d => ['pii','pii-sensitive','phi','payment','children'].includes(d))) return zero();
|
|
606
|
+
|
|
607
|
+
const revenueScale = {
|
|
608
|
+
'side-project': 0.05,
|
|
609
|
+
'startup': 0.20,
|
|
610
|
+
'growth': 1.00,
|
|
611
|
+
'scale': 4.00,
|
|
612
|
+
}[signals.revenueIndicator] || 0.5;
|
|
613
|
+
|
|
614
|
+
// Per-record settlement bands from public US class actions
|
|
615
|
+
// Equifax: $9.50, T-Mobile: $9.50, Marriott: $1.6, Capital One: $1.90
|
|
616
|
+
let perRecLow = 1, perRecLikely = 5, perRecHigh = 15;
|
|
617
|
+
if (dataAtRisk.includes('pii-sensitive') || dataAtRisk.includes('phi')) {
|
|
618
|
+
perRecLow = 5; perRecLikely = 15; perRecHigh = 40;
|
|
619
|
+
}
|
|
620
|
+
// Defense costs alone: $1M+ for any meaningful filed claim
|
|
621
|
+
const defenseFloor = 250_000;
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
low: Math.round(Math.max(0, userCount * perRecLow * revenueScale - defenseFloor)),
|
|
625
|
+
likely: Math.round(defenseFloor + userCount * perRecLikely * revenueScale),
|
|
626
|
+
high: Math.round(defenseFloor * 4 + userCount * perRecHigh * revenueScale),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// 9. Lost business / churn — IBM 2024: 39% of total breach cost on average.
|
|
631
|
+
// Applied as percentage of (direct damage + regulatory + class action).
|
|
632
|
+
// B2C churns more than B2B; pre-revenue has no business to lose.
|
|
633
|
+
function componentLostBusiness(signals, otherComponentsSum) {
|
|
634
|
+
if (signals.revenueIndicator === 'pre-revenue') return zero();
|
|
635
|
+
const pct = signals.hasB2BSignals ? 0.25 : 0.40; // B2B more sticky than B2C
|
|
636
|
+
return {
|
|
637
|
+
low: Math.round(otherComponentsSum.low * pct * 0.5),
|
|
638
|
+
likely: Math.round(otherComponentsSum.likely * pct),
|
|
639
|
+
high: Math.round(otherComponentsSum.high * pct * 1.5),
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
644
|
+
// HELPERS
|
|
645
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
646
|
+
|
|
647
|
+
function zero() { return { low: 0, likely: 0, high: 0 }; }
|
|
648
|
+
function addComp(a, b) {
|
|
649
|
+
return { low: a.low + b.low, likely: a.likely + b.likely, high: a.high + b.high };
|
|
650
|
+
}
|
|
651
|
+
function scaleComp(c, m) {
|
|
652
|
+
return { low: Math.round(c.low * m), likely: Math.round(c.likely * m), high: Math.round(c.high * m) };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// User count → regulatory fine scale (small breaches get lower fine bands).
|
|
656
|
+
function userCountScale(userCount) {
|
|
657
|
+
if (userCount <= 100) return { regMult: 0.15 };
|
|
658
|
+
if (userCount <= 1_000) return { regMult: 0.35 };
|
|
659
|
+
if (userCount <= 10_000) return { regMult: 0.70 };
|
|
660
|
+
if (userCount <= 100_000) return { regMult: 1.30 };
|
|
661
|
+
return { regMult: 2.50 };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function fmtMoney(n) {
|
|
665
|
+
if (n >= 1_000_000_000) return `$${(n / 1_000_000_000).toFixed(1)}B`;
|
|
666
|
+
if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(1)}M`;
|
|
667
|
+
if (n >= 1_000) return `$${Math.round(n / 1_000)}k`;
|
|
668
|
+
return `$${Math.round(n)}`;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
672
|
+
// MAIN NARRATIVE BUILDER
|
|
673
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
674
|
+
|
|
675
|
+
function buildNarrative(finding, signals) {
|
|
676
|
+
const cwe = getCwe(finding);
|
|
677
|
+
const scope = inferScope(finding, signals);
|
|
678
|
+
const dataAtRisk = inferDataClass(finding, signals, cwe);
|
|
679
|
+
const userCount = signals.estimatedUsers || 100;
|
|
680
|
+
|
|
681
|
+
// Compute each component as a three-point distribution
|
|
682
|
+
const ir = componentIR(signals, scope);
|
|
683
|
+
const legal = componentLegal(signals);
|
|
684
|
+
const pr = componentPR(signals, userCount, signals.revenueIndicator);
|
|
685
|
+
const notif = componentNotification(userCount, scope);
|
|
686
|
+
const creditMo = componentCreditMonitoring(signals, userCount, dataAtRisk, scope);
|
|
687
|
+
const reg = componentRegulatory(signals, userCount, dataAtRisk);
|
|
688
|
+
const damage = componentDirectDamage(signals, userCount, dataAtRisk, scope);
|
|
689
|
+
const classAct = componentClassAction(signals, userCount, dataAtRisk);
|
|
690
|
+
|
|
691
|
+
// Lost business depends on the sum of direct + regulatory + class action
|
|
692
|
+
const subtotal = addComp(addComp(damage, reg), classAct);
|
|
693
|
+
const lostBiz = componentLostBusiness(signals, subtotal);
|
|
694
|
+
|
|
695
|
+
// Total
|
|
696
|
+
const total = [ir, legal, pr, notif, creditMo, reg, damage, classAct, lostBiz]
|
|
697
|
+
.reduce(addComp, zero());
|
|
698
|
+
|
|
699
|
+
// Identify the dominant cost driver at the "likely" level
|
|
700
|
+
const drivers = [
|
|
701
|
+
['incident response', ir.likely],
|
|
702
|
+
['legal counsel', legal.likely],
|
|
703
|
+
['crisis PR', pr.likely],
|
|
704
|
+
['notification', notif.likely],
|
|
705
|
+
['credit monitoring', creditMo.likely],
|
|
706
|
+
['regulatory fines', reg.likely],
|
|
707
|
+
['direct damage', damage.likely],
|
|
708
|
+
['class action exposure', classAct.likely],
|
|
709
|
+
['lost business / churn', lostBiz.likely],
|
|
710
|
+
].sort((a, b) => b[1] - a[1]);
|
|
711
|
+
const dominantDriver = drivers[0][1] > 0 ? drivers[0][0] : 'incident response (no scaling drivers detected)';
|
|
712
|
+
|
|
713
|
+
// Comparable historical incident — try CWE first, fall back to vuln-name CWE
|
|
714
|
+
let comparable = COMPARABLE_INCIDENTS[cwe];
|
|
715
|
+
if (!comparable) {
|
|
716
|
+
const vuln = (finding.vuln || finding.title || '').toLowerCase();
|
|
717
|
+
for (const [kw, fallbackCwe] of Object.entries(VULN_TO_CWE)) {
|
|
718
|
+
if (vuln.includes(kw) && COMPARABLE_INCIDENTS[fallbackCwe]) {
|
|
719
|
+
comparable = COMPARABLE_INCIDENTS[fallbackCwe];
|
|
720
|
+
break;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (!comparable) comparable = COMPARABLE_INCIDENTS.DEFAULT;
|
|
725
|
+
|
|
726
|
+
// Confidence in estimate
|
|
727
|
+
const confidence = computeConfidence(signals);
|
|
728
|
+
|
|
729
|
+
// Narrative
|
|
730
|
+
const indProfile = INDUSTRY_PROFILES[signals.industry] || INDUSTRY_PROFILES.generic;
|
|
731
|
+
const dataPhrase = describeDataAtRisk(dataAtRisk, userCount);
|
|
732
|
+
const jurStr = [...signals.jurisdictions].slice(0, 4).join(' + ') || 'no specific regulatory exposure';
|
|
733
|
+
const controlsStr = signals.controls.size > 0 ?
|
|
734
|
+
` (controls reducing cost: ${[...signals.controls].join(', ')})` : '';
|
|
735
|
+
const line = finding.line || finding.source?.line || finding.sink?.line || 0;
|
|
736
|
+
|
|
737
|
+
const narrative =
|
|
738
|
+
`${finding.vuln || finding.title} on \`${finding.file}:${line}\` ` +
|
|
739
|
+
`could ${dataAtRisk.includes('rce') ? 'lead to' : 'expose'} ${dataPhrase}. ` +
|
|
740
|
+
`Context: ${indProfile.label} / ${jurStr}${controlsStr}. ` +
|
|
741
|
+
`Estimated cost: best ${fmtMoney(total.low)} · likely ${fmtMoney(total.likely)} · worst ${fmtMoney(total.high)}. ` +
|
|
742
|
+
`Dominant driver: ${dominantDriver}. ` +
|
|
743
|
+
`Comparable: ${comparable}.`;
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
scope,
|
|
747
|
+
dataAtRisk,
|
|
748
|
+
userCount,
|
|
749
|
+
industry: signals.industry,
|
|
750
|
+
jurisdictions: [...signals.jurisdictions],
|
|
751
|
+
controlsApplied: [...signals.controls],
|
|
752
|
+
|
|
753
|
+
// Three-point estimates
|
|
754
|
+
dollarBest: total.low,
|
|
755
|
+
dollarLikely: total.likely,
|
|
756
|
+
dollarWorst: total.high,
|
|
757
|
+
|
|
758
|
+
// Backward-compat aliases (existing consumers read these)
|
|
759
|
+
dollarLow: total.low,
|
|
760
|
+
dollarHigh: total.high,
|
|
761
|
+
|
|
762
|
+
components: {
|
|
763
|
+
incidentResponse: ir,
|
|
764
|
+
legal: legal,
|
|
765
|
+
crisisPR: pr,
|
|
766
|
+
notification: notif,
|
|
767
|
+
creditMonitoring: creditMo,
|
|
768
|
+
regulatoryFines: reg,
|
|
769
|
+
directDamage: damage,
|
|
770
|
+
classAction: classAct,
|
|
771
|
+
lostBusiness: lostBiz,
|
|
772
|
+
},
|
|
773
|
+
|
|
774
|
+
dominantDriver,
|
|
775
|
+
comparable,
|
|
776
|
+
confidence,
|
|
777
|
+
narrative,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function describeDataAtRisk(dataAtRisk, userCount) {
|
|
782
|
+
const n = userCount.toLocaleString();
|
|
783
|
+
if (dataAtRisk.includes('rce')) return 'full server compromise (remote code execution)';
|
|
784
|
+
if (dataAtRisk.includes('phi')) return `health records for ~${n} patients (HIPAA-covered)`;
|
|
785
|
+
if (dataAtRisk.includes('children')) return `children's data for ~${n} minors (COPPA-covered)`;
|
|
786
|
+
if (dataAtRisk.includes('pii-sensitive'))return `sensitive PII (SSN/DOB/govt-ID) for ~${n} users`;
|
|
787
|
+
if (dataAtRisk.includes('payment')) return `payment data for ~${n} users`;
|
|
788
|
+
if (dataAtRisk.includes('pii')) return `PII for ~${n} users`;
|
|
789
|
+
if (dataAtRisk.includes('credentials')) return 'production credentials and API keys';
|
|
790
|
+
if (dataAtRisk.includes('auth-tokens')) return `session tokens for ~${n} users`;
|
|
791
|
+
if (dataAtRisk.includes('email')) return `email/contact info for ~${n} users`;
|
|
792
|
+
return 'configuration / internal data';
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function computeConfidence(signals) {
|
|
796
|
+
let score = 0;
|
|
797
|
+
if (signals.industry !== 'generic') score += 2;
|
|
798
|
+
if (signals.industryConfidence === 'high') score += 1;
|
|
799
|
+
if (signals.estimatedUsers != null) score += 1;
|
|
800
|
+
if (signals.hasUserTable) score += 1;
|
|
801
|
+
if (signals.jurisdictions.size >= 2) score += 1;
|
|
802
|
+
if (signals.controls.size >= 1) score += 1;
|
|
803
|
+
if (score >= 5) return 'high';
|
|
804
|
+
if (score >= 3) return 'medium';
|
|
805
|
+
return 'low';
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
809
|
+
// PUBLIC API
|
|
810
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
811
|
+
|
|
812
|
+
export function enrichWithBlastRadius(scan, scanRoot) {
|
|
813
|
+
const signals = collectProjectSignals(scanRoot || process.cwd());
|
|
814
|
+
let decorated = 0;
|
|
815
|
+
for (const bucket of ['findings', 'secrets', 'logicVulns', 'supplyChain']) {
|
|
816
|
+
for (const f of (scan[bucket] || [])) {
|
|
817
|
+
const br = buildNarrative(f, signals);
|
|
818
|
+
if (br) { f.blastRadius = br; decorated++; }
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
scan.blastRadiusSignals = {
|
|
822
|
+
industry: signals.industry,
|
|
823
|
+
industryConfidence: signals.industryConfidence,
|
|
824
|
+
jurisdictions: [...signals.jurisdictions],
|
|
825
|
+
controls: [...signals.controls],
|
|
826
|
+
estimatedUsers: signals.estimatedUsers,
|
|
827
|
+
revenueIndicator: signals.revenueIndicator,
|
|
828
|
+
hasStripe: signals.hasStripe,
|
|
829
|
+
hasAuth: signals.hasAuth,
|
|
830
|
+
hasUserTable: signals.hasUserTable,
|
|
831
|
+
hasPII: signals.hasPII,
|
|
832
|
+
hasPHI: signals.hasPHI,
|
|
833
|
+
hasS3: signals.hasS3,
|
|
834
|
+
};
|
|
835
|
+
return { decorated, signals };
|
|
836
|
+
}
|