@clear-capabilities/agentic-security-scanner 0.133.0 → 0.136.2
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 +299 -0
- package/bin/agentic-security-audit.js +2 -1
- package/bin/agentic-security-consistency.js +2 -1
- package/bin/agentic-security.js +348 -40
- package/dist/113.index.js +6 -6
- package/dist/117.index.js +3 -1
- package/dist/178.index.js +1 -1
- package/dist/220.index.js +5 -3
- package/dist/238.index.js +4 -4
- package/dist/317.index.js +270 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +13 -7
- package/dist/499.index.js +86 -0
- package/dist/513.index.js +7 -3
- package/dist/526.index.js +6 -6
- package/dist/609.index.js +741 -0
- package/dist/637.index.js +1 -1
- package/dist/675.index.js +7 -5
- package/dist/839.index.js +4 -3
- package/dist/905.index.js +1173 -0
- package/dist/agentic-security.mjs +56 -56
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/ccpa.json +32 -0
- package/dist/compliance-frameworks/eu-ai-act.json +51 -0
- package/dist/compliance-frameworks/gdpr.json +45 -0
- package/dist/compliance-frameworks/hipaa-security-rule.json +56 -0
- package/dist/compliance-frameworks/nist-ai-600-1.json +51 -0
- package/dist/compliance-frameworks/nist-csf-2.json +73 -0
- package/dist/compliance-frameworks/nist-privacy-1-1.json +846 -0
- package/dist/compliance-frameworks/owasp-asvs-5.json +79 -0
- package/dist/compliance-frameworks/owasp-llm-top-10.json +69 -0
- package/package.json +14 -6
- package/src/badge.js +2 -1
- package/src/dataflow/cross-service-taint.js +2 -1
- package/src/dataflow/ifds-precise.js +6 -4
- package/src/dataflow/incremental.js +7 -5
- package/src/discovery/CLAUDE.md +48 -0
- package/src/discovery/confirm.js +47 -0
- package/src/discovery/disprove.js +79 -0
- package/src/discovery/hunter.js +116 -0
- package/src/discovery/index.js +331 -0
- package/src/discovery/judge.js +97 -0
- package/src/discovery/lenses.js +69 -0
- package/src/discovery/llm-invoke.js +120 -0
- package/src/discovery/memory.js +163 -0
- package/src/discovery/partition.js +92 -0
- package/src/engine.js +163 -7
- package/src/integrations/tickets.js +7 -6
- package/src/ir/ir-stats.js +1 -1
- package/src/ir/parser-cpp.js +1 -1
- package/src/leaderboard.js +3 -2
- package/src/llm-validator/consistency.js +6 -2
- package/src/llm-validator/index.js +30 -41
- package/src/llm-validator/providers.js +227 -0
- package/src/mcp/audit.js +2 -1
- package/src/mcp/tools.js +4 -3
- package/src/posture/CLAUDE.md +102 -1
- package/src/posture/accuracy-scorecard.js +59 -1
- package/src/posture/agents-memory.js +5 -3
- package/src/posture/auditor-walkthrough.js +9 -7
- package/src/posture/auth-posture-import.js +5 -4
- package/src/posture/autopilot.js +225 -0
- package/src/posture/comparison.js +181 -0
- package/src/posture/compliance-frameworks/nist-privacy-1-1.json +846 -0
- package/src/posture/compliance-policy.js +7 -9
- package/src/posture/custom-rules.js +7 -5
- package/src/posture/cve-alert-daemon.js +6 -5
- package/src/posture/dep-add-guard.js +2 -1
- package/src/posture/deterministic.js +3 -2
- package/src/posture/evidence-bundle.js +246 -0
- package/src/posture/execution-proof.js +25 -1
- package/src/posture/exploitability-probability.js +2 -1
- package/src/posture/feature-flags.js +3 -2
- package/src/posture/findings-memory.js +3 -3
- package/src/posture/fix-history.js +5 -2
- package/src/posture/fix-metrics.js +5 -5
- package/src/posture/fix-plan.js +2 -1
- package/src/posture/fleet.js +0 -0
- package/src/posture/grader-calibration.js +3 -4
- package/src/posture/intent-context.js +2 -1
- package/src/posture/learning.js +4 -3
- package/src/posture/license-attributions.js +5 -7
- package/src/posture/license-graph.js +2 -1
- package/src/posture/license-policy.js +2 -1
- package/src/posture/logic-claims.js +266 -0
- package/src/posture/model-rescan.js +4 -3
- package/src/posture/network-policy-import.js +3 -2
- package/src/posture/poc-inprocess.js +404 -2
- package/src/posture/pqc-migration-plan.js +7 -5
- package/src/posture/pr-augment.js +8 -5
- package/src/posture/privacy-framework.js +262 -0
- package/src/posture/proof-artifact.js +101 -0
- package/src/posture/prove-findings.js +28 -4
- package/src/posture/risk-dollars.js +2 -2
- package/src/posture/router.js +5 -4
- package/src/posture/ruleset-version.js +2 -2
- package/src/posture/runtime-correlation.js +2 -1
- package/src/posture/sbom-diff.js +12 -3
- package/src/posture/sca-policy.js +7 -4
- package/src/posture/scan-checkpoint.js +15 -0
- package/src/posture/state-dir.js +34 -0
- package/src/posture/telemetry-ingest.js +4 -3
- package/src/posture/threat-model-auto.js +4 -1
- package/src/posture/threat-model-grounding.js +11 -1
- package/src/posture/time-to-fix.js +3 -2
- package/src/posture/triage-memory.js +3 -2
- package/src/posture/waf-ingest.js +6 -5
- package/src/posture/watch-mode.js +4 -3
- package/src/report/index.js +9 -0
- package/src/sast/code-injection-multilang.js +29 -0
- package/src/sca/dep-confusion.js +2 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "owasp-asvs-5",
|
|
3
|
+
"name": "OWASP Application Security Verification Standard 5.0",
|
|
4
|
+
"publisher": "OWASP Foundation",
|
|
5
|
+
"license": "Creative Commons Attribution-ShareAlike 4.0",
|
|
6
|
+
"url": "https://owasp.org/www-project-application-security-verification-standard/",
|
|
7
|
+
"controls": [
|
|
8
|
+
{
|
|
9
|
+
"id": "V2.1",
|
|
10
|
+
"category": "Authentication",
|
|
11
|
+
"summary": "Verify that authentication is performed for protected functions.",
|
|
12
|
+
"evidence": ["Zero open critical findings in family auth-missing on the current scan."],
|
|
13
|
+
"mapsTo": ["family:auth-missing"]
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"id": "V2.7",
|
|
17
|
+
"category": "Authentication",
|
|
18
|
+
"summary": "Verify that MFA / secondary authentication is used for high-value operations.",
|
|
19
|
+
"evidence": ["aws-no-mfa-condition zero findings (if AWS in scope)."],
|
|
20
|
+
"mapsTo": ["family:aws-no-mfa"]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "V4.1",
|
|
24
|
+
"category": "Access Control",
|
|
25
|
+
"summary": "Verify that authorization is enforced for every resource access.",
|
|
26
|
+
"evidence": ["Zero idor / authz findings."],
|
|
27
|
+
"mapsTo": ["family:idor", "family:authz"]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "V5.1",
|
|
31
|
+
"category": "Input Validation",
|
|
32
|
+
"summary": "Verify input validation for type, length, and content.",
|
|
33
|
+
"evidence": ["Zero open critical findings in sqli/xss/command-injection/ldap-injection/xpath-injection."],
|
|
34
|
+
"mapsTo": ["family:sqli", "family:xss", "family:command-injection", "family:ldap-injection", "family:xpath-injection", "family:nosql-injection"]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "V6.1",
|
|
38
|
+
"category": "Cryptography",
|
|
39
|
+
"summary": "Verify use of strong cryptography per a documented policy.",
|
|
40
|
+
"evidence": ["Zero open findings in crypto-weak-cipher / crypto-weak-hash / crypto-ecb / crypto-static-iv."],
|
|
41
|
+
"mapsTo": ["family:crypto-weak-cipher", "family:crypto-weak-hash", "family:crypto-ecb", "family:crypto-static-iv", "family:crypto-kdf-weak"]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "V6.2",
|
|
45
|
+
"category": "Cryptography",
|
|
46
|
+
"summary": "Verify all transit data uses TLS ≥ 1.2.",
|
|
47
|
+
"evidence": ["Zero findings in crypto-tls-version / crypto-tls-no-verify."],
|
|
48
|
+
"mapsTo": ["family:crypto-tls-version", "family:crypto-tls-no-verify"]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "V7.1",
|
|
52
|
+
"category": "Error Handling & Logging",
|
|
53
|
+
"summary": "Verify that error handling does not disclose sensitive information.",
|
|
54
|
+
"evidence": ["No findings in family data-exposure."],
|
|
55
|
+
"mapsTo": ["family:data-exposure"]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"id": "V8.1",
|
|
59
|
+
"category": "Data Protection",
|
|
60
|
+
"summary": "Verify that personal data is protected at rest and in transit.",
|
|
61
|
+
"evidence": ["DPIA artifact at .agentic-security/dpia.md.", "Zero pii-exposure findings."],
|
|
62
|
+
"mapsTo": ["family:pii-exposure", "family:training-data-pii"]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"id": "V10.1",
|
|
66
|
+
"category": "Malicious Code",
|
|
67
|
+
"summary": "Verify the application does not include known-malicious or compromised dependencies.",
|
|
68
|
+
"evidence": [".agentic-security/sbom-history snapshots clean.", "No dependency-confusion or dependency-drift findings."],
|
|
69
|
+
"mapsTo": ["family:vulnerable-dependency", "family:dependency-confusion", "family:dependency-drift"]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "V14.1",
|
|
73
|
+
"category": "Configuration",
|
|
74
|
+
"summary": "Verify that build, deployment, and configuration are secure.",
|
|
75
|
+
"evidence": ["No findings in family iam-overpermissive / k8s-rbac-cluster-admin / k8s-pod-security-privileged."],
|
|
76
|
+
"mapsTo": ["family:iam-overpermissive", "family:k8s-rbac-cluster-admin", "family:k8s-pod-security-privileged"]
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "owasp-llm-top-10",
|
|
3
|
+
"name": "OWASP Top 10 for LLM Applications 2025",
|
|
4
|
+
"publisher": "OWASP Foundation",
|
|
5
|
+
"license": "Creative Commons Attribution-ShareAlike 4.0",
|
|
6
|
+
"url": "https://owasp.org/www-project-top-10-for-large-language-model-applications/",
|
|
7
|
+
"controls": [
|
|
8
|
+
{
|
|
9
|
+
"id": "LLM01",
|
|
10
|
+
"summary": "Prompt Injection — controls preventing injected instructions from overriding the system prompt.",
|
|
11
|
+
"evidence": ["Zero open findings in family prompt-injection / llm-app-security.", "Prompt-template integrity check passing."],
|
|
12
|
+
"mapsTo": ["family:prompt-injection", "family:llm-app-security", "family:prompt-integrity"]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"id": "LLM02",
|
|
16
|
+
"summary": "Insecure Output Handling — LLM output is not directly piped to dangerous sinks (eval/shell/SQL/HTML).",
|
|
17
|
+
"evidence": ["Zero open findings in subfamily llm-output-untrusted-sink."],
|
|
18
|
+
"mapsTo": ["family:llm-app-security:llm-output-untrusted-sink"]
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "LLM03",
|
|
22
|
+
"summary": "Training Data Poisoning — provenance and integrity controls on training/fine-tuning data.",
|
|
23
|
+
"evidence": ["No streaming-dataset-url findings.", "datasets.load_dataset uses pinned revisions."],
|
|
24
|
+
"mapsTo": ["family:streaming-dataset-url", "family:hf-datasets-rce"]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"id": "LLM04",
|
|
28
|
+
"summary": "Model Denial of Service — rate-limit / token-cap controls.",
|
|
29
|
+
"evidence": ["LLM API calls in source carry an explicit max_tokens cap (bodyguard rule no-max-tokens)."],
|
|
30
|
+
"mapsTo": ["rule:no-max-tokens"]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "LLM05",
|
|
34
|
+
"summary": "Supply Chain — model & dataset supply-chain integrity.",
|
|
35
|
+
"evidence": ["No mlflow-untrusted-uri / model-format / hf-endpoint-override findings.", "Sigstore provenance verification configured if opt-in."],
|
|
36
|
+
"mapsTo": ["family:mlflow-untrusted-uri", "family:model-format", "family:hf-endpoint-override"]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"id": "LLM06",
|
|
40
|
+
"summary": "Sensitive Information Disclosure — output filters prevent leakage.",
|
|
41
|
+
"evidence": ["Zero findings in subfamily llm-credential-in-prompt.", "Redact module covers prompt + response."],
|
|
42
|
+
"mapsTo": ["family:llm-app-security:llm-credential-in-prompt"]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "LLM07",
|
|
46
|
+
"summary": "Insecure Plugin / Tool Design — tools the LLM can invoke are narrowly scoped.",
|
|
47
|
+
"evidence": ["Zero llm-app-security:llm-tool-exec findings.", "Agent tool definitions reviewed for excessive agency."],
|
|
48
|
+
"mapsTo": ["family:agent-tool-exec", "family:llm-app-security:llm-tool-exec"]
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"id": "LLM08",
|
|
52
|
+
"summary": "Excessive Agency — agent autonomy is bounded by explicit guards.",
|
|
53
|
+
"evidence": ["MCP server config limits write-tools to confirm:true + reserved-write-path refusal.", "Apply-fix path requires HMAC-verified scan."],
|
|
54
|
+
"mapsTo": ["module:mcp-tools", "module:apply-fix"]
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "LLM09",
|
|
58
|
+
"summary": "Overreliance — human review checkpoints exist.",
|
|
59
|
+
"evidence": ["security-fixer agent has Stop-on-verify-fail loop.", "PreToolUse bodyguard exists."],
|
|
60
|
+
"mapsTo": ["module:security-fixer", "module:pre-edit-bodyguard"]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "LLM10",
|
|
64
|
+
"summary": "Model Theft — model access controls.",
|
|
65
|
+
"evidence": ["No private-key-in-frontend or rpc-key-inline findings in client code."],
|
|
66
|
+
"mapsTo": ["family:private-key-in-frontend", "family:rpc-key-inline"]
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.136.2",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin — SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, SARIF ingest, pipeline integrity, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -55,20 +55,21 @@
|
|
|
55
55
|
"lodash-es": "^4.18.1"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
|
-
"build": "ncc build bin/agentic-security.js -o dist --minify -e web-tree-sitter -e tree-sitter-wasms && mv dist/index.js dist/agentic-security.mjs && rm -f dist/package.json && chmod +x dist/agentic-security.mjs && node -e \"const fs=require('fs');const p='dist/agentic-security.mjs';const c=fs.readFileSync(p,'utf8');if(!c.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+c);\" && node -e \"const fs=require('fs');const c=require('crypto');const h=c.createHash('sha256').update(fs.readFileSync('dist/agentic-security.mjs')).digest('hex');fs.writeFileSync('dist/agentic-security.mjs.sha256',h+' agentic-security.mjs\\n');\"",
|
|
58
|
+
"build": "ncc build bin/agentic-security.js -o dist --minify -e web-tree-sitter -e tree-sitter-wasms && rm -rf dist/compliance-frameworks && mkdir -p dist/compliance-frameworks && cp src/posture/compliance-frameworks/*.json dist/compliance-frameworks/ && mv dist/index.js dist/agentic-security.mjs && rm -f dist/package.json && chmod +x dist/agentic-security.mjs && node -e \"const fs=require('fs');const p='dist/agentic-security.mjs';const c=fs.readFileSync(p,'utf8');if(!c.startsWith('#!'))fs.writeFileSync(p,'#!/usr/bin/env node\\n'+c);\" && node -e \"const fs=require('fs');const c=require('crypto');const h=c.createHash('sha256').update(fs.readFileSync('dist/agentic-security.mjs')).digest('hex');fs.writeFileSync('dist/agentic-security.mjs.sha256',h+' agentic-security.mjs\\n');\"",
|
|
59
59
|
"prepare": "node ../scripts/pre-push-gate.mjs --install-hook",
|
|
60
60
|
"prepublishOnly": "npm run build && node ../scripts/sync-scanner-changelog.mjs && node ../scripts/release-check.mjs",
|
|
61
|
-
"test": "npm run test:smoke && npm run test:glob && npm run test:sast && npm run test:posture && npm run test:dataflow && npm run test:mcp && npm run test:report && npm run test:bench-modules && npm run test:lifecycle && npm run test:eval && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js",
|
|
61
|
+
"test": "npm run test:smoke && npm run test:glob && npm run test:sast && npm run test:posture && npm run test:dataflow && npm run test:mcp && npm run test:report && npm run test:bench-modules && npm run test:lifecycle && npm run test:eval && npm run test:discovery && AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js",
|
|
62
62
|
"test:smoke": "node --test test/smoke.test.js",
|
|
63
63
|
"test:glob": "node --test test/glob-compat.test.js",
|
|
64
64
|
"test:sast": "node --test test/crypto-specialist.test.js test/llm.test.js test/llm-cost-advisor.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/file-upload.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/guard-recognition.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js",
|
|
65
|
-
"test:posture": "node --test test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/attestation.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js",
|
|
65
|
+
"test:posture": "node --test test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/independent-population.test.js test/evidence-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js",
|
|
66
66
|
"test:dataflow": "node --test test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/llm-validator-preset.test.js test/parser-py-cst.test.js test/parser-cs-kt.test.js test/parser-go.test.js test/parser-php-rb.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js",
|
|
67
67
|
"test:mcp": "node --test test/mcp.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js",
|
|
68
68
|
"test:report": "node --test test/sarif-ingest.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js test/report-render.test.js",
|
|
69
69
|
"test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js test/proof-corpus-lib.test.js test/proof-corpus-runner.test.js",
|
|
70
|
-
"test:lifecycle": "node --test test/dead-code.test.js test/no-dead-modules.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js test/dispatch-pre-tool.test.js test/dispatch-user-prompt.test.js test/session-start-model-capture.test.js test/sandbox.test.js test/sandbox-escape.test.js && node ../scripts/lint-command-descriptions.mjs",
|
|
70
|
+
"test:lifecycle": "node --test test/dead-code.test.js test/no-dead-modules.test.js test/no-stray-state.test.js test/tree-integrity.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js test/dispatch-pre-tool.test.js test/dispatch-user-prompt.test.js test/session-start-model-capture.test.js test/sandbox.test.js test/sandbox-escape.test.js && node ../scripts/lint-command-descriptions.mjs",
|
|
71
71
|
"test:eval": "node --test test/independent-eval.test.js test/realworld-recall.test.js",
|
|
72
|
+
"test:discovery": "node --test test/discovery-partition.test.js test/discovery-lenses.test.js test/discovery-hunter.test.js test/discovery-confirm.test.js test/discovery-disprove.test.js test/discovery-judge.test.js test/discovery-run.test.js test/discovery-wiring.test.js test/discovery-llm-invoke.test.js",
|
|
72
73
|
"eval:independent": "node ../bench/independent-eval/runner.mjs",
|
|
73
74
|
"eval:independent:gate": "node ../bench/independent-eval/runner.mjs --gate default",
|
|
74
75
|
"smoke": "node bin/agentic-security.js scan test/fixtures/vulnerable-js",
|
|
@@ -99,12 +100,19 @@
|
|
|
99
100
|
"scorecard": "node ../scripts/scorecard.mjs",
|
|
100
101
|
"corpus:enroll": "node ../scripts/enroll-proven-finding.mjs",
|
|
101
102
|
"corpus:provenance": "node ../scripts/corpus-provenance-check.mjs",
|
|
103
|
+
"fleet": "node ../scripts/fleet.mjs",
|
|
104
|
+
"autopilot": "node ../scripts/autopilot.mjs",
|
|
105
|
+
"comparison": "node ../scripts/comparison.mjs",
|
|
102
106
|
"determinism:attest": "node ../scripts/attest-fixture.mjs",
|
|
103
107
|
"scorecard:check": "node ../scripts/scorecard-check.mjs",
|
|
104
108
|
"release:check": "node ../scripts/release-check.mjs",
|
|
105
109
|
"release:check:fast": "node ../scripts/release-check.mjs --fast",
|
|
106
110
|
"gate:prepush": "node ../scripts/pre-push-gate.mjs",
|
|
107
|
-
"gate:prepush:install": "node ../scripts/pre-push-gate.mjs --install-hook"
|
|
111
|
+
"gate:prepush:install": "node ../scripts/pre-push-gate.mjs --install-hook",
|
|
112
|
+
"bench:independent": "node ../bench/independent/runner.mjs",
|
|
113
|
+
"bench:independent:fetch": "node ../bench/independent/fetch.mjs",
|
|
114
|
+
"bench:independent:mine": "node ../bench/independent/mine.mjs",
|
|
115
|
+
"bench:independent:materialise": "node ../bench/independent/materialise-cli.mjs"
|
|
108
116
|
},
|
|
109
117
|
"author": "Ross Young <ross@clearcapabilities.com>",
|
|
110
118
|
"license": "PolyForm-Internal-Use-1.0.0"
|
package/src/badge.js
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
import * as fs from 'node:fs';
|
|
29
29
|
import * as path from 'node:path';
|
|
30
30
|
|
|
31
|
+
import { statePath } from './posture/state-dir.js';
|
|
31
32
|
const COLORS = {
|
|
32
33
|
critical: '#e05d44', // red
|
|
33
34
|
high: '#fe7d37', // orange
|
|
@@ -42,7 +43,7 @@ const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
|
|
42
43
|
|
|
43
44
|
function _readLastScan(scanRoot) {
|
|
44
45
|
if (!scanRoot) return null;
|
|
45
|
-
const fp =
|
|
46
|
+
const fp = statePath(scanRoot, 'last-scan.json');
|
|
46
47
|
if (!fs.existsSync(fp)) return null;
|
|
47
48
|
try { return JSON.parse(fs.readFileSync(fp, 'utf8')); }
|
|
48
49
|
catch { return null; }
|
|
@@ -44,12 +44,13 @@ import * as fs from 'node:fs';
|
|
|
44
44
|
import * as path from 'node:path';
|
|
45
45
|
import * as yaml from '../util/yaml.js';
|
|
46
46
|
|
|
47
|
+
import { statePath } from '../posture/state-dir.js';
|
|
47
48
|
const SERVICES_FILE_NAMES = ['services.yml', 'services.yaml'];
|
|
48
49
|
|
|
49
50
|
export function loadServiceGraph(scanRoot) {
|
|
50
51
|
if (!scanRoot) return null;
|
|
51
52
|
for (const name of SERVICES_FILE_NAMES) {
|
|
52
|
-
const fp =
|
|
53
|
+
const fp = statePath(scanRoot, name);
|
|
53
54
|
if (!fs.existsSync(fp)) continue;
|
|
54
55
|
try {
|
|
55
56
|
const raw = fs.readFileSync(fp, 'utf8');
|
|
@@ -24,6 +24,7 @@ import * as fs from 'node:fs';
|
|
|
24
24
|
import * as path from 'node:path';
|
|
25
25
|
import * as crypto from 'node:crypto';
|
|
26
26
|
|
|
27
|
+
import { statePath, safeWriteState, stateWritesEnabled } from '../posture/state-dir.js';
|
|
27
28
|
// ── Per-call-site refined summaries ────────────────────────────────────────
|
|
28
29
|
|
|
29
30
|
/**
|
|
@@ -160,7 +161,7 @@ export function backwardSlice(callGraph, finding, opts = {}) {
|
|
|
160
161
|
// ── Persistent cross-scan summary cache ────────────────────────────────────
|
|
161
162
|
|
|
162
163
|
function _cachePath(scanRoot) {
|
|
163
|
-
return
|
|
164
|
+
return statePath(scanRoot, 'ifds-summaries.json');
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
function _fileHash(content) {
|
|
@@ -190,8 +191,9 @@ export function loadPersistedCache(scanRoot) {
|
|
|
190
191
|
* skip re-analysis of functions whose file hash hasn't changed.
|
|
191
192
|
*/
|
|
192
193
|
export function persistCache(scanRoot, cache, perFileIR) {
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
// A read-only scan still ANALYSES; it just may not persist the cache. The
|
|
195
|
+
// next scan simply recomputes, which is slower and identical. (PRD M1)
|
|
196
|
+
if (!stateWritesEnabled()) return;
|
|
195
197
|
const fileHashes = {};
|
|
196
198
|
for (const [filePath, ir] of (perFileIR || new Map())) {
|
|
197
199
|
if (ir && typeof ir._content === 'string') fileHashes[filePath] = _fileHash(ir._content);
|
|
@@ -203,7 +205,7 @@ export function persistCache(scanRoot, cache, perFileIR) {
|
|
|
203
205
|
if (sum.has('∅')) summaries[qid] = sum.get('∅');
|
|
204
206
|
}
|
|
205
207
|
const out = { scanTs: new Date().toISOString(), summaries, fileHashes };
|
|
206
|
-
try {
|
|
208
|
+
try { safeWriteState(_cachePath(scanRoot), JSON.stringify(out, null, 2)); }
|
|
207
209
|
catch { /* best-effort */ }
|
|
208
210
|
}
|
|
209
211
|
|
|
@@ -31,7 +31,8 @@ import * as fs from 'node:fs';
|
|
|
31
31
|
import * as path from 'node:path';
|
|
32
32
|
import * as crypto from 'node:crypto';
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
import { statePath, stateWritesEnabled } from '../posture/state-dir.js';
|
|
35
|
+
|
|
35
36
|
const FILES_PATH = 'files.json';
|
|
36
37
|
const SUMMARIES_PATH = 'summaries.json';
|
|
37
38
|
const VERSION_PATH = 'version.json';
|
|
@@ -44,7 +45,7 @@ export function hashFileContent(stripped) {
|
|
|
44
45
|
|
|
45
46
|
/** Read the persisted state. Returns a fresh empty state on any error. */
|
|
46
47
|
export function readIncrementalState(projectRoot) {
|
|
47
|
-
const dir =
|
|
48
|
+
const dir = statePath(projectRoot, 'incremental');
|
|
48
49
|
try {
|
|
49
50
|
const versionFp = path.join(dir, VERSION_PATH);
|
|
50
51
|
if (!fs.existsSync(versionFp)) return _emptyState();
|
|
@@ -197,9 +198,10 @@ export function serializeSummaries(summaryCache) {
|
|
|
197
198
|
*/
|
|
198
199
|
export function commitIncrementalState(projectRoot, state, currentVersion) {
|
|
199
200
|
if (!projectRoot) return false;
|
|
200
|
-
const dir =
|
|
201
|
+
const dir = statePath(projectRoot, 'incremental');
|
|
201
202
|
try {
|
|
202
|
-
|
|
203
|
+
if (!stateWritesEnabled()) return;
|
|
204
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
203
205
|
fs.writeFileSync(path.join(dir, VERSION_PATH), JSON.stringify(currentVersion, null, 2));
|
|
204
206
|
fs.writeFileSync(path.join(dir, FILES_PATH), JSON.stringify(state.files || {}, null, 2));
|
|
205
207
|
const payload = {
|
|
@@ -215,7 +217,7 @@ export function commitIncrementalState(projectRoot, state, currentVersion) {
|
|
|
215
217
|
|
|
216
218
|
/** Drop persisted state — used when a version mismatch is detected. */
|
|
217
219
|
export function dropIncrementalState(projectRoot) {
|
|
218
|
-
const dir =
|
|
220
|
+
const dir = statePath(projectRoot, 'incremental');
|
|
219
221
|
try {
|
|
220
222
|
if (!fs.existsSync(dir)) return true;
|
|
221
223
|
for (const fn of [VERSION_PATH, FILES_PATH, SUMMARIES_PATH]) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# scanner/src/discovery/
|
|
2
|
+
|
|
3
|
+
LLM-driven candidate discovery, gated by the deterministic engine.
|
|
4
|
+
|
|
5
|
+
User entry point: `agentic-security hunt --root <dir>` (`cmdHunt` in `bin/agentic-security.js`),
|
|
6
|
+
documented in `commands/scan.md`. Advisory only — it never gates a build and its
|
|
7
|
+
output does not enter `last-scan.json`.
|
|
8
|
+
|
|
9
|
+
`partition.js` splits the call graph into disjoint focus areas · `lenses.js`
|
|
10
|
+
holds the seven hunting lenses and the prompt builder · `hunter.js` runs one
|
|
11
|
+
bounded (area × lens) pass · `confirm.js` routes each candidate back through
|
|
12
|
+
the taint engine · `disprove.js` runs the three-angle majority-vote refutation
|
|
13
|
+
panel · `judge.js` shapes findings and dedupes against the prior scan ·
|
|
14
|
+
`llm-invoke.js` is the single injected LLM endpoint caller shared by
|
|
15
|
+
`hunter.js` and `disprove.js` (no other module may talk to an LLM directly) ·
|
|
16
|
+
`index.js` composes them.
|
|
17
|
+
|
|
18
|
+
## Rules
|
|
19
|
+
|
|
20
|
+
- **A hunter proposes; it never decides.** `confirm.js` sets a confirmation
|
|
21
|
+
tier and never filters — an `unconfirmed` candidate still proceeds, because
|
|
22
|
+
the taint engine models only a subset of the program and the absence of
|
|
23
|
+
corroboration must never be laundered into a false-positive verdict.
|
|
24
|
+
`disprove.js` is the only stage that removes a candidate: nothing reaches a
|
|
25
|
+
report without surviving refutation there.
|
|
26
|
+
- **Every LLM call is an injected `llmInvoke`.** No module may import an SDK or
|
|
27
|
+
hard-code an endpoint. Absence of an LLM degrades to an empty, well-formed
|
|
28
|
+
result — it never throws and never blocks a scan.
|
|
29
|
+
- **Silence never refutes.** A probe that says nothing lowers a candidate to
|
|
30
|
+
`unconfirmed`; only an argued majority refutes. A voter that errors is
|
|
31
|
+
excluded from the denominator, not counted as agreement.
|
|
32
|
+
- **Severity comes from evidence, not from the model.** The confirmation tier
|
|
33
|
+
sets it: taint-confirmed → high, sink-adjacent → medium, unconfirmed → low.
|
|
34
|
+
This layer never emits `critical`.
|
|
35
|
+
- **Ids are content digests.** No clock, no randomness, anywhere in an id, a
|
|
36
|
+
digest, or a sort key.
|
|
37
|
+
- **Bounded by default.** The pipeline is multiplicative — areas × lenses
|
|
38
|
+
hunter calls, then three refutation votes per surviving candidate. Six
|
|
39
|
+
files produced 168 LLM calls before C3. `makeBudget` in `index.js` wraps
|
|
40
|
+
`llmInvoke` so every call is counted against a ceiling, and `maxCandidates`
|
|
41
|
+
caps what reaches the panel. Never remove a bound without replacing it:
|
|
42
|
+
an unbounded run's cost is a function of repository size.
|
|
43
|
+
- **An exhausted budget means INCOMPLETE, not clean.** Exhaustion arrives
|
|
44
|
+
through the same degradation path as a dead endpoint and lands in
|
|
45
|
+
`coverage.reasons`. A capped candidate is neither a finding nor cleared —
|
|
46
|
+
it was not examined, and the report says so.
|
|
47
|
+
- **Coverage is reported.** Degraded runs and their reasons appear in every
|
|
48
|
+
report. A half-failed pass must never read as a clean one.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Route every model-proposed candidate back through the deterministic layer.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS. A hunter's output is a hypothesis. The engine underneath it
|
|
5
|
+
// can already decide, for a given file and line, whether tainted data reaches a
|
|
6
|
+
// modelled sink there. Asking it turns "the model thinks so" into "the model
|
|
7
|
+
// thinks so AND the taint engine agrees", which is a materially stronger claim
|
|
8
|
+
// than either layer makes alone.
|
|
9
|
+
//
|
|
10
|
+
// THE ASYMMETRY IS DELIBERATE. Confirmation raises standing; the absence of
|
|
11
|
+
// confirmation NEVER lowers it below `unconfirmed` and is never recorded as a
|
|
12
|
+
// refutation. The taint engine models a subset of the program — an
|
|
13
|
+
// unconfirmed candidate may be a real bug in a construct the engine does not
|
|
14
|
+
// model, and calling that a false positive would launder a coverage gap into a
|
|
15
|
+
// verdict. Refutation is `disprove.js`'s job, and it must be argued, not
|
|
16
|
+
// inferred from silence.
|
|
17
|
+
export const CONFIRMATION_TIERS = Object.freeze(['taint-confirmed', 'sink-adjacent', 'unconfirmed']);
|
|
18
|
+
|
|
19
|
+
function unconfirmed(probedBy, reason) {
|
|
20
|
+
return { tier: 'unconfirmed', evidence: null, probedBy, reason: reason || null };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function confirmCandidate(candidate, opts = {}) {
|
|
24
|
+
const probe = typeof opts.taintProbe === 'function' ? opts.taintProbe : null;
|
|
25
|
+
if (!probe) return { ...candidate, confirmation: unconfirmed(null, 'no taintProbe supplied') };
|
|
26
|
+
|
|
27
|
+
let res;
|
|
28
|
+
try {
|
|
29
|
+
res = await probe(candidate);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return { ...candidate, confirmation: unconfirmed('taintProbe', `probe failed: ${err?.message || String(err)}`) };
|
|
32
|
+
}
|
|
33
|
+
if (!res) return { ...candidate, confirmation: unconfirmed('taintProbe', null) };
|
|
34
|
+
if (!CONFIRMATION_TIERS.includes(res.tier)) {
|
|
35
|
+
return { ...candidate, confirmation: unconfirmed('taintProbe', `unknown tier: ${res.tier}`) };
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
...candidate,
|
|
39
|
+
confirmation: { tier: res.tier, evidence: res.evidence ?? null, probedBy: 'taintProbe', reason: null },
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function confirmAll(candidates, opts = {}) {
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const c of candidates || []) out.push(await confirmCandidate(c, opts));
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Adversarial refutation. Each voter is told to REFUTE the candidate, not to
|
|
3
|
+
// assess it: a model asked "is this real?" agrees with the premise far more
|
|
4
|
+
// often than one asked "show me why this cannot happen", and the second
|
|
5
|
+
// question is the one that kills plausible-but-wrong findings.
|
|
6
|
+
//
|
|
7
|
+
// THREE ANGLES, NOT THREE COPIES. A candidate can fail in more than one way,
|
|
8
|
+
// and three identical voters mostly measure sampling noise. Reachability,
|
|
9
|
+
// attacker preconditions, and sanitization are the three ways these candidates
|
|
10
|
+
// actually die.
|
|
11
|
+
//
|
|
12
|
+
// SILENCE NEVER REFUTES. A voter that errors or returns unparseable output did
|
|
13
|
+
// not vote, and is excluded from the denominator rather than counted as
|
|
14
|
+
// agreement. If nobody votes the panel is `undecided` and the candidate
|
|
15
|
+
// SURVIVES — an outage must not quietly delete findings.
|
|
16
|
+
import { resolveLlmInvoke } from './llm-invoke.js';
|
|
17
|
+
|
|
18
|
+
const DEFAULT_ANGLES = ['reachability', 'preconditions', 'sanitization'];
|
|
19
|
+
export const REFUTE_ANGLES = Object.freeze([...DEFAULT_ANGLES]);
|
|
20
|
+
|
|
21
|
+
const ANGLE_BRIEF = {
|
|
22
|
+
reachability: 'Can attacker-controlled data actually reach this line at runtime? Name the caller chain or show there is none.',
|
|
23
|
+
preconditions: 'What must the attacker already have — a session, a role, a tenant, a race window? If the prerequisites exceed the impact, it is refuted.',
|
|
24
|
+
sanitization: 'Is the value validated, escaped, parameterised, or type-constrained anywhere on the path? A framework default counts.',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function buildRefutePrompt(candidate, angle) {
|
|
28
|
+
return [
|
|
29
|
+
`Your job is to REFUTE the security finding below. Assume it is wrong and look for the reason.`,
|
|
30
|
+
`Refute it on this angle only: ${angle}. ${ANGLE_BRIEF[angle] || ''}`,
|
|
31
|
+
``,
|
|
32
|
+
`Finding: ${candidate.title}`,
|
|
33
|
+
`Location: ${candidate.file}:${candidate.line}`,
|
|
34
|
+
`Claimed reason: ${candidate.rationale || '(none given)'}`,
|
|
35
|
+
`Deterministic confirmation: ${candidate.confirmation?.tier || 'unknown'}`,
|
|
36
|
+
``,
|
|
37
|
+
`If you cannot refute it on this angle, say so honestly.`,
|
|
38
|
+
`Return JSON: {"refuted":true|false,"reason":"..."}`,
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseVote(raw) {
|
|
43
|
+
if (typeof raw !== 'string') return null;
|
|
44
|
+
const s = raw.indexOf('{'), e = raw.lastIndexOf('}');
|
|
45
|
+
if (s < 0 || e <= s) return null;
|
|
46
|
+
let p;
|
|
47
|
+
try { p = JSON.parse(raw.slice(s, e + 1)); } catch { return null; }
|
|
48
|
+
if (typeof p?.refuted !== 'boolean') return null;
|
|
49
|
+
return { refuted: p.refuted, reason: typeof p.reason === 'string' ? p.reason : '' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function disproveCandidate(candidate, opts = {}) {
|
|
53
|
+
const angles = Array.isArray(opts.angles) && opts.angles.length ? opts.angles : DEFAULT_ANGLES;
|
|
54
|
+
const llmInvoke = resolveLlmInvoke(opts);
|
|
55
|
+
|
|
56
|
+
const votes = [];
|
|
57
|
+
if (typeof llmInvoke === 'function') {
|
|
58
|
+
for (const angle of angles) {
|
|
59
|
+
let vote = null;
|
|
60
|
+
try { vote = parseVote(await llmInvoke(buildRefutePrompt(candidate, angle))); } catch { vote = null; }
|
|
61
|
+
if (vote) votes.push({ angle, ...vote });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const voterCount = votes.length;
|
|
66
|
+
const refuteCount = votes.filter(v => v.refuted).length;
|
|
67
|
+
const undecided = voterCount === 0;
|
|
68
|
+
const refuted = !undecided && refuteCount * 2 > voterCount;
|
|
69
|
+
return { ...candidate, refutation: { votes, voterCount, refuteCount, refuted, undecided } };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function disprovePanel(candidates, opts = {}) {
|
|
73
|
+
const survivors = [], refuted = [];
|
|
74
|
+
for (const c of candidates || []) {
|
|
75
|
+
const judged = await disproveCandidate(c, opts);
|
|
76
|
+
(judged.refutation.refuted ? refuted : survivors).push(judged);
|
|
77
|
+
}
|
|
78
|
+
return { survivors, refuted };
|
|
79
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
//
|
|
2
|
+
// One bounded hunter run = one focus area seen through one lens.
|
|
3
|
+
//
|
|
4
|
+
// A hunter PROPOSES. Nothing here is a finding: every candidate must survive
|
|
5
|
+
// `confirm.js` and `disprove.js` first. That separation is the whole design —
|
|
6
|
+
// the model is allowed to be imaginative precisely because something
|
|
7
|
+
// deterministic checks it afterwards.
|
|
8
|
+
//
|
|
9
|
+
// FAILURE IS ALWAYS DEGRADATION, NEVER AN EXCEPTION. A missing endpoint, a
|
|
10
|
+
// rate limit, or unparseable output yields `degraded:true` with a reason. A
|
|
11
|
+
// discovery pass that cannot run must leave the rest of the scan intact.
|
|
12
|
+
import * as crypto from 'node:crypto';
|
|
13
|
+
import { buildHunterPrompt } from './lenses.js';
|
|
14
|
+
import { resolveLlmInvoke } from './llm-invoke.js';
|
|
15
|
+
|
|
16
|
+
function appendEntry(transcript, entry) {
|
|
17
|
+
const prev = transcript.length ? transcript[transcript.length - 1].hash : null;
|
|
18
|
+
// Named `serialized`, not `body`: this is the canonical serialisation of a
|
|
19
|
+
// transcript entry being fed to a hash, and calling it `body` made the
|
|
20
|
+
// mass-assignment detector read it as a request payload. The name was simply
|
|
21
|
+
// wrong for what it holds, so this is a fix at the source rather than a
|
|
22
|
+
// suppression — and that detector's finding carries no line number, so a
|
|
23
|
+
// line-scoped ignore pragma could not have matched it anyway.
|
|
24
|
+
const serialized = JSON.stringify({ ...entry, prev });
|
|
25
|
+
const hash = crypto.createHash('sha256').update(serialized).digest('hex');
|
|
26
|
+
transcript.push({ ...entry, prev, hash });
|
|
27
|
+
return transcript;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractJsonWithFlag(raw) {
|
|
31
|
+
if (typeof raw !== 'string') return { parsed: null, found: false };
|
|
32
|
+
const start = raw.indexOf('{');
|
|
33
|
+
const end = raw.lastIndexOf('}');
|
|
34
|
+
if (start < 0 || end <= start) return { parsed: null, found: false };
|
|
35
|
+
try { return { parsed: JSON.parse(raw.slice(start, end + 1)), found: true }; } catch { return { parsed: null, found: false }; }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function candidateId(focusAreaId, lensKey, file, line, title) {
|
|
39
|
+
return crypto.createHash('sha256')
|
|
40
|
+
.update(`${focusAreaId}|${lensKey}|${file}|${line}|${title}`)
|
|
41
|
+
.digest('hex').slice(0, 12);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function candidatesFromParsed(parsed, focusArea, lens) {
|
|
45
|
+
const list = Array.isArray(parsed?.candidates) ? parsed.candidates : [];
|
|
46
|
+
const out = [];
|
|
47
|
+
for (const c of list) {
|
|
48
|
+
const file = typeof c?.file === 'string' ? c.file : null;
|
|
49
|
+
const line = Number.isInteger(c?.line) ? c.line : Number.parseInt(c?.line, 10);
|
|
50
|
+
if (!file || !Number.isInteger(line)) continue; // no location, no candidate
|
|
51
|
+
const title = typeof c?.title === 'string' && c.title ? c.title : `${lens.title} candidate`;
|
|
52
|
+
out.push({
|
|
53
|
+
id: candidateId(focusArea.id, lens.key, file, line, title),
|
|
54
|
+
focusAreaId: focusArea.id,
|
|
55
|
+
lens: lens.key,
|
|
56
|
+
title,
|
|
57
|
+
file,
|
|
58
|
+
line,
|
|
59
|
+
family: lens.family,
|
|
60
|
+
cwe: lens.cwe,
|
|
61
|
+
rationale: typeof c?.rationale === 'string' ? c.rationale : '',
|
|
62
|
+
entryPoint: typeof c?.entryPoint === 'string' ? c.entryPoint : '',
|
|
63
|
+
sink: typeof c?.sink === 'string' ? c.sink : '',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseCandidates(raw, focusArea, lens) {
|
|
70
|
+
const { parsed } = extractJsonWithFlag(raw);
|
|
71
|
+
return candidatesFromParsed(parsed, focusArea, lens);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function runHunter(focusArea, lens, ctx = {}, opts = {}) {
|
|
75
|
+
const transcript = [];
|
|
76
|
+
const lensKey = lens?.key || 'unknown';
|
|
77
|
+
const base = { focusAreaId: focusArea.id, lens: lensKey, transcript };
|
|
78
|
+
const llmInvoke = resolveLlmInvoke(opts);
|
|
79
|
+
|
|
80
|
+
if (typeof llmInvoke !== 'function') {
|
|
81
|
+
const reason = 'no llmInvoke supplied and AGENTIC_SECURITY_LLM_ENDPOINT not set';
|
|
82
|
+
appendEntry(transcript, { phase: 'init', reason });
|
|
83
|
+
return { ...base, candidates: [], degraded: true, reason };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let prompt;
|
|
87
|
+
try {
|
|
88
|
+
prompt = buildHunterPrompt(focusArea, lens, ctx);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const reason = `failed to build hunter prompt: ${err?.message || String(err)}`;
|
|
91
|
+
appendEntry(transcript, { phase: 'prompt_error', reason });
|
|
92
|
+
return { ...base, candidates: [], degraded: true, reason };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
appendEntry(transcript, { phase: 'prompt', promptChars: prompt.length, files: focusArea.files.length });
|
|
96
|
+
|
|
97
|
+
let raw;
|
|
98
|
+
try {
|
|
99
|
+
raw = await llmInvoke(prompt);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const reason = `hunter llm call failed: ${err?.message || String(err)}`;
|
|
102
|
+
appendEntry(transcript, { phase: 'error', reason });
|
|
103
|
+
return { ...base, candidates: [], degraded: true, reason };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { parsed, found } = extractJsonWithFlag(raw);
|
|
107
|
+
if (!found) {
|
|
108
|
+
const reason = 'hunter output was not parseable JSON';
|
|
109
|
+
appendEntry(transcript, { phase: 'parse_error', reason });
|
|
110
|
+
return { ...base, candidates: [], degraded: true, reason };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const candidates = candidatesFromParsed(parsed, focusArea, lens);
|
|
114
|
+
appendEntry(transcript, { phase: 'result', candidateCount: candidates.length });
|
|
115
|
+
return { ...base, candidates, degraded: false, reason: null };
|
|
116
|
+
}
|