@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,118 @@
|
|
|
1
|
+
// FR-VER-10 — Ephemeral verification target (Docker driver).
|
|
2
|
+
//
|
|
3
|
+
// Docker-based ephemeral sandbox for running generated PoCs against a fresh
|
|
4
|
+
// copy of the customer's app. The customer supplies the image (or a
|
|
5
|
+
// docker-compose.yml); we spin it up, run the PoC, tear down. Network is
|
|
6
|
+
// disabled by default; mount is read-only.
|
|
7
|
+
//
|
|
8
|
+
// Public API:
|
|
9
|
+
// isAvailable() → boolean — `docker --version` succeeds
|
|
10
|
+
// startTarget({ image, env, port, scanRoot }) → { containerId, url, stop() }
|
|
11
|
+
// runPoCAgainst(transcript, target, pocCode) → { exitCode, stdout, stderr }
|
|
12
|
+
//
|
|
13
|
+
// Falls back gracefully to `infra-unavailable` when Docker is missing. This
|
|
14
|
+
// module does NOT make cloud API calls; it only wraps the local Docker CLI.
|
|
15
|
+
|
|
16
|
+
import { spawnSync, spawn } from 'node:child_process';
|
|
17
|
+
import * as crypto from 'node:crypto';
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import * as os from 'node:os';
|
|
21
|
+
|
|
22
|
+
const STARTUP_TIMEOUT_MS = 30_000;
|
|
23
|
+
const POC_TIMEOUT_MS = 15_000;
|
|
24
|
+
|
|
25
|
+
export function isAvailable() {
|
|
26
|
+
try {
|
|
27
|
+
const r = spawnSync('docker', ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
28
|
+
return r.status === 0;
|
|
29
|
+
} catch { return false; }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function startTarget(opts = {}) {
|
|
33
|
+
if (!isAvailable()) {
|
|
34
|
+
return { available: false, reason: 'docker-not-installed' };
|
|
35
|
+
}
|
|
36
|
+
const image = opts.image || '';
|
|
37
|
+
if (!image) return { available: false, reason: 'no-image-supplied' };
|
|
38
|
+
const containerName = `as-target-${crypto.randomBytes(4).toString('hex')}`;
|
|
39
|
+
const port = opts.port || 3000;
|
|
40
|
+
const args = [
|
|
41
|
+
'run', '--rm', '--detach',
|
|
42
|
+
'--name', containerName,
|
|
43
|
+
'--network', opts.network === 'host' ? 'host' : 'bridge',
|
|
44
|
+
'--cap-drop=ALL',
|
|
45
|
+
'--memory=512m',
|
|
46
|
+
'--cpus=0.5',
|
|
47
|
+
'--read-only',
|
|
48
|
+
'--tmpfs', '/tmp:size=64m',
|
|
49
|
+
'--publish', `127.0.0.1:0:${port}`,
|
|
50
|
+
];
|
|
51
|
+
for (const [k, v] of Object.entries(opts.env || {})) args.push('--env', `${k}=${v}`);
|
|
52
|
+
args.push(image);
|
|
53
|
+
if (opts.cmd) args.push(...(Array.isArray(opts.cmd) ? opts.cmd : [opts.cmd]));
|
|
54
|
+
|
|
55
|
+
const r = spawnSync('docker', args, { encoding: 'utf8', timeout: STARTUP_TIMEOUT_MS });
|
|
56
|
+
if (r.status !== 0) {
|
|
57
|
+
return { available: false, reason: `docker-run-failed:${(r.stderr || '').slice(0, 200)}` };
|
|
58
|
+
}
|
|
59
|
+
const containerId = (r.stdout || '').trim();
|
|
60
|
+
|
|
61
|
+
// Resolve the actually-assigned host port.
|
|
62
|
+
let url = null;
|
|
63
|
+
try {
|
|
64
|
+
const inspect = spawnSync('docker', [
|
|
65
|
+
'port', containerId, String(port),
|
|
66
|
+
], { encoding: 'utf8' });
|
|
67
|
+
const hostPortLine = (inspect.stdout || '').split('\n').find(l => l.startsWith('127.0.0.1:'));
|
|
68
|
+
if (hostPortLine) url = 'http://' + hostPortLine.trim();
|
|
69
|
+
} catch {}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
available: true,
|
|
73
|
+
containerId,
|
|
74
|
+
url,
|
|
75
|
+
stop() {
|
|
76
|
+
try { spawnSync('docker', ['kill', containerId], { timeout: 10_000 }); } catch {}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Run a PoC string in a one-shot Node container; return its exit code +
|
|
82
|
+
// captured stdout/stderr. Network is host-bridged so the PoC can reach the
|
|
83
|
+
// target URL the caller provides via TARGET_URL env.
|
|
84
|
+
export function runPoCAgainst(target, pocCode, opts = {}) {
|
|
85
|
+
if (!isAvailable()) {
|
|
86
|
+
return { available: false, reason: 'docker-not-installed' };
|
|
87
|
+
}
|
|
88
|
+
if (!target || !target.url) return { available: false, reason: 'no-target' };
|
|
89
|
+
const tmp = path.join(os.tmpdir(), `as-poc-${crypto.randomBytes(4).toString('hex')}.mjs`);
|
|
90
|
+
try { fs.writeFileSync(tmp, pocCode, 'utf8'); }
|
|
91
|
+
catch (e) { return { available: false, reason: `tmp-write-failed:${e.message}` }; }
|
|
92
|
+
try {
|
|
93
|
+
const args = [
|
|
94
|
+
'run', '--rm',
|
|
95
|
+
'--network', 'bridge',
|
|
96
|
+
'--cap-drop=ALL',
|
|
97
|
+
'--memory=128m',
|
|
98
|
+
'--cpus=0.25',
|
|
99
|
+
'--read-only',
|
|
100
|
+
'--tmpfs', '/tmp:size=16m',
|
|
101
|
+
'--volume', `${tmp}:/poc.mjs:ro`,
|
|
102
|
+
'--env', `TARGET_URL=${target.url}`,
|
|
103
|
+
'--workdir', '/tmp',
|
|
104
|
+
opts.runtimeImage || 'node:20-alpine',
|
|
105
|
+
'node', '/poc.mjs',
|
|
106
|
+
];
|
|
107
|
+
const r = spawnSync('docker', args, { encoding: 'utf8', timeout: POC_TIMEOUT_MS });
|
|
108
|
+
return {
|
|
109
|
+
available: true,
|
|
110
|
+
exitCode: r.status,
|
|
111
|
+
stdout: r.stdout || '',
|
|
112
|
+
stderr: r.stderr || '',
|
|
113
|
+
timedOut: r.signal === 'SIGTERM' || r.error?.code === 'ETIMEDOUT',
|
|
114
|
+
};
|
|
115
|
+
} finally {
|
|
116
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Verifier target harness (FR-LIVE-HARNESS).
|
|
2
|
+
//
|
|
3
|
+
// Customer projects describe how to bring up the app they want the verifier
|
|
4
|
+
// to execute PoCs against. The manifest lives at
|
|
5
|
+
// `.agentic-security/verifier-target.yaml` and the verifier reads it before
|
|
6
|
+
// running `--live` mode. v1 supports two manifest shapes:
|
|
7
|
+
//
|
|
8
|
+
// shape: docker-compose
|
|
9
|
+
// compose: docker-compose.yml
|
|
10
|
+
// service: web
|
|
11
|
+
// port: 3000
|
|
12
|
+
// wait-for: http://localhost:3000/healthz
|
|
13
|
+
//
|
|
14
|
+
// ─── or ───
|
|
15
|
+
//
|
|
16
|
+
// shape: command
|
|
17
|
+
// start: npm run dev
|
|
18
|
+
// port: 3000
|
|
19
|
+
// wait-for: http://localhost:3000/healthz
|
|
20
|
+
// stop: pkill -f "npm run dev"
|
|
21
|
+
//
|
|
22
|
+
// We DO NOT execute the manifest in this module — that's the verifier
|
|
23
|
+
// sandbox's job (and the customer's explicit opt-in via --live). This
|
|
24
|
+
// module parses, validates, and surfaces a structured object the verifier
|
|
25
|
+
// can act on (or refuse to act on).
|
|
26
|
+
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
|
|
30
|
+
const MANIFEST_PATH = path.join('.agentic-security', 'verifier-target.yaml');
|
|
31
|
+
|
|
32
|
+
// Minimal YAML subset parser — keep parity with the polyglot bench parser,
|
|
33
|
+
// scoped to the small set of keys this manifest uses. Standalone here to
|
|
34
|
+
// avoid a runtime dep.
|
|
35
|
+
function _parse(text) {
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const raw of String(text || '').split(/\r?\n/)) {
|
|
38
|
+
const line = raw.replace(/#.*$/, '').trim();
|
|
39
|
+
if (!line) continue;
|
|
40
|
+
const m = line.match(/^([\w-]+)\s*:\s*(.*)$/);
|
|
41
|
+
if (!m) continue;
|
|
42
|
+
const [, key, val] = m;
|
|
43
|
+
out[key] = _coerce(val);
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _coerce(v) {
|
|
49
|
+
if (v === undefined || v === null) return null;
|
|
50
|
+
v = String(v).trim();
|
|
51
|
+
if (/^".*"$/.test(v)) return v.slice(1, -1);
|
|
52
|
+
if (/^'.*'$/.test(v)) return v.slice(1, -1);
|
|
53
|
+
if (/^-?\d+$/.test(v)) return parseInt(v, 10);
|
|
54
|
+
return v;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read the verifier-target manifest from scanRoot. Returns:
|
|
59
|
+
* { ok: true, target } — successfully parsed and validated
|
|
60
|
+
* { ok: false, reason } — manifest missing or invalid
|
|
61
|
+
*
|
|
62
|
+
* Never throws.
|
|
63
|
+
*/
|
|
64
|
+
export function loadTargetManifest(scanRoot) {
|
|
65
|
+
const fp = path.join(scanRoot || process.cwd(), MANIFEST_PATH);
|
|
66
|
+
if (!fs.existsSync(fp)) return { ok: false, reason: 'no-manifest' };
|
|
67
|
+
let raw;
|
|
68
|
+
try { raw = fs.readFileSync(fp, 'utf8'); }
|
|
69
|
+
catch (e) { return { ok: false, reason: `read-error:${e.message}` }; }
|
|
70
|
+
const parsed = _parse(raw);
|
|
71
|
+
const shape = parsed.shape;
|
|
72
|
+
if (shape !== 'docker-compose' && shape !== 'command') {
|
|
73
|
+
return { ok: false, reason: `unknown-shape:${shape || 'missing'}` };
|
|
74
|
+
}
|
|
75
|
+
if (shape === 'docker-compose') {
|
|
76
|
+
if (!parsed.compose || !parsed.service) {
|
|
77
|
+
return { ok: false, reason: 'docker-compose-shape-needs-compose-and-service' };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (shape === 'command') {
|
|
81
|
+
if (!parsed.start) {
|
|
82
|
+
return { ok: false, reason: 'command-shape-needs-start' };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const target = {
|
|
86
|
+
shape,
|
|
87
|
+
compose: parsed.compose || null,
|
|
88
|
+
service: parsed.service || null,
|
|
89
|
+
start: parsed.start || null,
|
|
90
|
+
stop: parsed.stop || null,
|
|
91
|
+
port: parsed.port || null,
|
|
92
|
+
waitFor: parsed['wait-for'] || parsed.waitFor || null,
|
|
93
|
+
url: parsed.url || (parsed.port ? `http://localhost:${parsed.port}` : null),
|
|
94
|
+
};
|
|
95
|
+
return { ok: true, target };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Render a quick summary for human consumption (CLI output, logs).
|
|
100
|
+
*/
|
|
101
|
+
export function describeTarget(target) {
|
|
102
|
+
if (!target) return '(none)';
|
|
103
|
+
if (target.shape === 'docker-compose') {
|
|
104
|
+
return `docker-compose service ${target.service} from ${target.compose} on ${target.url || `:${target.port}`}`;
|
|
105
|
+
}
|
|
106
|
+
if (target.shape === 'command') {
|
|
107
|
+
return `command "${target.start}" on ${target.url || `:${target.port}`}`;
|
|
108
|
+
}
|
|
109
|
+
return `unknown-shape ${target.shape}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Pre-flight safety check: refuse to bring up a target whose start command
|
|
114
|
+
* looks dangerous. Curated allowlist of common shapes; everything else
|
|
115
|
+
* requires AGENTIC_SECURITY_VERIFY_TARGET_OK=1 to opt in explicitly.
|
|
116
|
+
*/
|
|
117
|
+
export function validateTarget(target) {
|
|
118
|
+
if (!target) return { ok: false, reason: 'no-target' };
|
|
119
|
+
if (target.shape === 'docker-compose') {
|
|
120
|
+
// docker-compose is safe-ish by design.
|
|
121
|
+
return { ok: true };
|
|
122
|
+
}
|
|
123
|
+
if (target.shape === 'command') {
|
|
124
|
+
const SAFE_HINTS = [
|
|
125
|
+
/^npm\s+(?:run\s+)?(?:dev|start|serve)/,
|
|
126
|
+
/^yarn\s+(?:dev|start|serve)/,
|
|
127
|
+
/^pnpm\s+(?:run\s+)?(?:dev|start|serve)/,
|
|
128
|
+
/^node\s+/,
|
|
129
|
+
/^python(?:\d?)\s+-m\s+/,
|
|
130
|
+
/^python(?:\d?)\s+\S+\.py/,
|
|
131
|
+
/^uvicorn\s+/,
|
|
132
|
+
/^gunicorn\s+/,
|
|
133
|
+
/^flask\s+run/,
|
|
134
|
+
/^go\s+run\s+/,
|
|
135
|
+
/^java\s+-jar\s+/,
|
|
136
|
+
/^cargo\s+run/,
|
|
137
|
+
];
|
|
138
|
+
if (process.env.AGENTIC_SECURITY_VERIFY_TARGET_OK === '1') return { ok: true, escaped: true };
|
|
139
|
+
if (!SAFE_HINTS.some(re => re.test(target.start))) {
|
|
140
|
+
return { ok: false, reason: `start-command-not-in-safe-allowlist (set AGENTIC_SECURITY_VERIFY_TARGET_OK=1 to override)` };
|
|
141
|
+
}
|
|
142
|
+
return { ok: true };
|
|
143
|
+
}
|
|
144
|
+
return { ok: false, reason: `unknown-shape:${target.shape}` };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export const _internals = { MANIFEST_PATH };
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// Verifier sandbox loop (FR-VER-3, FR-VER-6, FR-VER-7 — Phase-1 P1.2).
|
|
2
|
+
//
|
|
3
|
+
// Consumes the PoC artifacts produced by P1.1 (`f.poc`) and assigns a
|
|
4
|
+
// per-finding `verifier_verdict` in:
|
|
5
|
+
//
|
|
6
|
+
// verified-exploit — PoC ran against a live target and exited 0
|
|
7
|
+
// verified-by-llm — Layer-3 LLM accepted the finding (no PoC ran)
|
|
8
|
+
// verified-sanitizer-absence — pattern-based proof that no sanitizer is on the flow
|
|
9
|
+
// unverified-by-design — CWE family for which v1 explicitly doesn't ship a PoC
|
|
10
|
+
// cannot-verify — PoC failed to run / LLM returned escalate / sandbox error
|
|
11
|
+
//
|
|
12
|
+
// Honest scope for v1:
|
|
13
|
+
// * Default mode is "validate-only": parse the PoC, refuse to ship one that
|
|
14
|
+
// contains a destructive payload, but do NOT execute it. Findings get
|
|
15
|
+
// `verifier_verdict` set from the static signals.
|
|
16
|
+
// * Live execution mode (AGENTIC_SECURITY_VERIFY_LIVE=1) runs each PoC
|
|
17
|
+
// against a caller-provided target URL (AGENTIC_SECURITY_VERIFY_TARGET).
|
|
18
|
+
// Without a target, live mode falls back to validate-only with a
|
|
19
|
+
// `cannot-verify` verdict + reason 'no-target'.
|
|
20
|
+
// * Sandbox: Docker by default with restrictive flags; subprocess fallback
|
|
21
|
+
// with ulimit. The sandbox runner is exported so the CLI subcommand can
|
|
22
|
+
// reuse it.
|
|
23
|
+
//
|
|
24
|
+
// Fail-closed semantics (FR-VER-7): any error — Docker missing, target down,
|
|
25
|
+
// PoC throws — produces `cannot-verify`, never `rejected`. An attacker who
|
|
26
|
+
// can break the verifier can only make findings UNVERIFIED, never SILENCED.
|
|
27
|
+
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
import * as path from 'node:path';
|
|
30
|
+
import * as os from 'node:os';
|
|
31
|
+
import { spawnSync } from 'node:child_process';
|
|
32
|
+
import { isExplicitlyNoPoc } from './poc-cwe-map.js';
|
|
33
|
+
|
|
34
|
+
// ─── PoC static validation ──────────────────────────────────────────────────
|
|
35
|
+
//
|
|
36
|
+
// Refuse to ship a PoC that:
|
|
37
|
+
// - is too long (template runaway)
|
|
38
|
+
// - mentions a banned pattern (destructive shell, fork bomb)
|
|
39
|
+
// - hardcodes a real cloud-metadata IP
|
|
40
|
+
// - doesn't end with a `process.exit(...)` so verdict assignment is reliable
|
|
41
|
+
|
|
42
|
+
const MAX_POC_BYTES = 16_384;
|
|
43
|
+
|
|
44
|
+
const BANNED_PATTERNS = [
|
|
45
|
+
/rm\s+-rf\s+\//, // recursive destructive
|
|
46
|
+
/mkfs/,
|
|
47
|
+
/dd\s+if=\/dev\/(?:zero|random|urandom)/,
|
|
48
|
+
/:\(\)\s*\{\s*:\s*\|\s*:/, // fork bomb
|
|
49
|
+
/shutdown\b/,
|
|
50
|
+
/reboot\b/,
|
|
51
|
+
/chmod\s+777\s+\//,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const BANNED_HOSTS = [
|
|
55
|
+
'169.254.169.254',
|
|
56
|
+
'metadata.google.internal',
|
|
57
|
+
'fd00:ec2::254',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
export function validatePoc(poc) {
|
|
61
|
+
if (!poc || typeof poc !== 'object') return { ok: false, reason: 'no-poc' };
|
|
62
|
+
if (typeof poc.code !== 'string' || poc.code.length === 0) return { ok: false, reason: 'empty-code' };
|
|
63
|
+
if (poc.code.length > MAX_POC_BYTES) return { ok: false, reason: 'code-too-long' };
|
|
64
|
+
for (const re of BANNED_PATTERNS) {
|
|
65
|
+
if (re.test(poc.code)) return { ok: false, reason: `banned-pattern:${re.source.slice(0, 30)}` };
|
|
66
|
+
}
|
|
67
|
+
for (const h of BANNED_HOSTS) {
|
|
68
|
+
if (poc.code.includes(h)) return { ok: false, reason: `banned-host:${h}` };
|
|
69
|
+
}
|
|
70
|
+
if (!/process\.exit\s*\(/.test(poc.code) && poc.lang === 'node') {
|
|
71
|
+
return { ok: false, reason: 'no-deterministic-exit' };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Sanitizer-absence proof ────────────────────────────────────────────────
|
|
77
|
+
//
|
|
78
|
+
// For a flow-based finding with a clear source → sink path, we can prove a
|
|
79
|
+
// SANITIZER IS ABSENT by checking that none of the family's known sanitizers
|
|
80
|
+
// appears in the surrounding code window.
|
|
81
|
+
|
|
82
|
+
const SANITIZER_TABLE = {
|
|
83
|
+
'sql-injection': /\bprepare(?:Statement)?\s*\(|parameterized|\$\d+\b(?![=A-Za-z])|sequelize\.literal\b|\bescape\s*\(|(?:\bquery|\bexecute|\b\$queryRaw|\b\$executeRaw)\s*\([^)]*,\s*\[/i,
|
|
84
|
+
'command-injection': /execFile\s*\(|spawn\s*\(\s*['"][^'"]+['"]\s*,\s*\[|shlex\.quote/i,
|
|
85
|
+
'xss': /escapeHtml|sanitize-html|DOMPurify|encodeURIComponent\(|textContent\s*=|res\.json\(/i,
|
|
86
|
+
'path-traversal': /path\.resolve\s*\(|path\.basename\s*\(|\.startsWith\s*\(\s*\w+\s*\)/i,
|
|
87
|
+
'ssrf': /isPrivateIP|new\s+URL\s*\(|allowlist|allowedHosts|trustedHosts/i,
|
|
88
|
+
'code-injection': /(?!eval).*?\beval\s*\(.*JSON/i, // very weak; intentionally narrow
|
|
89
|
+
'open-redirect': /allowed(?:Redirects|Urls|Hosts)|\.includes\s*\(\s*\w+\s*\)\s*\?/i,
|
|
90
|
+
'xxe': /\bnoent\s*[:=]\s*false|resolve_entities\s*=\s*False|XMLInputFactory.*IS_SUPPORTING_EXTERNAL_ENTITIES.*false/i,
|
|
91
|
+
'insecure-deserialization':/JSON\.parse|yaml\.safe_load|safe_load_all/i,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function _windowAroundLine(file, line, fileContents) {
|
|
95
|
+
if (!file || !line || !fileContents || !fileContents[file]) return '';
|
|
96
|
+
const lines = fileContents[file].split('\n');
|
|
97
|
+
const start = Math.max(0, line - 11);
|
|
98
|
+
const end = Math.min(lines.length, line + 10);
|
|
99
|
+
return lines.slice(start, end).join('\n');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function proveSanitizerAbsence(finding, fileContents) {
|
|
103
|
+
const fam = finding.family;
|
|
104
|
+
if (!fam) return { ok: false, reason: 'no-family' };
|
|
105
|
+
const rx = SANITIZER_TABLE[fam];
|
|
106
|
+
if (!rx) return { ok: false, reason: 'no-rule' };
|
|
107
|
+
const file = finding.file || finding.sink?.file;
|
|
108
|
+
const line = finding.line || finding.sink?.line || 0;
|
|
109
|
+
const window = _windowAroundLine(file, line, fileContents);
|
|
110
|
+
if (!window) return { ok: false, reason: 'no-source-window' };
|
|
111
|
+
if (rx.test(window)) return { ok: false, reason: 'sanitizer-present' };
|
|
112
|
+
return { ok: true, reason: `no-sanitizer-in-window`, window: window.length };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Sandbox execution ──────────────────────────────────────────────────────
|
|
116
|
+
//
|
|
117
|
+
// Runs the PoC and returns { ok, exitCode, stderr, runner }.
|
|
118
|
+
// Caller decides what to do with the result. Internal — surfaced via
|
|
119
|
+
// `_internals.runSandboxed` for tests; the public contract is
|
|
120
|
+
// `annotateVerifierVerdicts`.
|
|
121
|
+
|
|
122
|
+
function runSandboxed(poc, opts = {}) {
|
|
123
|
+
const target = opts.target;
|
|
124
|
+
if (!target) return { ok: false, reason: 'no-target' };
|
|
125
|
+
// Materialise the PoC to a temp file.
|
|
126
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'as-poc-'));
|
|
127
|
+
const file = path.join(dir, poc.lang === 'python' ? 'poc.py' : 'poc.mjs');
|
|
128
|
+
try {
|
|
129
|
+
fs.writeFileSync(file, _patchTarget(poc.code, target));
|
|
130
|
+
} catch (e) {
|
|
131
|
+
return { ok: false, reason: `write-failed:${e.message}` };
|
|
132
|
+
}
|
|
133
|
+
const docker = _haveDocker() ? _runDocker(file, dir, poc.lang, opts) : null;
|
|
134
|
+
const result = docker || _runSubprocess(file, poc.lang, opts);
|
|
135
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _patchTarget(code, target) {
|
|
140
|
+
// Replace the localhost:3000 placeholder with the caller-provided target.
|
|
141
|
+
return code.replace(/http:\/\/localhost:3000/g, target);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _haveDocker() {
|
|
145
|
+
try {
|
|
146
|
+
const r = spawnSync('docker', ['version'], { stdio: 'ignore', timeout: 3000 });
|
|
147
|
+
return r.status === 0;
|
|
148
|
+
} catch { return false; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function _runDocker(file, dir, lang, opts) {
|
|
152
|
+
const image = lang === 'python' ? 'python:3.12-slim' : 'node:22-slim';
|
|
153
|
+
const cmd = lang === 'python' ? ['python3', '/work/poc.py'] : ['node', '/work/poc.mjs'];
|
|
154
|
+
const args = [
|
|
155
|
+
'run', '--rm',
|
|
156
|
+
'--network=host', // PoC must reach the target; host is the smallest blast radius
|
|
157
|
+
'--cap-drop=ALL',
|
|
158
|
+
'--memory=256m',
|
|
159
|
+
'--cpu-quota=20000',
|
|
160
|
+
'--pids-limit=64',
|
|
161
|
+
'--read-only',
|
|
162
|
+
'--tmpfs=/tmp',
|
|
163
|
+
'--user', 'nobody',
|
|
164
|
+
'-v', `${dir}:/work:ro`,
|
|
165
|
+
image,
|
|
166
|
+
...cmd,
|
|
167
|
+
];
|
|
168
|
+
const r = spawnSync('docker', args, {
|
|
169
|
+
timeout: opts.timeoutMs || 15000,
|
|
170
|
+
encoding: 'utf8',
|
|
171
|
+
});
|
|
172
|
+
if (r.error) return { ok: false, reason: `docker-error:${r.error.code || r.error.message}`, runner: 'docker' };
|
|
173
|
+
return { ok: true, exitCode: r.status, stderr: r.stderr || '', stdout: r.stdout || '', runner: 'docker' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function _runSubprocess(file, lang, opts) {
|
|
177
|
+
const bin = lang === 'python' ? 'python3' : 'node';
|
|
178
|
+
const r = spawnSync(bin, [file], {
|
|
179
|
+
timeout: opts.timeoutMs || 15000,
|
|
180
|
+
encoding: 'utf8',
|
|
181
|
+
// Best-effort containment without Docker. Operators are warned in stderr
|
|
182
|
+
// that the subprocess fallback offers materially weaker isolation.
|
|
183
|
+
env: { PATH: process.env.PATH || '', NODE_OPTIONS: '' },
|
|
184
|
+
});
|
|
185
|
+
if (r.error) return { ok: false, reason: `subprocess-error:${r.error.code || r.error.message}`, runner: 'subprocess' };
|
|
186
|
+
return { ok: true, exitCode: r.status, stderr: r.stderr || '', stdout: r.stdout || '', runner: 'subprocess' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─── Per-finding verdict assignment ─────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
export function verdictForFinding(finding, ctx = {}) {
|
|
192
|
+
// 1. Families we explicitly do not ship a PoC for.
|
|
193
|
+
if (isExplicitlyNoPoc(finding.family)) {
|
|
194
|
+
return { verdict: 'unverified-by-design', reason: `family-no-poc:${finding.family}` };
|
|
195
|
+
}
|
|
196
|
+
// 2. Validator already passed it via the LLM.
|
|
197
|
+
if (finding.validator_verdict === 'accept') {
|
|
198
|
+
return { verdict: 'verified-by-llm', reason: 'llm-accept' };
|
|
199
|
+
}
|
|
200
|
+
// 3. PoC present and live mode is on — run it.
|
|
201
|
+
const liveMode = process.env.AGENTIC_SECURITY_VERIFY_LIVE === '1';
|
|
202
|
+
const target = ctx.target || process.env.AGENTIC_SECURITY_VERIFY_TARGET || null;
|
|
203
|
+
if (finding.poc && liveMode && target) {
|
|
204
|
+
const v = validatePoc(finding.poc);
|
|
205
|
+
if (!v.ok) return { verdict: 'cannot-verify', reason: `poc-rejected:${v.reason}` };
|
|
206
|
+
const r = runSandboxed(finding.poc, { target, timeoutMs: ctx.timeoutMs });
|
|
207
|
+
if (!r.ok) return { verdict: 'cannot-verify', reason: r.reason || 'sandbox-error', runner: r.runner };
|
|
208
|
+
if (r.exitCode === 0) return { verdict: 'verified-exploit', reason: 'poc-exit-0', runner: r.runner };
|
|
209
|
+
return { verdict: 'cannot-verify', reason: `poc-exit:${r.exitCode}`, runner: r.runner, stderr: (r.stderr || '').slice(0, 240) };
|
|
210
|
+
}
|
|
211
|
+
// 4. PoC present but we're not running it — static validate only.
|
|
212
|
+
if (finding.poc) {
|
|
213
|
+
const v = validatePoc(finding.poc);
|
|
214
|
+
if (!v.ok) return { verdict: 'cannot-verify', reason: `poc-validation-failed:${v.reason}` };
|
|
215
|
+
// Static validation says the PoC is shippable; absent live execution we
|
|
216
|
+
// can't claim verified-exploit. Try the sanitizer-absence proof next.
|
|
217
|
+
}
|
|
218
|
+
// 5. Sanitizer-absence proof.
|
|
219
|
+
if (ctx.fileContents) {
|
|
220
|
+
const sa = proveSanitizerAbsence(finding, ctx.fileContents);
|
|
221
|
+
if (sa.ok) return { verdict: 'verified-sanitizer-absence', reason: sa.reason };
|
|
222
|
+
}
|
|
223
|
+
return { verdict: 'cannot-verify', reason: 'no-poc-no-sanitizer-rule' };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ─── Batch annotation ───────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
export function annotateVerifierVerdicts(findings, opts = {}) {
|
|
229
|
+
if (!Array.isArray(findings)) return;
|
|
230
|
+
for (const f of findings) {
|
|
231
|
+
if (!f || typeof f !== 'object') continue;
|
|
232
|
+
try {
|
|
233
|
+
const v = verdictForFinding(f, opts);
|
|
234
|
+
f.verifier_verdict = v.verdict;
|
|
235
|
+
f.verifier_reason = v.reason || null;
|
|
236
|
+
if (v.runner) f.verifier_runner = v.runner;
|
|
237
|
+
} catch (e) {
|
|
238
|
+
// Defense in depth: any exception → cannot-verify, never throws upward.
|
|
239
|
+
f.verifier_verdict = 'cannot-verify';
|
|
240
|
+
f.verifier_reason = `verifier-exception:${e.message?.slice(0, 80)}`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ─── Summary helpers ────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
export function verifierCoverageSummary(findings) {
|
|
248
|
+
const out = { 'verified-exploit': 0, 'verified-by-llm': 0, 'verified-sanitizer-absence': 0, 'unverified-by-design': 0, 'cannot-verify': 0 };
|
|
249
|
+
for (const f of findings || []) {
|
|
250
|
+
const v = f?.verifier_verdict;
|
|
251
|
+
if (v && v in out) out[v]++;
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// For tests and the no-dead-modules check.
|
|
257
|
+
export const _internals = { MAX_POC_BYTES, BANNED_HOSTS, SANITIZER_TABLE, runSandboxed };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Shared version source — read from scanner/package.json at module load.
|
|
2
|
+
//
|
|
3
|
+
// Premortems 3R1.3 / 3R2.1: previously CURRENT_RULESET_VERSION and
|
|
4
|
+
// SARIF tool.driver.version were independently hardcoded constants that
|
|
5
|
+
// diverged from the actual scanner version on every release. This module
|
|
6
|
+
// reads the truth from package.json so the version is single-sourced.
|
|
7
|
+
//
|
|
8
|
+
// The bundled scanner is built via ncc into dist/agentic-security.mjs; ncc
|
|
9
|
+
// inlines the package.json import at build time, so this module returns the
|
|
10
|
+
// frozen version that was bundled with the build.
|
|
11
|
+
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
// Premortem 4R-7: silently falling back to 'unknown' meant downstream
|
|
18
|
+
// consumers (CI gate, telemetry, ruleset-version stamp) couldn't tell whether
|
|
19
|
+
// they were looking at a real version or a packaging failure. We now read
|
|
20
|
+
// the version eagerly and surface a clear stderr error when package.json
|
|
21
|
+
// isn't readable, instead of poisoning every report with 'unknown'. Tests can
|
|
22
|
+
// opt out of the eager assertion with AGENTIC_SECURITY_VERSION_UNCHECKED=1.
|
|
23
|
+
//
|
|
24
|
+
// Bundled mode (P1.3 follow-up): under ncc, `import.meta.url` resolves to
|
|
25
|
+
// `dist/agentic-security.mjs`, so `../../package.json` (relative to src/posture/)
|
|
26
|
+
// no longer maps to scanner/package.json. We probe a small set of likely
|
|
27
|
+
// paths so the bundle and the source tree both work.
|
|
28
|
+
function _readVersion() {
|
|
29
|
+
const candidates = [];
|
|
30
|
+
// 1. require('../../package.json') works in the source tree.
|
|
31
|
+
candidates.push(() => {
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
return require('../../package.json');
|
|
34
|
+
});
|
|
35
|
+
// 2. Walk upward from the current file looking for any package.json that
|
|
36
|
+
// carries `@clear-capabilities/agentic-security-scanner`. Survives ncc.
|
|
37
|
+
candidates.push(() => {
|
|
38
|
+
let dir;
|
|
39
|
+
try { dir = path.dirname(fileURLToPath(import.meta.url)); }
|
|
40
|
+
catch { return null; }
|
|
41
|
+
for (let i = 0; i < 6; i++) {
|
|
42
|
+
const fp = path.join(dir, 'package.json');
|
|
43
|
+
if (fs.existsSync(fp)) {
|
|
44
|
+
const pkg = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
45
|
+
if (pkg && pkg.name && pkg.name.endsWith('agentic-security-scanner')) return pkg;
|
|
46
|
+
}
|
|
47
|
+
const parent = path.dirname(dir);
|
|
48
|
+
if (parent === dir) break;
|
|
49
|
+
dir = parent;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
});
|
|
53
|
+
for (const fn of candidates) {
|
|
54
|
+
try {
|
|
55
|
+
const pkg = fn();
|
|
56
|
+
if (pkg && typeof pkg.version === 'string' && pkg.version) return pkg.version;
|
|
57
|
+
} catch { /* try next */ }
|
|
58
|
+
}
|
|
59
|
+
throw new Error('scanner/package.json not found in any expected location');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let _version;
|
|
63
|
+
try {
|
|
64
|
+
_version = _readVersion();
|
|
65
|
+
} catch (e) {
|
|
66
|
+
_version = 'unknown';
|
|
67
|
+
if (process.env.AGENTIC_SECURITY_VERSION_UNCHECKED !== '1') {
|
|
68
|
+
process.stderr.write(
|
|
69
|
+
`agentic-security: WARNING — could not resolve scanner version (${e.message}). ` +
|
|
70
|
+
`Reports will carry version='unknown'. Set AGENTIC_SECURITY_VERSION_UNCHECKED=1 to silence.\n`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const SCANNER_VERSION = _version;
|