@clear-capabilities/agentic-security-scanner 0.134.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 +196 -0
- package/bin/agentic-security-audit.js +2 -1
- package/bin/agentic-security-consistency.js +2 -1
- package/bin/agentic-security.js +269 -44
- 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/513.index.js +7 -3
- package/dist/526.index.js +6 -6
- 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 +14 -14
- 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 +9 -5
- 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 +10 -0
- package/src/discovery/index.js +175 -3
- package/src/discovery/llm-invoke.js +90 -1
- package/src/discovery/memory.js +163 -0
- package/src/engine.js +45 -8
- 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 +1 -2
- package/src/mcp/audit.js +2 -1
- package/src/mcp/tools.js +4 -3
- package/src/posture/CLAUDE.md +26 -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/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/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/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/model-rescan.js +4 -3
- package/src/posture/network-policy-import.js +3 -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/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/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,19 +55,19 @@
|
|
|
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
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/package-contents-check.test.js test/gate-verdict-cache.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",
|
|
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
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",
|
|
73
73
|
"eval:independent": "node ../bench/independent-eval/runner.mjs",
|
|
@@ -108,7 +108,11 @@
|
|
|
108
108
|
"release:check": "node ../scripts/release-check.mjs",
|
|
109
109
|
"release:check:fast": "node ../scripts/release-check.mjs --fast",
|
|
110
110
|
"gate:prepush": "node ../scripts/pre-push-gate.mjs",
|
|
111
|
-
"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"
|
|
112
116
|
},
|
|
113
117
|
"author": "Ross Young <ross@clearcapabilities.com>",
|
|
114
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]) {
|
package/src/discovery/CLAUDE.md
CHANGED
|
@@ -34,5 +34,15 @@ panel · `judge.js` shapes findings and dedupes against the prior scan ·
|
|
|
34
34
|
This layer never emits `critical`.
|
|
35
35
|
- **Ids are content digests.** No clock, no randomness, anywhere in an id, a
|
|
36
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.
|
|
37
47
|
- **Coverage is reported.** Degraded runs and their reasons appear in every
|
|
38
48
|
report. A half-failed pass must never read as a clean one.
|
package/src/discovery/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { runHunter } from './hunter.js';
|
|
|
13
13
|
import { confirmAll } from './confirm.js';
|
|
14
14
|
import { disprovePanel } from './disprove.js';
|
|
15
15
|
import { judgeCandidates } from './judge.js';
|
|
16
|
+
import { loadMemory, saveMemory, rememberRun, previouslyRefuted, nextWavePlan } from './memory.js';
|
|
16
17
|
|
|
17
18
|
// Bridge a candidate to the deterministic layer. A taint finding at or within
|
|
18
19
|
// two lines of the candidate corroborates it; a modelled sink on the line
|
|
@@ -54,10 +55,91 @@ async function runDeepAnalysisSafe(perFileIR, callGraph) {
|
|
|
54
55
|
// where { perFileIR, callGraph } come from buildProjectIR(fileContents),
|
|
55
56
|
// which returns { perFile, callGraph } — callers must pass perFile as
|
|
56
57
|
// perFileIR (see scanner/src/ir/index.js).
|
|
58
|
+
/**
|
|
59
|
+
* PRD Phase 0 / C3 — the run budget.
|
|
60
|
+
*
|
|
61
|
+
* This pipeline is multiplicative and was, until now, unbounded. Eight focus
|
|
62
|
+
* areas × seven lenses is 56 hunter calls before a single candidate exists, and
|
|
63
|
+
* every surviving candidate then costs three more calls in the refutation
|
|
64
|
+
* panel. Nothing capped any of it, so the cost of a run was a function of how
|
|
65
|
+
* large the repository happened to be — which is not a property you want to
|
|
66
|
+
* discover from an invoice.
|
|
67
|
+
*
|
|
68
|
+
* ENFORCED AT THE ONE SEAM EVERY CALL PASSES THROUGH. Rather than thread checks
|
|
69
|
+
* through the hunter and the panel, the budget wraps `llmInvoke` itself. When
|
|
70
|
+
* it is spent the wrapper throws, and both callers already treat a throwing
|
|
71
|
+
* llmInvoke as ordinary degradation with a stated reason. So exhaustion arrives
|
|
72
|
+
* through the same path as a rate limit or a dead endpoint, and lands in
|
|
73
|
+
* `coverage.reasons` like any other coverage gap. No new failure mode.
|
|
74
|
+
*
|
|
75
|
+
* CALLS AND WALL CLOCK, NOT TOKENS. `llmInvoke` is an injected callback that
|
|
76
|
+
* returns a string; it carries no usage metadata, so counting tokens here would
|
|
77
|
+
* mean inventing a number. Calls are exactly countable and wall clock is
|
|
78
|
+
* exactly observable. A caller who knows their per-call cost can pass
|
|
79
|
+
* `costPerCallUsd` and get a `maxCostUsd` ceiling expressed in calls, which is
|
|
80
|
+
* honest about being an estimate derived from their figure rather than ours.
|
|
81
|
+
*/
|
|
82
|
+
// Internal, not exported: the dead-module guard treats an export with no
|
|
83
|
+
// external call site as shipped dead code, and these are read only by
|
|
84
|
+
// makeBudget below. A consumer sets a ceiling by passing opts, not by importing
|
|
85
|
+
// a constant.
|
|
86
|
+
const DEFAULT_MAX_LLM_CALLS = 200;
|
|
87
|
+
const DEFAULT_MAX_WALL_MS = 15 * 60 * 1000;
|
|
88
|
+
const DEFAULT_MAX_CANDIDATES = 50;
|
|
89
|
+
|
|
90
|
+
export function makeBudget(opts = {}, now = Date.now) {
|
|
91
|
+
const startedAt = now();
|
|
92
|
+
let maxCalls = Number.isInteger(opts.maxLlmCalls) && opts.maxLlmCalls >= 0
|
|
93
|
+
? opts.maxLlmCalls : DEFAULT_MAX_LLM_CALLS;
|
|
94
|
+
// A dollar ceiling is only meaningful with a caller-supplied per-call cost.
|
|
95
|
+
// Converting it to a call count keeps one enforcement mechanism rather than
|
|
96
|
+
// two that can disagree.
|
|
97
|
+
if (Number.isFinite(opts.maxCostUsd) && Number.isFinite(opts.costPerCallUsd) && opts.costPerCallUsd > 0) {
|
|
98
|
+
maxCalls = Math.min(maxCalls, Math.floor(opts.maxCostUsd / opts.costPerCallUsd));
|
|
99
|
+
}
|
|
100
|
+
const maxWallMs = Number.isInteger(opts.maxWallMs) && opts.maxWallMs > 0
|
|
101
|
+
? opts.maxWallMs : DEFAULT_MAX_WALL_MS;
|
|
102
|
+
|
|
103
|
+
let calls = 0;
|
|
104
|
+
let exhaustedReason = null;
|
|
105
|
+
|
|
106
|
+
const check = () => {
|
|
107
|
+
if (exhaustedReason) return exhaustedReason;
|
|
108
|
+
if (calls >= maxCalls) return (exhaustedReason = `LLM call budget spent (${calls}/${maxCalls} calls)`);
|
|
109
|
+
if (now() - startedAt >= maxWallMs) {
|
|
110
|
+
return (exhaustedReason = `wall-clock budget spent (${Math.round(maxWallMs / 1000)}s)`);
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
get calls() { return calls; },
|
|
117
|
+
get maxCalls() { return maxCalls; },
|
|
118
|
+
get exhaustedReason() { return exhaustedReason; },
|
|
119
|
+
spent: () => check() !== null,
|
|
120
|
+
/** Wrap an llmInvoke so every call is counted and the ceiling is enforced. */
|
|
121
|
+
wrap(llmInvoke) {
|
|
122
|
+
if (typeof llmInvoke !== 'function') return llmInvoke;
|
|
123
|
+
return async (prompt) => {
|
|
124
|
+
const stop = check();
|
|
125
|
+
if (stop) throw new Error(`discovery budget exhausted: ${stop}`);
|
|
126
|
+
calls += 1;
|
|
127
|
+
return llmInvoke(prompt);
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
57
133
|
export async function runDiscovery(ctx = {}, opts = {}) {
|
|
58
134
|
const areas = partitionCallGraph(ctx.callGraph, { maxAreas: opts.maxAreas ?? 8 });
|
|
59
135
|
|
|
60
136
|
const reasons = [];
|
|
137
|
+
const budget = makeBudget(opts);
|
|
138
|
+
// PRD C4 — what previous runs already judged. scanRoot absent => no memory,
|
|
139
|
+
// which is the correct default for a library call with nowhere to persist.
|
|
140
|
+
const memory = opts.scanRoot ? loadMemory(opts.scanRoot) : null;
|
|
141
|
+
// Every LLM call in this pipeline goes through this one wrapped callback.
|
|
142
|
+
const llmInvoke = budget.wrap(opts.llmInvoke);
|
|
61
143
|
|
|
62
144
|
// An explicit array (including an empty one) is honoured exactly — a caller
|
|
63
145
|
// narrowing a run to no lenses must get no lenses, not a silent fallback to
|
|
@@ -87,7 +169,7 @@ export async function runDiscovery(ctx = {}, opts = {}) {
|
|
|
87
169
|
for (const area of areas) {
|
|
88
170
|
let areaDegradedCount = 0;
|
|
89
171
|
for (const lens of lenses) {
|
|
90
|
-
const run = await runHunter(area, lens, { fileContents: ctx.fileContents || {} }, { llmInvoke
|
|
172
|
+
const run = await runHunter(area, lens, { fileContents: ctx.fileContents || {} }, { llmInvoke });
|
|
91
173
|
runs.push({ focusAreaId: run.focusAreaId, lens: run.lens, degraded: run.degraded, reason: run.reason, candidateCount: run.candidates.length });
|
|
92
174
|
if (run.degraded && run.reason) reasons.push(`${area.label} × ${lens.key}: ${run.reason}`);
|
|
93
175
|
if (run.degraded) areaDegradedCount += 1;
|
|
@@ -97,11 +179,69 @@ export async function runDiscovery(ctx = {}, opts = {}) {
|
|
|
97
179
|
if (lenses.length > 0 && areaDegradedCount === 0) fullyHunted.add(area.id);
|
|
98
180
|
}
|
|
99
181
|
|
|
100
|
-
|
|
182
|
+
// PRD Phase 0 / C3.2 — the candidate cap.
|
|
183
|
+
//
|
|
184
|
+
// Every candidate that reaches the panel costs three more LLM calls, so an
|
|
185
|
+
// unusually productive hunt multiplies straight into spend. Cap it, and
|
|
186
|
+
// REPORT the cap rather than applying it silently: a run that quietly
|
|
187
|
+
// examined the first N candidates and said nothing would look identical to a
|
|
188
|
+
// run that found only N. Same precedent as prove-findings.js's `capped`.
|
|
189
|
+
// PRD C4 — drop what a previous run already refuted, BEFORE spending the
|
|
190
|
+
// panel's three calls per candidate on it again. Only refutals suppress: a
|
|
191
|
+
// candidate previously judged fresh is re-reported, because it was never
|
|
192
|
+
// fixed, which is the same asymmetry judge.js applies to tp/fp triage.
|
|
193
|
+
let rememberedRefutals = 0;
|
|
194
|
+
if (memory) {
|
|
195
|
+
const before = candidates.length;
|
|
196
|
+
candidates = candidates.filter(c => !previouslyRefuted(memory, c));
|
|
197
|
+
rememberedRefutals = before - candidates.length;
|
|
198
|
+
if (rememberedRefutals > 0) {
|
|
199
|
+
reasons.push(`${rememberedRefutals} candidate(s) were refuted by an earlier run and not re-examined ` +
|
|
200
|
+
'(clear with --forget-refuted if a model, ruleset or the code has changed since)');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const maxCandidates = Number.isInteger(opts.maxCandidates) && opts.maxCandidates >= 0
|
|
205
|
+
? opts.maxCandidates : DEFAULT_MAX_CANDIDATES;
|
|
206
|
+
let candidatesCapped = 0;
|
|
207
|
+
if (candidates.length > maxCandidates) {
|
|
208
|
+
candidatesCapped = candidates.length - maxCandidates;
|
|
209
|
+
// Deterministic: candidates arrive in a stable (area, lens) order, so the
|
|
210
|
+
// cap keeps the same prefix on every run over the same inputs.
|
|
211
|
+
candidates = candidates.slice(0, maxCandidates);
|
|
212
|
+
reasons.push(`candidate cap: ${candidatesCapped} candidate(s) were NOT confirmed or refuted ` +
|
|
213
|
+
`(cap ${maxCandidates}); they are neither findings nor cleared — they were not examined`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// PRD D3 — the hybrid-loop uplift measurement.
|
|
217
|
+
//
|
|
218
|
+
// `confirm: false` runs the pipeline with the deterministic gate switched OFF,
|
|
219
|
+
// so every candidate reaches the panel as `unconfirmed`. Running a population
|
|
220
|
+
// both ways and diffing the result isolates what the taint engine contributes
|
|
221
|
+
// on top of the model — a number no surveyed competitor can compute, because
|
|
222
|
+
// none of them has a deterministic layer to switch off.
|
|
223
|
+
//
|
|
224
|
+
// It exists ONLY to be measured against. It is not a performance switch, and
|
|
225
|
+
// a run with it disabled is strictly weaker: severity collapses to `low` for
|
|
226
|
+
// everything, since the confirmation tier is what sets it.
|
|
227
|
+
const confirmationEnabled = opts.confirm !== false;
|
|
228
|
+
const taintProbe = confirmationEnabled ? makeTaintProbe(ctx.perFileIR, ctx.callGraph) : null;
|
|
229
|
+
if (!confirmationEnabled) {
|
|
230
|
+
reasons.push('deterministic confirmation was DISABLED for this run (uplift measurement); ' +
|
|
231
|
+
'every candidate is reported unconfirmed and severity is not evidence-derived');
|
|
232
|
+
}
|
|
101
233
|
const confirmed = await confirmAll(candidates, { taintProbe });
|
|
102
|
-
const { survivors, refuted } = await disprovePanel(confirmed, { llmInvoke
|
|
234
|
+
const { survivors, refuted } = await disprovePanel(confirmed, { llmInvoke });
|
|
103
235
|
const { fresh, duplicates, suppressed } = judgeCandidates(survivors, ctx.priorScan, ctx.triageFeedback);
|
|
104
236
|
|
|
237
|
+
// A spent budget is a coverage gap, stated once at the top level rather than
|
|
238
|
+
// left to be inferred from N identical per-run degradation reasons.
|
|
239
|
+
if (budget.exhaustedReason) {
|
|
240
|
+
reasons.push(`RUN INCOMPLETE — ${budget.exhaustedReason}. Work remained when the budget ran ` +
|
|
241
|
+
'out, so absence of a finding below is not evidence of absence. Raise maxLlmCalls / ' +
|
|
242
|
+
'maxWallMs, or narrow the scope with --root or --lens, and re-run.');
|
|
243
|
+
}
|
|
244
|
+
|
|
105
245
|
// Coverage must not stop at the hunter stage. `confirm.js` correctly never
|
|
106
246
|
// lowers a candidate below `unconfirmed`, and `disprove.js` correctly lets
|
|
107
247
|
// a candidate survive when no voter votes — each rule is right on its own,
|
|
@@ -124,6 +264,16 @@ export async function runDiscovery(ctx = {}, opts = {}) {
|
|
|
124
264
|
reasons.push(`refutation panel returned no votes for any of ${panelsRun} candidate(s) — every finding below survived unrefuted, not because it withstood scrutiny`);
|
|
125
265
|
}
|
|
126
266
|
|
|
267
|
+
// PRD C4 — fold this run into the memory so the next one can be additive
|
|
268
|
+
// rather than a repeat. Persistence failure is non-fatal: the report is still
|
|
269
|
+
// valid, it just will not inform the next run.
|
|
270
|
+
if (memory && opts.scanRoot) {
|
|
271
|
+
saveMemory(opts.scanRoot, rememberRun(memory, {
|
|
272
|
+
fresh, refutedCandidates: refuted,
|
|
273
|
+
areas: areas.map(a => ({ id: a.id, label: a.label, files: a.files.length, hunted: hunted.has(a.id) })),
|
|
274
|
+
}));
|
|
275
|
+
}
|
|
276
|
+
|
|
127
277
|
return {
|
|
128
278
|
schema: 'agentic-security/discovery@1',
|
|
129
279
|
focusAreas: areas.map(a => ({ id: a.id, label: a.label, files: a.files.length, size: a.size })),
|
|
@@ -153,6 +303,28 @@ export async function runDiscovery(ctx = {}, opts = {}) {
|
|
|
153
303
|
// of those came back with no votes at all (undecided, not refuted).
|
|
154
304
|
panelsRun,
|
|
155
305
|
undecidedPanels,
|
|
306
|
+
// PRD Phase 0 / C3 — what the run cost and whether the budget stopped it.
|
|
307
|
+
// `budgetExhausted` true means the report is INCOMPLETE by construction:
|
|
308
|
+
// work remained and was not done. Reading it as a clean result is the
|
|
309
|
+
// exact misreading the coverage block exists to prevent.
|
|
310
|
+
// PRD C4 — what history contributed, and what to hunt next. A coverage
|
|
311
|
+
// report says what happened; `nextWave` says what to do about it.
|
|
312
|
+
rememberedRefutals,
|
|
313
|
+
priorRuns: memory ? memory.runs : null,
|
|
314
|
+
nextWave: memory ? nextWavePlan(memory, areas.map(a => ({ id: a.id, label: a.label }))) : null,
|
|
315
|
+
llmCalls: budget.calls,
|
|
316
|
+
maxLlmCalls: budget.maxCalls,
|
|
317
|
+
// PRD N4 — the standing cost metric. C3 bounded the worst case and C4
|
|
318
|
+
// moved the typical case by 4x, so cost is a property that drifts across
|
|
319
|
+
// several workstreams rather than one that a phase finishes. A number
|
|
320
|
+
// that only appears when somebody goes looking regresses silently, so it
|
|
321
|
+
// is reported every run and carries its denominator like every other rate
|
|
322
|
+
// in this engine. `null` when nothing was found — dividing by zero
|
|
323
|
+
// findings would print Infinity and read as a catastrophe rather than as
|
|
324
|
+
// "there is nothing to divide".
|
|
325
|
+
callsPerFinding: fresh.length > 0 ? Number((budget.calls / fresh.length).toFixed(1)) : null,
|
|
326
|
+
budgetExhausted: Boolean(budget.exhaustedReason),
|
|
327
|
+
candidatesCapped,
|
|
156
328
|
reasons,
|
|
157
329
|
},
|
|
158
330
|
};
|