@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,500 @@
|
|
|
1
|
+
// PoC generator (FR-VER-2 — Phase-1 P1.1 of docs/PRD-next-gen-sast-phase1.md).
|
|
2
|
+
//
|
|
3
|
+
// Produces a runnable proof-of-concept file per finding for the top-10 CWE
|
|
4
|
+
// families. The output is consumed in two ways:
|
|
5
|
+
//
|
|
6
|
+
// 1. As metadata on the finding (`f.poc = { lang, code, runHint, kind }`)
|
|
7
|
+
// so reports can render it inline.
|
|
8
|
+
// 2. By the verifier (P1.2) which executes the PoC in a sandbox and tags
|
|
9
|
+
// the finding `verified-exploit` if the PoC demonstrates the vuln on
|
|
10
|
+
// the discovered fixture.
|
|
11
|
+
//
|
|
12
|
+
// SAFETY: templates use intentionally-readable payloads (cat /etc/passwd,
|
|
13
|
+
// alert(document.domain), etc.) — they are designed to PROVE the bug exists,
|
|
14
|
+
// not to weaponize it. No template performs destructive actions, attempts
|
|
15
|
+
// privilege escalation, or makes outbound network requests beyond the
|
|
16
|
+
// project's own localhost endpoints. The verifier sandbox (P1.2) will deny
|
|
17
|
+
// network egress and write access outside the working dir as a second line
|
|
18
|
+
// of defense.
|
|
19
|
+
//
|
|
20
|
+
// Out of scope for P1.1:
|
|
21
|
+
// - Sandbox execution.
|
|
22
|
+
// - Assigning a verified-exploit verdict.
|
|
23
|
+
// - Per-language template variants beyond the primary host language.
|
|
24
|
+
// Those land in P1.2.
|
|
25
|
+
|
|
26
|
+
import { CWE_TO_FAMILY, FAMILY_TO_PRIMARY_CWE } from './poc-cwe-map.js';
|
|
27
|
+
|
|
28
|
+
// ─── Template selectors ─────────────────────────────────────────────────────
|
|
29
|
+
//
|
|
30
|
+
// Each entry: { cwe, family, vulnContains, lang, render(finding, ctx) → code }
|
|
31
|
+
// `vulnContains` is an array of substrings; the first matching template wins.
|
|
32
|
+
// `render` returns the PoC body. The harness wraps it.
|
|
33
|
+
|
|
34
|
+
const TEMPLATES = [
|
|
35
|
+
{
|
|
36
|
+
cwe: 'CWE-89',
|
|
37
|
+
family: 'sql-injection',
|
|
38
|
+
vulnContains: ['SQL Injection', 'NoSQL Injection'],
|
|
39
|
+
lang: 'node',
|
|
40
|
+
kind: 'http-payload',
|
|
41
|
+
render: (f, ctx) => _httpPocNode(ctx, {
|
|
42
|
+
header: 'Demonstrates SQL injection by sending a UNION-style payload.',
|
|
43
|
+
payload: `' UNION SELECT username, password FROM users--`,
|
|
44
|
+
expect: 'response status 500 or body contains "syntax error" / leaked column / SQL stacktrace',
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
cwe: 'CWE-78',
|
|
49
|
+
family: 'command-injection',
|
|
50
|
+
vulnContains: ['Command Injection'],
|
|
51
|
+
lang: 'node',
|
|
52
|
+
kind: 'http-payload',
|
|
53
|
+
render: (f, ctx) => _httpPocNode(ctx, {
|
|
54
|
+
header: 'Demonstrates OS command injection via a shell-metacharacter payload.',
|
|
55
|
+
payload: `; printf "POC_MARKER_$(whoami)\\n"`,
|
|
56
|
+
expect: 'response body contains "POC_MARKER_" — the marker proves the injected command ran',
|
|
57
|
+
}),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
cwe: 'CWE-79',
|
|
61
|
+
family: 'xss',
|
|
62
|
+
vulnContains: ['XSS', 'Reflected XSS', 'Stored XSS', 'DOM XSS', 'document.write'],
|
|
63
|
+
lang: 'node',
|
|
64
|
+
kind: 'http-payload',
|
|
65
|
+
render: (f, ctx) => _httpPocNode(ctx, {
|
|
66
|
+
header: 'Demonstrates reflected XSS by checking the script payload appears unencoded.',
|
|
67
|
+
payload: `"><script>__POC_XSS_${Math.random().toString(36).slice(2, 8)}</script>`,
|
|
68
|
+
expect: 'response body contains the literal <script> payload (proves no HTML encoding)',
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
cwe: 'CWE-22',
|
|
73
|
+
family: 'path-traversal',
|
|
74
|
+
vulnContains: ['Path Traversal'],
|
|
75
|
+
lang: 'node',
|
|
76
|
+
kind: 'http-payload',
|
|
77
|
+
render: (f, ctx) => _httpPocNode(ctx, {
|
|
78
|
+
header: 'Demonstrates path traversal by reading a sentinel file outside the intended dir.',
|
|
79
|
+
payload: `../../../../../../etc/hostname`,
|
|
80
|
+
expect: 'response body contains a hostname-shaped string (lowercased letters/digits, no traversal markers)',
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
cwe: 'CWE-918',
|
|
85
|
+
family: 'ssrf',
|
|
86
|
+
vulnContains: ['SSRF'],
|
|
87
|
+
lang: 'node',
|
|
88
|
+
kind: 'http-payload',
|
|
89
|
+
render: (f, ctx) => _httpPocNode(ctx, {
|
|
90
|
+
header: 'Demonstrates SSRF by forcing the server to fetch a localhost sentinel URL.',
|
|
91
|
+
// Use a port we own in the verifier sandbox; never the real cloud metadata IP.
|
|
92
|
+
payload: `http://127.0.0.1:65533/poc-ssrf-sentinel`,
|
|
93
|
+
expect: 'sentinel server logs a request from the target — proves the target made an outbound call we controlled',
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
cwe: 'CWE-94',
|
|
98
|
+
family: 'code-injection',
|
|
99
|
+
vulnContains: ['Code Injection', 'VM Sandbox'],
|
|
100
|
+
lang: 'node',
|
|
101
|
+
kind: 'http-payload',
|
|
102
|
+
render: (f, ctx) => _httpPocNode(ctx, {
|
|
103
|
+
header: 'Demonstrates JS code injection via an eval-ed expression with a side effect.',
|
|
104
|
+
// Set a global; the target evals it; we check it via a second request to a debug endpoint.
|
|
105
|
+
payload: `1; (globalThis.__POC_CODE_INJ__ = "yes"); 1`,
|
|
106
|
+
expect: 'server-side __POC_CODE_INJ__ becomes "yes" — observe via /debug or via observing 200 with no exception',
|
|
107
|
+
}),
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
cwe: 'CWE-352',
|
|
111
|
+
family: 'csrf',
|
|
112
|
+
vulnContains: ['CSRF', 'Missing CSRF protection'],
|
|
113
|
+
lang: 'node',
|
|
114
|
+
kind: 'http-csrf',
|
|
115
|
+
render: (f, ctx) => _csrfPocNode(ctx),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
cwe: 'CWE-601',
|
|
119
|
+
family: 'open-redirect',
|
|
120
|
+
vulnContains: ['Open Redirect'],
|
|
121
|
+
lang: 'node',
|
|
122
|
+
kind: 'http-payload',
|
|
123
|
+
render: (f, ctx) => _httpPocNode(ctx, {
|
|
124
|
+
header: 'Demonstrates open redirect by checking Location header points at attacker-controlled host.',
|
|
125
|
+
payload: `https://poc-attacker.invalid/`,
|
|
126
|
+
expect: 'response status 30x AND Location header starts with https://poc-attacker.invalid/',
|
|
127
|
+
noFollow: true,
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
cwe: 'CWE-611',
|
|
132
|
+
family: 'xxe',
|
|
133
|
+
vulnContains: ['XXE', 'Unsafe XML Parsing'],
|
|
134
|
+
lang: 'node',
|
|
135
|
+
kind: 'http-xml',
|
|
136
|
+
render: (f, ctx) => _xxePocNode(ctx),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
cwe: 'CWE-502',
|
|
140
|
+
family: 'insecure-deserialization',
|
|
141
|
+
vulnContains: ['Insecure Deserialization', 'Insecure Java Deserialization', 'Unsafe Deserialization'],
|
|
142
|
+
lang: 'node',
|
|
143
|
+
kind: 'http-payload',
|
|
144
|
+
render: (f, ctx) => _httpPocNode(ctx, {
|
|
145
|
+
header: 'Demonstrates unsafe deserialization with a benign marker-emitting payload.',
|
|
146
|
+
payload: `{"__class__":"PocMarker","value":"deserialization-reached"}`,
|
|
147
|
+
expect: 'server-side log includes "PocMarker" — proves the deserialization callback fired',
|
|
148
|
+
}),
|
|
149
|
+
},
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
// ─── PoC harness templates (Node.js) ────────────────────────────────────────
|
|
153
|
+
//
|
|
154
|
+
// Generic harness wraps the payload in a self-contained Node script with:
|
|
155
|
+
// - one fetch() call to the discovered route
|
|
156
|
+
// - exit 0 on demonstrated exploit, non-zero otherwise
|
|
157
|
+
// - all observations printed to stderr for the verifier to parse
|
|
158
|
+
|
|
159
|
+
function _httpPocNode(ctx, { header, payload, expect, noFollow = false }) {
|
|
160
|
+
const url = ctx.url || 'http://localhost:3000/REPLACE-WITH-ENDPOINT';
|
|
161
|
+
const method = (ctx.method || 'POST').toUpperCase();
|
|
162
|
+
const param = ctx.param || 'input';
|
|
163
|
+
const safePayload = String(payload).replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
164
|
+
return `// ${header}
|
|
165
|
+
// Endpoint: ${method} ${url}
|
|
166
|
+
// Payload: ${safePayload.slice(0, 120)}${safePayload.length > 120 ? '…' : ''}
|
|
167
|
+
// Expect: ${expect}
|
|
168
|
+
// Run: node poc.mjs
|
|
169
|
+
// Exit code: 0 = exploit demonstrated, 1 = not demonstrated, 2 = error
|
|
170
|
+
|
|
171
|
+
const URL_ = ${JSON.stringify(url)};
|
|
172
|
+
const METHOD = ${JSON.stringify(method)};
|
|
173
|
+
const PAYLOAD = \`${safePayload}\`;
|
|
174
|
+
|
|
175
|
+
const body = METHOD === 'GET'
|
|
176
|
+
? null
|
|
177
|
+
: JSON.stringify({ ${JSON.stringify(param)}: PAYLOAD });
|
|
178
|
+
|
|
179
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
180
|
+
|
|
181
|
+
const reqUrl = METHOD === 'GET'
|
|
182
|
+
? URL_ + (URL_.includes('?') ? '&' : '?') + ${JSON.stringify(param)} + '=' + encodeURIComponent(PAYLOAD)
|
|
183
|
+
: URL_;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const r = await fetch(reqUrl, { method: METHOD, headers, body, redirect: ${noFollow ? "'manual'" : "'follow'"} });
|
|
187
|
+
const text = await r.text();
|
|
188
|
+
const sig = ${_evidenceSignal(expect, payload)};
|
|
189
|
+
if (sig) {
|
|
190
|
+
process.stderr.write('PoC: exploit demonstrated — ' + sig + '\\n');
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
process.stderr.write('PoC: payload sent (status ' + r.status + '), no exploit evidence in response\\n');
|
|
194
|
+
process.exit(1);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
process.stderr.write('PoC: error reaching target — ' + e.message + '\\n');
|
|
197
|
+
process.exit(2);
|
|
198
|
+
}
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function _evidenceSignal(expect, payload) {
|
|
203
|
+
// Produce a JS expression that returns a non-empty string on demonstrated
|
|
204
|
+
// exploit. Each template's evidence shape differs slightly; we infer from
|
|
205
|
+
// the `expect` and `payload` what to check for.
|
|
206
|
+
const exp = String(expect || '').toLowerCase();
|
|
207
|
+
if (exp.includes('marker_')) return `(text.match(/POC_MARKER_\\w+/)?.[0] ?? '')`;
|
|
208
|
+
if (exp.includes('script>') || exp.includes('unencoded')) return `(text.includes('<script>__POC_XSS') ? 'unencoded <script> in response' : '')`;
|
|
209
|
+
if (exp.includes('syntax error') || exp.includes('sql')) return `(/syntax error|psql|mysql|sqlite|near \"/i.test(text) ? 'sql error reflected' : '')`;
|
|
210
|
+
if (exp.includes('hostname')) return `(text && /^[a-z0-9\\-]+$/i.test(text.trim().slice(0, 64)) ? 'hostname-shaped response' : '')`;
|
|
211
|
+
if (exp.includes('location header')) return `(r.status >= 300 && r.status < 400 && r.headers.get('location')?.startsWith('https://poc-attacker.invalid') ? 'redirect to attacker host' : '')`;
|
|
212
|
+
if (exp.includes('pocmarker') || exp.includes('marker')) return `(text.includes('PocMarker') ? 'deserialization marker echoed' : '')`;
|
|
213
|
+
if (exp.includes('__poc_code_inj__')) return `(r.status === 200 ? 'code-eval accepted (200 with no error)' : '')`;
|
|
214
|
+
// Default: presence of the payload string itself reflected in response.
|
|
215
|
+
return `(text.includes(${JSON.stringify(String(payload).slice(0, 40))}) ? 'payload reflected' : '')`;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function _csrfPocNode(ctx) {
|
|
219
|
+
const url = ctx.url || 'http://localhost:3000/REPLACE-WITH-ENDPOINT';
|
|
220
|
+
return `// Demonstrates CSRF by making a state-changing request from an off-origin
|
|
221
|
+
// context with NO csrf token AND a cookie-only session — if it succeeds,
|
|
222
|
+
// the route is unprotected.
|
|
223
|
+
// Run: node poc.mjs
|
|
224
|
+
// Exit code: 0 = state-changing request accepted (vulnerable), 1 = rejected
|
|
225
|
+
|
|
226
|
+
const URL_ = ${JSON.stringify(url)};
|
|
227
|
+
const ATTACKER_ORIGIN = 'https://attacker.invalid';
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
const r = await fetch(URL_, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: { 'Content-Type': 'application/json', Origin: ATTACKER_ORIGIN, Referer: ATTACKER_ORIGIN + '/' },
|
|
233
|
+
body: JSON.stringify({ csrfMarker: 'forged' }),
|
|
234
|
+
redirect: 'manual',
|
|
235
|
+
});
|
|
236
|
+
if (r.status >= 200 && r.status < 300) {
|
|
237
|
+
process.stderr.write('PoC: route accepted forged-origin state-change (status ' + r.status + ')\\n');
|
|
238
|
+
process.exit(0);
|
|
239
|
+
}
|
|
240
|
+
process.stderr.write('PoC: route rejected (status ' + r.status + ') — possibly CSRF-protected\\n');
|
|
241
|
+
process.exit(1);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
process.stderr.write('PoC: error reaching target — ' + e.message + '\\n');
|
|
244
|
+
process.exit(2);
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function _xxePocNode(ctx) {
|
|
250
|
+
const url = ctx.url || 'http://localhost:3000/REPLACE-WITH-ENDPOINT';
|
|
251
|
+
return `// Demonstrates XXE by submitting an XML body that references an external
|
|
252
|
+
// entity. We pin to a local sentinel file the verifier sandbox provides.
|
|
253
|
+
// Run: node poc.mjs
|
|
254
|
+
// Exit code: 0 = sentinel content appears in response (XXE confirmed), 1 = not
|
|
255
|
+
|
|
256
|
+
const URL_ = ${JSON.stringify(url)};
|
|
257
|
+
const SENTINEL = '/tmp/poc-xxe-sentinel-' + Math.random().toString(36).slice(2,8);
|
|
258
|
+
|
|
259
|
+
const XML = \`<?xml version="1.0"?>
|
|
260
|
+
<!DOCTYPE root [<!ENTITY xxe SYSTEM "file://\${SENTINEL}">]>
|
|
261
|
+
<root>&xxe;</root>\`;
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
// Verifier sandbox writes SENTINEL with a known string before running this PoC.
|
|
265
|
+
const r = await fetch(URL_, { method: 'POST', headers: { 'Content-Type': 'application/xml' }, body: XML });
|
|
266
|
+
const text = await r.text();
|
|
267
|
+
if (text.includes('XXE_SENTINEL_CONTENT')) {
|
|
268
|
+
process.stderr.write('PoC: XXE confirmed — sentinel content leaked into response\\n');
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
process.stderr.write('PoC: payload accepted (status ' + r.status + '), no sentinel content in response\\n');
|
|
272
|
+
process.exit(1);
|
|
273
|
+
} catch (e) {
|
|
274
|
+
process.stderr.write('PoC: error reaching target — ' + e.message + '\\n');
|
|
275
|
+
process.exit(2);
|
|
276
|
+
}
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Select a template for a finding. Returns the template object or null.
|
|
284
|
+
*/
|
|
285
|
+
function pickTemplate(finding) {
|
|
286
|
+
if (!finding || typeof finding !== 'object') return null;
|
|
287
|
+
const vuln = String(finding.vuln || '');
|
|
288
|
+
const cwe = String(finding.cwe || '').toUpperCase();
|
|
289
|
+
for (const t of TEMPLATES) {
|
|
290
|
+
if (t.cwe === cwe) return t;
|
|
291
|
+
}
|
|
292
|
+
for (const t of TEMPLATES) {
|
|
293
|
+
if (t.vulnContains.some(substr => vuln.includes(substr))) return t;
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Premortem #12: infer the request-body/query/params key the handler reads
|
|
299
|
+
// from the actual finding evidence — NOT a hardcoded 'id'. Sources looked at:
|
|
300
|
+
// 1. finding.source.label (taint engine sets this to e.g. "req.body.host")
|
|
301
|
+
// 2. finding.whyFired.evidence.sourceSnippet (regex parses out req.body.X)
|
|
302
|
+
// 3. finding.snippet (last-resort: regex over the sink line itself)
|
|
303
|
+
// Premortem #12: infer the param key from the actual handler code.
|
|
304
|
+
// Strategy:
|
|
305
|
+
// 1. Re-read a wide window from the file (line-2 .. line+25). This
|
|
306
|
+
// survives detector snippet misattribution (premortem 2R-D).
|
|
307
|
+
// 2. Find every req.body.X / req.query.X / req.params.X / req.headers.X
|
|
308
|
+
// AND every request.json["X"]/form.get("X")/args.get("X") match in
|
|
309
|
+
// the window, with the line on which it appears.
|
|
310
|
+
// 3. Find every "sink" keyword (exec, eval, query, system, spawn,
|
|
311
|
+
// Runtime.exec, fs.readFile, render, redirect) and its line.
|
|
312
|
+
// 4. Return the param whose line is closest to a sink keyword. Ties
|
|
313
|
+
// go to body > query > params (HTTP semantics: body is the most
|
|
314
|
+
// user-controlled vector). If we have nothing, fall back to the
|
|
315
|
+
// detector's snippet/source.label so we still produce SOMETHING.
|
|
316
|
+
const _SINK_KEYWORDS = /\b(?:exec|eval|spawn|spawnSync|execSync|system|popen|query|raw|readFile|readFileSync|writeFile|redirect|render|innerHTML|setAttribute|location|open|require)\b/;
|
|
317
|
+
const _PARAM_RES = [
|
|
318
|
+
{ re: /\breq(?:uest)?\.body\.([A-Za-z_$][\w$]*)/g, score: 3 },
|
|
319
|
+
{ re: /\breq(?:uest)?\.body\[["']([^"']+)["']\]/g, score: 3 },
|
|
320
|
+
{ re: /\breq(?:uest)?\.query\.([A-Za-z_$][\w$]*)/g, score: 2 },
|
|
321
|
+
{ re: /\breq(?:uest)?\.query\[["']([^"']+)["']\]/g, score: 2 },
|
|
322
|
+
{ re: /\breq(?:uest)?\.params\.([A-Za-z_$][\w$]*)/g, score: 1 },
|
|
323
|
+
{ re: /\breq(?:uest)?\.headers\.([A-Za-z_$][\w$]*)/g, score: 1 },
|
|
324
|
+
{ re: /\brequest\.(?:json|form|args)(?:\.get)?\(["']([^"']+)["']\)/g, score: 3 },
|
|
325
|
+
{ re: /\bctx\.request\.body\.([A-Za-z_$][\w$]*)/g, score: 3 },
|
|
326
|
+
];
|
|
327
|
+
function _inferParamKey(finding, fileContents) {
|
|
328
|
+
// Premortem #12: some detectors set f.sink.line / f.source.line instead of
|
|
329
|
+
// f.line. Try all locations so window analysis works regardless of which
|
|
330
|
+
// detector emitted the finding (PoC runs before the normalizer collapses).
|
|
331
|
+
const effectiveLine = finding.line || finding.sink?.line || finding.source?.line || 0;
|
|
332
|
+
const effectiveFile = finding.file || finding.sink?.file || finding.source?.file || null;
|
|
333
|
+
if (process.env.AGENTIC_SECURITY_POC_DEBUG === '1') {
|
|
334
|
+
process.stderr.write(`[poc-debug] file=${effectiveFile} line=${effectiveLine} fc=${fileContents ? Object.keys(fileContents).length : 0}\n`);
|
|
335
|
+
}
|
|
336
|
+
// First-pass: file-window analysis. This is the most reliable.
|
|
337
|
+
if (fileContents && effectiveFile && effectiveLine) {
|
|
338
|
+
const code = fileContents[effectiveFile];
|
|
339
|
+
if (typeof code === 'string') {
|
|
340
|
+
const lines = code.split('\n');
|
|
341
|
+
const idx = (effectiveLine || 1) - 1;
|
|
342
|
+
const lo = Math.max(0, idx - 2);
|
|
343
|
+
const hi = Math.min(lines.length, idx + 26);
|
|
344
|
+
// Find sink keyword lines in window (relative to window start).
|
|
345
|
+
const sinkLines = [];
|
|
346
|
+
for (let i = lo; i < hi; i++) {
|
|
347
|
+
if (_SINK_KEYWORDS.test(lines[i])) sinkLines.push(i);
|
|
348
|
+
}
|
|
349
|
+
// Find param matches in window.
|
|
350
|
+
const matches = []; // { name, score, line }
|
|
351
|
+
for (let i = lo; i < hi; i++) {
|
|
352
|
+
const line = lines[i];
|
|
353
|
+
for (const { re, score } of _PARAM_RES) {
|
|
354
|
+
re.lastIndex = 0;
|
|
355
|
+
let m;
|
|
356
|
+
while ((m = re.exec(line)) !== null) {
|
|
357
|
+
if (m[1]) matches.push({ name: m[1], score, line: i });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (matches.length) {
|
|
362
|
+
// Rank by (closest-to-sink, then higher base score, then earliest in
|
|
363
|
+
// file). When no sink line is detected, fall back to score+order.
|
|
364
|
+
const distTo = (ln) => sinkLines.length
|
|
365
|
+
? Math.min(...sinkLines.map(s => Math.abs(s - ln)))
|
|
366
|
+
: 999;
|
|
367
|
+
matches.sort((a, b) => {
|
|
368
|
+
const da = distTo(a.line), db = distTo(b.line);
|
|
369
|
+
if (da !== db) return da - db;
|
|
370
|
+
if (a.score !== b.score) return b.score - a.score;
|
|
371
|
+
return a.line - b.line;
|
|
372
|
+
});
|
|
373
|
+
return matches[0].name;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
// Fallbacks: snippet / source label (may be misattributed; LAST RESORT).
|
|
378
|
+
const candidates = [];
|
|
379
|
+
if (finding.source?.label) candidates.push(String(finding.source.label));
|
|
380
|
+
const wf = finding.whyFired && finding.whyFired.evidence;
|
|
381
|
+
if (wf?.sinkSnippet) candidates.push(String(wf.sinkSnippet));
|
|
382
|
+
if (finding.snippet) candidates.push(String(finding.snippet));
|
|
383
|
+
for (const c of candidates) {
|
|
384
|
+
for (const { re } of _PARAM_RES) {
|
|
385
|
+
re.lastIndex = 0;
|
|
386
|
+
const m = re.exec(c);
|
|
387
|
+
if (m && m[1]) return m[1];
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Resolve the HTTP endpoint context for a finding from the project's
|
|
395
|
+
* discovered route list. Returns { url, method, param } or null.
|
|
396
|
+
*/
|
|
397
|
+
function endpointFor(finding, routes, fileContents) {
|
|
398
|
+
if (!Array.isArray(routes) || routes.length === 0) return null;
|
|
399
|
+
// Match by file + line proximity.
|
|
400
|
+
const fp = finding.file || finding.sink?.file;
|
|
401
|
+
const ln = finding.line || finding.sink?.line || 0;
|
|
402
|
+
if (!fp) return null;
|
|
403
|
+
let best = null;
|
|
404
|
+
let bestDist = Infinity;
|
|
405
|
+
for (const r of routes) {
|
|
406
|
+
if (r.file !== fp) continue;
|
|
407
|
+
const dist = Math.abs((r.line || 0) - ln);
|
|
408
|
+
if (dist < bestDist) { bestDist = dist; best = r; }
|
|
409
|
+
}
|
|
410
|
+
if (!best) return null;
|
|
411
|
+
// Harness-engineering note (post-derived): when the deterministic inference
|
|
412
|
+
// fails, surface the uncertainty instead of falling back to a generic key.
|
|
413
|
+
// A PoC that posts to 'input' against a handler that reads 'host' is a
|
|
414
|
+
// silent failure — the scanner emitted something, the verifier ran it, and
|
|
415
|
+
// both reported "no exploit demonstrated" when the actual problem was that
|
|
416
|
+
// we asked the wrong question. Better to mark the PoC as low-confidence so
|
|
417
|
+
// downstream (verifier, regression-test-gen, reports) can route accordingly.
|
|
418
|
+
const inferred = _inferParamKey(finding, fileContents);
|
|
419
|
+
const fromSourceVar = finding.source?.variable;
|
|
420
|
+
const paramKey = inferred || fromSourceVar || 'input';
|
|
421
|
+
const paramKeyConfidence =
|
|
422
|
+
inferred ? 'high' // from real file-window analysis
|
|
423
|
+
: fromSourceVar ? 'medium' // detector hinted; might be stale
|
|
424
|
+
: 'low'; // pure default — PoC likely won't fire
|
|
425
|
+
return {
|
|
426
|
+
url: 'http://localhost:3000' + (best.path || '/REPLACE-WITH-ENDPOINT'),
|
|
427
|
+
method: best.method || 'POST',
|
|
428
|
+
param: paramKey,
|
|
429
|
+
paramKeyConfidence,
|
|
430
|
+
paramKeyInferred: !!inferred,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Generate a PoC object for a finding. Returns:
|
|
436
|
+
* { lang, code, runHint, kind, cwe } when a template matches.
|
|
437
|
+
* null when no template covers this CWE family in v1.
|
|
438
|
+
*/
|
|
439
|
+
export function generatePoc(finding, { routes = [], fileContents = null } = {}) {
|
|
440
|
+
const t = pickTemplate(finding);
|
|
441
|
+
if (!t) return null;
|
|
442
|
+
const ctx = endpointFor(finding, routes, fileContents) || {};
|
|
443
|
+
let code;
|
|
444
|
+
try { code = t.render(finding, ctx); }
|
|
445
|
+
catch (e) {
|
|
446
|
+
// Fail-closed: an exception in a template never crashes the scan.
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
if (!code || typeof code !== 'string' || code.length < 50) return null;
|
|
450
|
+
return {
|
|
451
|
+
lang: t.lang,
|
|
452
|
+
kind: t.kind,
|
|
453
|
+
cwe: t.cwe,
|
|
454
|
+
family: t.family,
|
|
455
|
+
runHint: t.lang === 'node' ? 'node poc.mjs' :
|
|
456
|
+
t.lang === 'python' ? 'python3 poc.py' :
|
|
457
|
+
t.lang === 'java' ? 'javac PoC.java && java PoC' :
|
|
458
|
+
null,
|
|
459
|
+
code,
|
|
460
|
+
// Surface the deterministic-inference confidence on the emitted PoC so
|
|
461
|
+
// the verifier and regression-test-gen can refuse to run uncertain ones.
|
|
462
|
+
paramKey: ctx.param || null,
|
|
463
|
+
paramKeyConfidence: ctx.paramKeyConfidence || 'low',
|
|
464
|
+
paramKeyInferred: !!ctx.paramKeyInferred,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Annotate findings with PoCs. Used by the engine's emit pipeline.
|
|
470
|
+
*
|
|
471
|
+
* Sets `f.poc` to either the generated PoC object or `null` (explicit "no
|
|
472
|
+
* template covers this CWE family in v1"). Never throws.
|
|
473
|
+
*/
|
|
474
|
+
export function annotatePocs(findings, opts = {}) {
|
|
475
|
+
const routes = opts.routes || [];
|
|
476
|
+
const fileContents = opts.fileContents || null;
|
|
477
|
+
if (!Array.isArray(findings)) return;
|
|
478
|
+
for (const f of findings) {
|
|
479
|
+
if (!f || typeof f !== 'object') continue;
|
|
480
|
+
f.poc = generatePoc(f, { routes, fileContents });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Surface table of CWE → family → primary lang → expected PoC presence for
|
|
485
|
+
// the reporter to render coverage in the "what we can/can't prove" section.
|
|
486
|
+
export function pocCoverageSummary(findings) {
|
|
487
|
+
const summary = { withPoc: 0, withoutPoc: 0, byFamily: {} };
|
|
488
|
+
for (const f of findings) {
|
|
489
|
+
if (!f || typeof f !== 'object') continue;
|
|
490
|
+
const fam = f.family || 'unknown';
|
|
491
|
+
summary.byFamily[fam] ||= { withPoc: 0, withoutPoc: 0 };
|
|
492
|
+
if (f.poc) { summary.withPoc++; summary.byFamily[fam].withPoc++; }
|
|
493
|
+
else { summary.withoutPoc++; summary.byFamily[fam].withoutPoc++; }
|
|
494
|
+
}
|
|
495
|
+
return summary;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// For tests and the no-dead-modules check — surfaces template count.
|
|
499
|
+
export function _templateCount() { return TEMPLATES.length; }
|
|
500
|
+
export const _knownCwes = TEMPLATES.map(t => t.cwe);
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Policy-as-code gate (FR-SDLC-9).
|
|
2
|
+
//
|
|
3
|
+
// Today's CI gate is `--fail-on <severity>`. That's coarse. Customers want
|
|
4
|
+
// to write nuanced rules: "fail if any sql-injection finding has
|
|
5
|
+
// confidence ≥ 0.8 AND the file is under src/api/", or "fail if total
|
|
6
|
+
// exploitability score across critical findings exceeds 5".
|
|
7
|
+
//
|
|
8
|
+
// We support two modes:
|
|
9
|
+
//
|
|
10
|
+
// 1. EXTERNAL OPA: if the `opa` binary is on PATH and `--policy <file.rego>`
|
|
11
|
+
// is supplied, we shell out to `opa eval -d <file> -i <findings.json>
|
|
12
|
+
// "data.<package>.deny"`. This is the right answer for customers who
|
|
13
|
+
// already use OPA elsewhere.
|
|
14
|
+
//
|
|
15
|
+
// 2. EMBEDDED MINI: when no opa binary is available, fall back to a tiny
|
|
16
|
+
// DSL that's a strict subset of rego. Rules read top-level
|
|
17
|
+
// `package`/`deny` blocks; each `deny` is a JS-evaluable expression
|
|
18
|
+
// over findings[]. This lets the v1 ship without an external binary
|
|
19
|
+
// dep while documenting the upgrade path.
|
|
20
|
+
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
|
|
24
|
+
// ─── External OPA ─────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
function _haveOpa() {
|
|
27
|
+
try {
|
|
28
|
+
const r = spawnSync('opa', ['version'], { stdio: 'ignore', timeout: 3000 });
|
|
29
|
+
return r.status === 0;
|
|
30
|
+
} catch { return false; }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function _runOpa(policyFile, findingsJsonPath, packageName) {
|
|
34
|
+
const r = spawnSync('opa', [
|
|
35
|
+
'eval', '-d', policyFile, '-i', findingsJsonPath,
|
|
36
|
+
`data.${packageName}.deny`,
|
|
37
|
+
], { encoding: 'utf8', timeout: 10_000 });
|
|
38
|
+
if (r.error) return { ok: false, reason: `opa-error:${r.error.code || r.error.message}` };
|
|
39
|
+
if (r.status !== 0) return { ok: false, reason: `opa-exit:${r.status}`, stderr: r.stderr };
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(r.stdout);
|
|
42
|
+
const result = parsed.result?.[0]?.expressions?.[0]?.value;
|
|
43
|
+
return { ok: true, denials: Array.isArray(result) ? result : [] };
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return { ok: false, reason: `opa-output-parse:${e.message}` };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Embedded mini DSL ────────────────────────────────────────────────────
|
|
50
|
+
//
|
|
51
|
+
// Rego is too big to reimplement. We support a tiny shape:
|
|
52
|
+
//
|
|
53
|
+
// # POLICY: agentic-security policy-gate v1
|
|
54
|
+
// deny[msg] {
|
|
55
|
+
// finding := input.findings[_]
|
|
56
|
+
// finding.severity == "critical"
|
|
57
|
+
// msg := sprintf("critical finding: %v at %v", [finding.vuln, finding.file])
|
|
58
|
+
// }
|
|
59
|
+
//
|
|
60
|
+
// Parser strategy: extract each `deny[msg] { ... }` block; translate the
|
|
61
|
+
// body to a JS predicate. The grammar we accept is:
|
|
62
|
+
//
|
|
63
|
+
// - `<lhs> == <value>` / `<lhs> != <value>` / `<lhs> > <num>` / `<lhs> < <num>`
|
|
64
|
+
// - `<lhs>` references `finding.<field>` or `input.<field>`
|
|
65
|
+
// - `msg := "..."` or `msg := sprintf("...", [args])` — the msg literal
|
|
66
|
+
// - newlines + `;` as separators
|
|
67
|
+
//
|
|
68
|
+
// Anything more complex requires the external OPA binary.
|
|
69
|
+
|
|
70
|
+
function _parseEmbedded(policyText) {
|
|
71
|
+
const blocks = [];
|
|
72
|
+
// Match each `deny[NAME] { ... }` block (or `deny { ... }`).
|
|
73
|
+
const blockRe = /\bdeny(?:\s*\[\s*(\w+)\s*\])?\s*\{([\s\S]*?)\}/g;
|
|
74
|
+
let m;
|
|
75
|
+
while ((m = blockRe.exec(policyText))) {
|
|
76
|
+
const body = m[2];
|
|
77
|
+
const conditions = [];
|
|
78
|
+
let msgExpr = `"policy violation"`;
|
|
79
|
+
for (const line of body.split(/[\n;]/)) {
|
|
80
|
+
const ln = line.trim();
|
|
81
|
+
if (!ln || ln.startsWith('#')) continue;
|
|
82
|
+
// Assignment: `<id> := <expr>`
|
|
83
|
+
const asn = ln.match(/^(\w+)\s*:=\s*(.+)$/);
|
|
84
|
+
if (asn && asn[1] !== 'msg') continue; // skip non-msg assignments
|
|
85
|
+
if (asn && asn[1] === 'msg') { msgExpr = asn[2].trim(); continue; }
|
|
86
|
+
// Comparison: `finding.<field> <op> <value>`
|
|
87
|
+
const cmp = ln.match(/^(finding|input)\.([a-zA-Z_][\w.]*)\s*(==|!=|<=|>=|<|>)\s*(.+)$/);
|
|
88
|
+
if (!cmp) continue;
|
|
89
|
+
const [, scope, field, op, valueRaw] = cmp;
|
|
90
|
+
let value = valueRaw.trim();
|
|
91
|
+
if (/^".*"$/.test(value)) value = JSON.stringify(value.slice(1, -1));
|
|
92
|
+
conditions.push({ scope, field, op, value });
|
|
93
|
+
}
|
|
94
|
+
blocks.push({ conditions, msgExpr });
|
|
95
|
+
}
|
|
96
|
+
return blocks;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function _evalBlock(block, finding) {
|
|
100
|
+
for (const c of block.conditions) {
|
|
101
|
+
const lhs = _resolvePath(finding, c.field);
|
|
102
|
+
let rhs;
|
|
103
|
+
try { rhs = JSON.parse(c.value); }
|
|
104
|
+
catch { rhs = c.value.replace(/^"|"$/g, ''); }
|
|
105
|
+
if (c.op === '==' && lhs !== rhs) return null;
|
|
106
|
+
if (c.op === '!=' && lhs === rhs) return null;
|
|
107
|
+
if (c.op === '>' && !(Number(lhs) > Number(rhs))) return null;
|
|
108
|
+
if (c.op === '<' && !(Number(lhs) < Number(rhs))) return null;
|
|
109
|
+
if (c.op === '>=' && !(Number(lhs) >= Number(rhs))) return null;
|
|
110
|
+
if (c.op === '<=' && !(Number(lhs) <= Number(rhs))) return null;
|
|
111
|
+
}
|
|
112
|
+
return _renderMsg(block.msgExpr, finding);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function _resolvePath(obj, dotPath) {
|
|
116
|
+
return dotPath.split('.').reduce((o, k) => (o == null ? undefined : o[k]), obj);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _renderMsg(expr, finding) {
|
|
120
|
+
// Strip outer quotes if string literal.
|
|
121
|
+
const m = expr.match(/^["'](.+)["']$/);
|
|
122
|
+
if (m) return m[1];
|
|
123
|
+
// sprintf shape: sprintf("...", [a, b])
|
|
124
|
+
const sf = expr.match(/^sprintf\s*\(\s*"([^"]+)"\s*,\s*\[(.+)\]\s*\)$/);
|
|
125
|
+
if (sf) {
|
|
126
|
+
const fmt = sf[1];
|
|
127
|
+
const args = sf[2].split(',').map(s => _resolvePath(finding, s.trim().replace(/^finding\./, '')));
|
|
128
|
+
let i = 0;
|
|
129
|
+
return fmt.replace(/%v/g, () => String(args[i++] ?? ''));
|
|
130
|
+
}
|
|
131
|
+
return expr;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function _runEmbedded(policyText, findings) {
|
|
135
|
+
const blocks = _parseEmbedded(policyText);
|
|
136
|
+
const denials = [];
|
|
137
|
+
for (const f of findings) {
|
|
138
|
+
for (const b of blocks) {
|
|
139
|
+
const msg = _evalBlock(b, f);
|
|
140
|
+
if (msg) denials.push(msg);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { ok: true, denials };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Evaluate a policy file against the findings list.
|
|
150
|
+
* Returns { ok, denials, runner } — denials is an array of human-readable
|
|
151
|
+
* strings (one per violation). When denials.length > 0, the gate fails.
|
|
152
|
+
*/
|
|
153
|
+
export function evaluatePolicy(policyPath, findings, opts = {}) {
|
|
154
|
+
if (!policyPath || !fs.existsSync(policyPath)) {
|
|
155
|
+
return { ok: false, reason: 'policy-file-missing' };
|
|
156
|
+
}
|
|
157
|
+
const policyText = fs.readFileSync(policyPath, 'utf8');
|
|
158
|
+
const useExternal = !opts.embeddedOnly && _haveOpa();
|
|
159
|
+
if (useExternal) {
|
|
160
|
+
// Write findings to a temp file the opa binary reads.
|
|
161
|
+
const tmp = `/tmp/as-policy-${Date.now()}.json`;
|
|
162
|
+
fs.writeFileSync(tmp, JSON.stringify({ findings }));
|
|
163
|
+
const pkgMatch = policyText.match(/^\s*package\s+([\w.]+)/m);
|
|
164
|
+
const pkg = pkgMatch ? pkgMatch[1] : 'main';
|
|
165
|
+
const r = _runOpa(policyPath, tmp, pkg);
|
|
166
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
167
|
+
if (r.ok) return { ...r, runner: 'opa' };
|
|
168
|
+
// Fall through to embedded on opa error.
|
|
169
|
+
}
|
|
170
|
+
const r = _runEmbedded(policyText, findings);
|
|
171
|
+
return { ...r, runner: 'embedded' };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const _internals = { _parseEmbedded, _evalBlock, _runEmbedded };
|