@clear-capabilities/agentic-security-scanner 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1580 -0
- package/bin/.agentic-security/findings.json +1577 -0
- package/bin/.agentic-security/last-scan.json +1577 -0
- package/bin/.agentic-security/last-scan.json.sig +1 -0
- package/bin/.agentic-security/scan-history.json +465 -0
- package/bin/.agentic-security/streak.json +25 -0
- package/bin/agentic-security-audit.js +198 -0
- package/bin/agentic-security-consistency.js +80 -0
- package/bin/agentic-security-diff.js +136 -0
- package/bin/agentic-security-lsp.js +12 -0
- package/bin/agentic-security-mcp.js +40 -0
- package/bin/agentic-security-rule.js +153 -0
- package/bin/agentic-security.js +1683 -0
- package/dist/117.index.js +207 -0
- package/dist/178.index.js +250 -0
- package/dist/218.index.js +793 -0
- package/dist/227.index.js +192 -0
- package/dist/301.index.js +167 -0
- package/dist/384.index.js +18 -0
- package/dist/476.index.js +126 -0
- package/dist/513.index.js +373 -0
- package/dist/520.index.js +13 -0
- package/dist/601.index.js +1038 -0
- package/dist/634.index.js +1892 -0
- package/dist/637.index.js +216 -0
- package/dist/660.index.js +131 -0
- package/dist/675.index.js +451 -0
- package/dist/826.index.js +188 -0
- package/dist/830.index.js +133 -0
- package/dist/agentic-security.mjs +272 -0
- package/dist/agentic-security.mjs.sha256 +1 -0
- package/dist/calibration-seed.json +27 -0
- package/package.json +77 -0
- package/src/.agentic-security/findings.json +80844 -0
- package/src/.agentic-security/last-scan.json +80844 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +8408 -0
- package/src/.agentic-security/streak.json +26 -0
- package/src/badge.js +188 -0
- package/src/compare.js +203 -0
- package/src/dataflow/.agentic-security/findings.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json +3487 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +735 -0
- package/src/dataflow/.agentic-security/streak.json +24 -0
- package/src/dataflow/CLAUDE.md +38 -0
- package/src/dataflow/access-paths.js +172 -0
- package/src/dataflow/async-sequencing.js +177 -0
- package/src/dataflow/backward.js +201 -0
- package/src/dataflow/catalog-expanded.js +485 -0
- package/src/dataflow/catalog.js +659 -0
- package/src/dataflow/cross-repo.js +219 -0
- package/src/dataflow/engine.js +588 -0
- package/src/dataflow/exception-flow.js +116 -0
- package/src/dataflow/exploit-prover.js +187 -0
- package/src/dataflow/higher-order.js +221 -0
- package/src/dataflow/ifds.js +347 -0
- package/src/dataflow/implicit-flow.js +129 -0
- package/src/dataflow/incremental.js +229 -0
- package/src/dataflow/index.js +181 -0
- package/src/dataflow/numeric-domain.js +192 -0
- package/src/dataflow/path-feasibility.js +114 -0
- package/src/dataflow/points-to.js +337 -0
- package/src/dataflow/polyglot.js +190 -0
- package/src/dataflow/proven-clean.js +159 -0
- package/src/dataflow/receiver-context.js +76 -0
- package/src/dataflow/sanitizer-proof.js +154 -0
- package/src/dataflow/soft-taint.js +140 -0
- package/src/dataflow/string-domain.js +234 -0
- package/src/dataflow/stub-aware-filter.js +100 -0
- package/src/dataflow/summaries.js +132 -0
- package/src/dataflow/symbolic-exec.js +238 -0
- package/src/dataflow/tabulation.js +135 -0
- package/src/engine.js +7763 -0
- package/src/history-scan.js +229 -0
- package/src/index.js +3 -0
- package/src/integrations/.agentic-security/findings.json +1504 -0
- package/src/integrations/.agentic-security/last-scan.json +1504 -0
- package/src/integrations/.agentic-security/scan-history.json +40 -0
- package/src/integrations/.agentic-security/streak.json +21 -0
- package/src/integrations/index.js +321 -0
- package/src/integrations/tickets.js +200 -0
- package/src/ir/.agentic-security/findings.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json +3036 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +364 -0
- package/src/ir/.agentic-security/streak.json +23 -0
- package/src/ir/CLAUDE.md +172 -0
- package/src/ir/callgraph.js +73 -0
- package/src/ir/class-hierarchy.js +195 -0
- package/src/ir/index.js +152 -0
- package/src/ir/parser-cs.js +260 -0
- package/src/ir/parser-java.js +286 -0
- package/src/ir/parser-js.js +413 -0
- package/src/ir/parser-kt.js +258 -0
- package/src/ir/parser-py-cst.js +136 -0
- package/src/ir/parser-py.helper.py +501 -0
- package/src/ir/parser-py.js +312 -0
- package/src/ir/ssa.js +315 -0
- package/src/ir/type-stubs.js +288 -0
- package/src/leaderboard.js +152 -0
- package/src/llm-validator/.agentic-security/findings.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json +1891 -0
- package/src/llm-validator/.agentic-security/last-scan.json.sig +1 -0
- package/src/llm-validator/.agentic-security/scan-history.json +168 -0
- package/src/llm-validator/.agentic-security/streak.json +20 -0
- package/src/llm-validator/consistency.js +141 -0
- package/src/llm-validator/index.js +437 -0
- package/src/lsp/.agentic-security/findings.json +28 -0
- package/src/lsp/.agentic-security/last-scan.json +28 -0
- package/src/lsp/.agentic-security/scan-history.json +79 -0
- package/src/lsp/.agentic-security/streak.json +22 -0
- package/src/lsp/server.js +275 -0
- package/src/mcp/.agentic-security/findings.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json +8358 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +1125 -0
- package/src/mcp/.agentic-security/streak.json +22 -0
- package/src/mcp/CLAUDE.md +54 -0
- package/src/mcp/audit.js +136 -0
- package/src/mcp/redact.js +75 -0
- package/src/mcp/server.js +158 -0
- package/src/mcp/stdio.js +83 -0
- package/src/mcp/tools.js +940 -0
- package/src/mcp/validate.js +49 -0
- package/src/personality.js +164 -0
- package/src/poc-video.js +239 -0
- package/src/posture/.agentic-security/findings.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json +51239 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +5557 -0
- package/src/posture/.agentic-security/streak.json +24 -0
- package/src/posture/CLAUDE.md +42 -0
- package/src/posture/adversarial-self-test.js +114 -0
- package/src/posture/adversary-agent.js +204 -0
- package/src/posture/agents-memory.js +135 -0
- package/src/posture/ai-code-fingerprint.js +171 -0
- package/src/posture/aibom.js +284 -0
- package/src/posture/api-inventory.js +96 -0
- package/src/posture/attack-playbooks.js +305 -0
- package/src/posture/auditor-agent.js +115 -0
- package/src/posture/auth-posture-import.js +135 -0
- package/src/posture/baseline-compare.js +114 -0
- package/src/posture/blast-radius.js +836 -0
- package/src/posture/bounty-prediction.js +141 -0
- package/src/posture/business-logic.js +239 -0
- package/src/posture/calibration-drift.js +93 -0
- package/src/posture/calibration-seed.json +27 -0
- package/src/posture/calibration.js +204 -0
- package/src/posture/clustering.js +75 -0
- package/src/posture/concurrency-checker.js +265 -0
- package/src/posture/confidence.js +65 -0
- package/src/posture/container-runtime.js +149 -0
- package/src/posture/counterfactual.js +109 -0
- package/src/posture/cross-lang-graphql.js +165 -0
- package/src/posture/cross-lang-grpc.js +166 -0
- package/src/posture/cross-lang-meta.js +101 -0
- package/src/posture/cross-lang-openapi.js +187 -0
- package/src/posture/cross-lang-orm.js +153 -0
- package/src/posture/cross-lang-queues.js +210 -0
- package/src/posture/crown-jewels.js +110 -0
- package/src/posture/custom-rules.js +361 -0
- package/src/posture/cve-alert-daemon.js +433 -0
- package/src/posture/cve-lookup.js +129 -0
- package/src/posture/dead-code.js +430 -0
- package/src/posture/defender-agent.js +158 -0
- package/src/posture/deploy-platform.js +204 -0
- package/src/posture/detector-fuzz.js +61 -0
- package/src/posture/deterministic.js +99 -0
- package/src/posture/drift.js +165 -0
- package/src/posture/epss.js +156 -0
- package/src/posture/exploitability-probability.js +212 -0
- package/src/posture/exploitability.js +121 -0
- package/src/posture/feature-flags.js +110 -0
- package/src/posture/finding-defaults.js +132 -0
- package/src/posture/fix-history.js +411 -0
- package/src/posture/fix-plan.js +121 -0
- package/src/posture/fix-verify-loop.js +157 -0
- package/src/posture/fix-verify.js +130 -0
- package/src/posture/flow-narration.js +105 -0
- package/src/posture/grader-calibration.js +156 -0
- package/src/posture/harness-discovery.js +113 -0
- package/src/posture/holdout-eval.js +144 -0
- package/src/posture/iac-reachability.js +163 -0
- package/src/posture/iam-policy.js +128 -0
- package/src/posture/integrity.js +97 -0
- package/src/posture/learning.js +166 -0
- package/src/posture/license-policy.js +109 -0
- package/src/posture/llm-redteam-prompts.js +418 -0
- package/src/posture/llm-redteam.js +303 -0
- package/src/posture/material-change.js +163 -0
- package/src/posture/mitigation-composite.js +55 -0
- package/src/posture/mttr.js +91 -0
- package/src/posture/network-policy-import.js +126 -0
- package/src/posture/path-predicates.js +99 -0
- package/src/posture/persona-prioritization.js +153 -0
- package/src/posture/poc-cwe-map.js +51 -0
- package/src/posture/poc-generator.js +500 -0
- package/src/posture/policy-gate.js +174 -0
- package/src/posture/pre-incident-archaeology.js +110 -0
- package/src/posture/profile.js +93 -0
- package/src/posture/reachability-filter.js +42 -0
- package/src/posture/regression-test-gen.js +200 -0
- package/src/posture/reverse-blast-radius.js +110 -0
- package/src/posture/router.js +109 -0
- package/src/posture/rule-overrides.js +198 -0
- package/src/posture/rule-pack-signing.js +209 -0
- package/src/posture/rule-packs.js +143 -0
- package/src/posture/rule-synthesis.js +108 -0
- package/src/posture/ruleset-version.js +71 -0
- package/src/posture/sbom.js +129 -0
- package/src/posture/schema-aware-bridge.js +207 -0
- package/src/posture/security-trend.js +87 -0
- package/src/posture/semantic-clone.js +114 -0
- package/src/posture/specification-mining.js +170 -0
- package/src/posture/stable-id.js +75 -0
- package/src/posture/stack-playbook.js +229 -0
- package/src/posture/streak.js +249 -0
- package/src/posture/suppressions.js +135 -0
- package/src/posture/telemetry-ingest.js +112 -0
- package/src/posture/threat-model.js +145 -0
- package/src/posture/three-agent-pipeline.js +74 -0
- package/src/posture/triage.js +146 -0
- package/src/posture/trust-boundary-diagram.js +115 -0
- package/src/posture/type-narrowing.js +129 -0
- package/src/posture/validator-metrics.js +179 -0
- package/src/posture/verifier-ephemeral.js +118 -0
- package/src/posture/verifier-target.js +147 -0
- package/src/posture/verifier.js +257 -0
- package/src/posture/version.js +75 -0
- package/src/posture/waf-ingest.js +200 -0
- package/src/posture/why-fired.js +141 -0
- package/src/pr-comment.js +172 -0
- package/src/pr-delta.js +198 -0
- package/src/report/.agentic-security/findings.json +79 -0
- package/src/report/.agentic-security/last-scan.json +79 -0
- package/src/report/.agentic-security/last-scan.json.sig +1 -0
- package/src/report/.agentic-security/scan-history.json +332 -0
- package/src/report/.agentic-security/streak.json +23 -0
- package/src/report/index.js +1136 -0
- package/src/report/mascot.js +42 -0
- package/src/runScan.js +141 -0
- package/src/sast/.agentic-security/findings.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json +5051 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +788 -0
- package/src/sast/.agentic-security/streak.json +23 -0
- package/src/sast/CLAUDE.md +39 -0
- package/src/sast/_comment-strip.js +46 -0
- package/src/sast/agent-tool-escalation.js +131 -0
- package/src/sast/auth-provider.js +171 -0
- package/src/sast/authz.js +236 -0
- package/src/sast/bench-shape/.agentic-security/findings.json +28 -0
- package/src/sast/bench-shape/.agentic-security/last-scan.json +28 -0
- package/src/sast/bench-shape/.agentic-security/scan-history.json +24 -0
- package/src/sast/bench-shape/.agentic-security/streak.json +22 -0
- package/src/sast/bench-shape/index.js +62 -0
- package/src/sast/claude-hook-injection.js +199 -0
- package/src/sast/claude-md-prompt-injection.js +170 -0
- package/src/sast/claude-settings.js +165 -0
- package/src/sast/client-side.js +149 -0
- package/src/sast/cpp-bench-extras.js +122 -0
- package/src/sast/cpp-dataflow.js +430 -0
- package/src/sast/cpp.js +248 -0
- package/src/sast/csharp.js +152 -0
- package/src/sast/csrf.js +82 -0
- package/src/sast/dart-flutter.js +173 -0
- package/src/sast/db-rls.js +147 -0
- package/src/sast/db-taint.js +215 -0
- package/src/sast/defi-deep.js +242 -0
- package/src/sast/deserialization-gadgets.js +113 -0
- package/src/sast/django-hardening.js +230 -0
- package/src/sast/env-hygiene.js +125 -0
- package/src/sast/fastapi-hardening.js +145 -0
- package/src/sast/go-extended.js +84 -0
- package/src/sast/host-header.js +106 -0
- package/src/sast/index.js +17 -0
- package/src/sast/java-ast-folding.js +561 -0
- package/src/sast/java-bench-extras.js +708 -0
- package/src/sast/java-collection-passthrough.js +178 -0
- package/src/sast/java-constant-fold.js +244 -0
- package/src/sast/java-deserialization.js +125 -0
- package/src/sast/jndi.js +104 -0
- package/src/sast/juliet-shape.js +324 -0
- package/src/sast/jwt-exp.js +104 -0
- package/src/sast/kotlin.js +82 -0
- package/src/sast/laravel-hardening.js +198 -0
- package/src/sast/ldap-injection.js +100 -0
- package/src/sast/llm-owasp.js +465 -0
- package/src/sast/llm-stored-prompt.js +103 -0
- package/src/sast/llm-trading-agent.js +161 -0
- package/src/sast/llm.js +308 -0
- package/src/sast/logic.js +140 -0
- package/src/sast/mass-assignment.js +101 -0
- package/src/sast/mcp-audit.js +242 -0
- package/src/sast/mobile-manifest.js +195 -0
- package/src/sast/model-load.js +164 -0
- package/src/sast/mutation-xss.js +87 -0
- package/src/sast/nosql-injection.js +82 -0
- package/src/sast/open-redirect.js +119 -0
- package/src/sast/php.js +91 -0
- package/src/sast/pipeline.js +122 -0
- package/src/sast/primary-cwe-java.js +155 -0
- package/src/sast/prompt-firewall.js +151 -0
- package/src/sast/prompt-template.js +157 -0
- package/src/sast/prototype-pollution.js +112 -0
- package/src/sast/python-sinks.js +195 -0
- package/src/sast/quarkus-hardening.js +102 -0
- package/src/sast/rag-poisoning.js +118 -0
- package/src/sast/rate-limit.js +128 -0
- package/src/sast/response-splitting.js +138 -0
- package/src/sast/ruby.js +108 -0
- package/src/sast/rust.js +105 -0
- package/src/sast/solidity.js +167 -0
- package/src/sast/springboot-hardening.js +186 -0
- package/src/sast/ssrf-cloud-metadata.js +80 -0
- package/src/sast/ssti.js +116 -0
- package/src/sast/swift.js +162 -0
- package/src/sast/toctou.js +95 -0
- package/src/sast/webhook.js +101 -0
- package/src/sast/xpath-injection.js +51 -0
- package/src/sast/xxe.js +140 -0
- package/src/sast/zip-slip.js +200 -0
- package/src/sca/base-images.json +45 -0
- package/src/sca/container.js +107 -0
- package/src/sca/dep-confusion.js +134 -0
- package/src/sca/index.js +6 -0
- package/src/sca/popular-packages.json +41 -0
- package/src/sca/sarif-ingest.js +187 -0
- package/src/sca/vuln-function-hints.json +89 -0
- package/src/secrets/index.js +4 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// Dead-code scanner — multi-language unused-code surface.
|
|
2
|
+
//
|
|
3
|
+
// Detects three categories of dead code:
|
|
4
|
+
// 1. unused-export — an `export function/const/class` with zero callers
|
|
5
|
+
// 2. unused-file — a file with zero inbound imports (excluding entry points)
|
|
6
|
+
// 3. wrapper-fn — a one-line function whose body is `return other(...args)`
|
|
7
|
+
//
|
|
8
|
+
// Per-language strategy:
|
|
9
|
+
// JS/TS — use the existing IR + callgraph (`src/ir/`) to compute callers
|
|
10
|
+
// per qid + dynamic-reference filter (string-literal grep,
|
|
11
|
+
// decorator usage, framework call sites).
|
|
12
|
+
// Python — shell out to `vulture` if installed; otherwise AST-based fallback.
|
|
13
|
+
// Go — shell out to `deadcode ./...` if installed.
|
|
14
|
+
// Rust — shell out to `cargo +nightly udeps` if installed.
|
|
15
|
+
//
|
|
16
|
+
// Output: standard finding shape with extras:
|
|
17
|
+
// {
|
|
18
|
+
// family: 'dead-code',
|
|
19
|
+
// kind: 'unused-export' | 'unused-file' | 'wrapper-fn',
|
|
20
|
+
// tier: 'safe' | 'caution' | 'danger',
|
|
21
|
+
// severity: 'info',
|
|
22
|
+
// stableId, confidence, exploitability, file, line, vuln, ...
|
|
23
|
+
// }
|
|
24
|
+
//
|
|
25
|
+
// Tier semantics:
|
|
26
|
+
// safe — internal / module-private symbol with no callers AND no
|
|
27
|
+
// dynamic-reference matches AND not a known framework callback.
|
|
28
|
+
// caution — public-API export OR matches one dynamic-reference signal
|
|
29
|
+
// (string-literal grep, decorator-style call). External
|
|
30
|
+
// consumers may exist.
|
|
31
|
+
// danger — entry points, exported class with subclasses, decorated
|
|
32
|
+
// with a framework decorator (@app.get, @Component, etc.),
|
|
33
|
+
// or referenced via reflection (`getattr`, `Reflect.get`).
|
|
34
|
+
//
|
|
35
|
+
// IMPORTANT: scanDeadCode is OFF by default — it runs in O(N²) on the file
|
|
36
|
+
// count (cross-file ref check). Opt in via opts.enabled or the slash
|
|
37
|
+
// command's explicit invocation.
|
|
38
|
+
|
|
39
|
+
import { execSync } from 'node:child_process';
|
|
40
|
+
import * as fs from 'node:fs';
|
|
41
|
+
import * as path from 'node:path';
|
|
42
|
+
|
|
43
|
+
const ENTRY_POINT_PATTERNS = [
|
|
44
|
+
/^bin\//, /^scripts\//, /(^|\/)cli\.[jt]s$/, /(^|\/)index\.[jt]s$/,
|
|
45
|
+
/(^|\/)main\.[jt]s$/, /\.test\.[jt]s$/, /\.spec\.[jt]s$/,
|
|
46
|
+
/(^|\/)conftest\.py$/, /(^|\/)__init__\.py$/, /(^|\/)manage\.py$/,
|
|
47
|
+
/(^|\/)main\.go$/, /(^|\/)main\.rs$/,
|
|
48
|
+
// npm-script-invoked entry points — bench runners, audit scripts, etc.
|
|
49
|
+
/(^|\/)bench(?:[.-][^/]+)?\.(?:m?js|ts)$/, /(^|\/)audit-[^/]+\.(?:m?js|ts)$/,
|
|
50
|
+
/^benchmark\//, /\/benchmark\//,
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const FRAMEWORK_DECORATORS = new Set([
|
|
54
|
+
// Python
|
|
55
|
+
'app.route', 'app.get', 'app.post', 'app.put', 'app.delete', 'app.patch',
|
|
56
|
+
'blueprint.route', 'router.get', 'router.post', 'router.put',
|
|
57
|
+
'app.task', 'celery.task', 'shared_task',
|
|
58
|
+
'pytest.fixture', 'pytest.mark.parametrize',
|
|
59
|
+
'click.command', 'click.group',
|
|
60
|
+
// JS/TS (decorator metadata)
|
|
61
|
+
'Component', 'Injectable', 'Module', 'Controller', 'Service',
|
|
62
|
+
'Get', 'Post', 'Put', 'Delete', 'Patch',
|
|
63
|
+
// Java/Spring (already caught via java IR but listed for completeness)
|
|
64
|
+
'GetMapping', 'PostMapping', 'PutMapping', 'DeleteMapping', 'RequestMapping',
|
|
65
|
+
'Bean', 'Component', 'Service', 'Repository', 'Controller', 'Configuration',
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const DYNAMIC_REFERENCE_PATTERNS = [
|
|
69
|
+
// String-literal match — the symbol name appears as a JS/Python string
|
|
70
|
+
// literal (often a router key, an event name, or a getattr target).
|
|
71
|
+
(name) => new RegExp(`['"\`]${escapeRegex(name)}['"\`]`),
|
|
72
|
+
// Reflect / getattr — JS Reflect.get / Python getattr / Object[key]
|
|
73
|
+
(name) => new RegExp(`(getattr|Reflect\\.(get|has)|\\["${escapeRegex(name)}"\\])`),
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function escapeRegex(s) {
|
|
77
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isEntryPoint(relPath) {
|
|
81
|
+
return ENTRY_POINT_PATTERNS.some((re) => re.test(relPath));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Filter candidate-dead symbols by checking for dynamic references.
|
|
86
|
+
* candidates: [{ name, file, line, kind }]
|
|
87
|
+
* allFiles: Map<relPath, content>
|
|
88
|
+
*
|
|
89
|
+
* Returns the same shape with:
|
|
90
|
+
* - removed: candidates that match a dynamic-reference signal (kept out)
|
|
91
|
+
* - kept: candidates we still believe are dead
|
|
92
|
+
* - tierHints: Map<idKey, 'caution'|'danger'>
|
|
93
|
+
*/
|
|
94
|
+
export function filterDynamicReferences(candidates, allFiles) {
|
|
95
|
+
const tierHints = new Map();
|
|
96
|
+
const kept = [];
|
|
97
|
+
const removed = [];
|
|
98
|
+
for (const c of candidates || []) {
|
|
99
|
+
let dynamicMatch = false;
|
|
100
|
+
let frameworkMatch = false;
|
|
101
|
+
for (const [fp, content] of allFiles) {
|
|
102
|
+
// Skip the file where the symbol is declared — self-references don't
|
|
103
|
+
// prove external use.
|
|
104
|
+
if (fp === c.file) continue;
|
|
105
|
+
for (const buildRe of DYNAMIC_REFERENCE_PATTERNS) {
|
|
106
|
+
if (buildRe(c.name).test(content)) { dynamicMatch = true; break; }
|
|
107
|
+
}
|
|
108
|
+
if (dynamicMatch) break;
|
|
109
|
+
}
|
|
110
|
+
// Framework decorator check — any line `@<deco>` immediately above the
|
|
111
|
+
// declaration in the symbol's own file → danger tier (framework calls it).
|
|
112
|
+
const ownContent = allFiles.get(c.file);
|
|
113
|
+
if (ownContent && c.line) {
|
|
114
|
+
const lines = ownContent.split('\n');
|
|
115
|
+
const above = lines[c.line - 2] || '';
|
|
116
|
+
const m = above.match(/^\s*@([\w.]+)/);
|
|
117
|
+
if (m && FRAMEWORK_DECORATORS.has(m[1])) frameworkMatch = true;
|
|
118
|
+
}
|
|
119
|
+
if (frameworkMatch) {
|
|
120
|
+
tierHints.set(c.key || `${c.file}::${c.name}`, 'danger');
|
|
121
|
+
removed.push({ ...c, reason: 'framework-decorator' });
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (dynamicMatch) {
|
|
125
|
+
tierHints.set(c.key || `${c.file}::${c.name}`, 'caution');
|
|
126
|
+
kept.push({ ...c, tierHint: 'caution', reason: 'dynamic-reference-match' });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
kept.push({ ...c, tierHint: 'safe' });
|
|
130
|
+
}
|
|
131
|
+
return { kept, removed, tierHints };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Detect unused JS/TS exports + unused files using the existing IR
|
|
136
|
+
* call graph as ground truth.
|
|
137
|
+
*
|
|
138
|
+
* projectRoot: absolute path
|
|
139
|
+
* fileContents: Map<relPath, content> (JS/TS files only)
|
|
140
|
+
* callgraph: output of buildCallGraph(perFileIR)
|
|
141
|
+
*
|
|
142
|
+
* Returns a list of dead-code findings.
|
|
143
|
+
*/
|
|
144
|
+
export function detectDeadJsTs(projectRoot, fileContents, callgraph) {
|
|
145
|
+
if (!fileContents || !callgraph) return [];
|
|
146
|
+
const findings = [];
|
|
147
|
+
// 1. unused-export: every fn qid with no callers AND has an exported name
|
|
148
|
+
// + no entry-point file context.
|
|
149
|
+
for (const [qid, fn] of callgraph.functions) {
|
|
150
|
+
if (!fn.exported) continue;
|
|
151
|
+
const callers = callgraph.callersOf.get(qid) || [];
|
|
152
|
+
if (callers.length > 0) continue;
|
|
153
|
+
if (isEntryPoint(fn.file)) continue;
|
|
154
|
+
findings.push({
|
|
155
|
+
family: 'dead-code',
|
|
156
|
+
kind: 'unused-export',
|
|
157
|
+
severity: 'info',
|
|
158
|
+
file: fn.file,
|
|
159
|
+
line: fn.line || 1,
|
|
160
|
+
name: fn.name,
|
|
161
|
+
key: `${fn.file}::${fn.name}`,
|
|
162
|
+
tierHint: 'safe',
|
|
163
|
+
vuln: 'Unused export',
|
|
164
|
+
description: `\`${fn.name}\` is exported from ${fn.file} but has no internal callers.`,
|
|
165
|
+
remediation: `Remove the export or wire it into a call site. If it is a public API, allowlist it.`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// 2. unused-file: file has no inbound textual imports from any other file.
|
|
169
|
+
// Textual import scan catches namespace imports (`import * as N from ...`)
|
|
170
|
+
// and side-effect imports (`import './foo';`) that the callgraph misses.
|
|
171
|
+
const fileHasInbound = new Map();
|
|
172
|
+
for (const f of fileContents.keys()) fileHasInbound.set(f, false);
|
|
173
|
+
for (const [importerFp, content] of fileContents) {
|
|
174
|
+
for (const m of String(content).matchAll(/(?:from\s+|import\s*\(\s*)['"]([^'"]+)['"]/g)) {
|
|
175
|
+
const spec = m[1];
|
|
176
|
+
if (!spec) continue;
|
|
177
|
+
if (spec.startsWith('.')) {
|
|
178
|
+
// Relative import — resolve to a candidate path within fileContents.
|
|
179
|
+
const importerDir = path.dirname(importerFp);
|
|
180
|
+
const resolved = path.normalize(path.join(importerDir, spec));
|
|
181
|
+
// Try as-is, with .js, /index.js, etc.
|
|
182
|
+
for (const cand of [resolved, `${resolved}.js`, `${resolved}.ts`, `${resolved}.mjs`,
|
|
183
|
+
`${resolved}/index.js`, `${resolved}/index.ts`]) {
|
|
184
|
+
if (fileHasInbound.has(cand)) { fileHasInbound.set(cand, true); break; }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const [fp, hasIn] of fileHasInbound) {
|
|
190
|
+
if (hasIn) continue;
|
|
191
|
+
if (isEntryPoint(fp)) continue;
|
|
192
|
+
// Skip if the file declares zero functions — likely a constants module.
|
|
193
|
+
let hasFn = false;
|
|
194
|
+
for (const fn of callgraph.functions.values()) if (fn.file === fp) { hasFn = true; break; }
|
|
195
|
+
if (!hasFn) continue;
|
|
196
|
+
findings.push({
|
|
197
|
+
family: 'dead-code',
|
|
198
|
+
kind: 'unused-file',
|
|
199
|
+
severity: 'info',
|
|
200
|
+
file: fp,
|
|
201
|
+
line: 1,
|
|
202
|
+
name: path.basename(fp),
|
|
203
|
+
key: `${fp}::__file__`,
|
|
204
|
+
tierHint: 'caution',
|
|
205
|
+
vuln: 'Unused file',
|
|
206
|
+
description: `No file in the project imports ${fp}.`,
|
|
207
|
+
remediation: `Delete the file, or verify it is loaded dynamically (e.g., via a glob or a registry).`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return findings;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Detect wrapper functions whose body is essentially `return other(...args)`.
|
|
215
|
+
* These add no semantic value and are usually leftover indirection from
|
|
216
|
+
* refactors. Operates per-file on JS/TS using the IR.
|
|
217
|
+
*/
|
|
218
|
+
export function detectWrapperFns(callgraph) {
|
|
219
|
+
if (!callgraph || !callgraph.functions) return [];
|
|
220
|
+
const out = [];
|
|
221
|
+
for (const fn of callgraph.functions.values()) {
|
|
222
|
+
if (!fn.body) continue;
|
|
223
|
+
// Conservative: body is a single expression-statement whose value is
|
|
224
|
+
// a call passing the parameters straight through (in order).
|
|
225
|
+
const body = fn.body.trim();
|
|
226
|
+
if (body.length > 120) continue;
|
|
227
|
+
const m = body.match(/^(?:return\s+)?(\w+(?:\.\w+)*)\s*\(\s*([^)]*)\s*\)\s*;?$/);
|
|
228
|
+
if (!m) continue;
|
|
229
|
+
const calleeName = m[1];
|
|
230
|
+
const args = m[2].split(',').map((s) => s.trim()).filter(Boolean);
|
|
231
|
+
const params = (fn.params || []).map((p) => p.name || p);
|
|
232
|
+
if (args.length !== params.length) continue;
|
|
233
|
+
const passThrough = args.every((a, i) => a === params[i]);
|
|
234
|
+
if (!passThrough) continue;
|
|
235
|
+
out.push({
|
|
236
|
+
family: 'dead-code',
|
|
237
|
+
kind: 'wrapper-fn',
|
|
238
|
+
severity: 'info',
|
|
239
|
+
file: fn.file,
|
|
240
|
+
line: fn.line || 1,
|
|
241
|
+
name: fn.name,
|
|
242
|
+
key: `${fn.file}::${fn.name}::wrapper`,
|
|
243
|
+
tierHint: 'caution',
|
|
244
|
+
vuln: 'Wrapper function',
|
|
245
|
+
description: `\`${fn.name}\` only forwards its arguments to \`${calleeName}\`. The indirection adds no semantic value.`,
|
|
246
|
+
remediation: `Inline \`${calleeName}\` at the call sites and delete \`${fn.name}\`.`,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Shell out to a language-native dead-code tool. Returns an array of
|
|
254
|
+
* findings in our schema, or [] if the tool is not installed.
|
|
255
|
+
*
|
|
256
|
+
* tool: 'vulture' | 'deadcode' | 'cargo-udeps'
|
|
257
|
+
* cwd: project root
|
|
258
|
+
*/
|
|
259
|
+
export function runExternalDeadCodeTool(tool, cwd) {
|
|
260
|
+
try {
|
|
261
|
+
switch (tool) {
|
|
262
|
+
case 'vulture': {
|
|
263
|
+
// vulture emits "<file>:<line>: unused <kind> '<name>' (60% confidence)"
|
|
264
|
+
const out = execSync('vulture --min-confidence 60 .', {
|
|
265
|
+
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 60_000,
|
|
266
|
+
});
|
|
267
|
+
return parseVultureOutput(out, cwd);
|
|
268
|
+
}
|
|
269
|
+
case 'deadcode': {
|
|
270
|
+
const out = execSync('deadcode ./...', {
|
|
271
|
+
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 60_000,
|
|
272
|
+
});
|
|
273
|
+
return parseGoDeadcodeOutput(out, cwd);
|
|
274
|
+
}
|
|
275
|
+
case 'cargo-udeps': {
|
|
276
|
+
const out = execSync('cargo +nightly udeps --output json', {
|
|
277
|
+
cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 120_000,
|
|
278
|
+
});
|
|
279
|
+
return parseCargoUdepsOutput(out, cwd);
|
|
280
|
+
}
|
|
281
|
+
default: return [];
|
|
282
|
+
}
|
|
283
|
+
} catch (_e) {
|
|
284
|
+
// Tool not installed or returned non-zero. Silent — caller decides
|
|
285
|
+
// whether to surface that as a warning.
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function parseVultureOutput(text, cwd) {
|
|
291
|
+
const findings = [];
|
|
292
|
+
for (const line of String(text || '').split('\n')) {
|
|
293
|
+
const m = line.match(/^(.+?):(\d+):\s*unused\s+(function|variable|class|method|attribute|import|property)\s+'([^']+)'\s+\((\d+)%\s+confidence\)/);
|
|
294
|
+
if (!m) continue;
|
|
295
|
+
const [, file, lineNo, kind, name, conf] = m;
|
|
296
|
+
const confidence = Math.min(0.95, parseInt(conf, 10) / 100);
|
|
297
|
+
findings.push({
|
|
298
|
+
family: 'dead-code',
|
|
299
|
+
kind: `unused-${kind}`,
|
|
300
|
+
severity: 'info',
|
|
301
|
+
file: path.relative(cwd, file),
|
|
302
|
+
line: parseInt(lineNo, 10),
|
|
303
|
+
name,
|
|
304
|
+
key: `${file}::${name}`,
|
|
305
|
+
tierHint: confidence >= 0.8 ? 'safe' : 'caution',
|
|
306
|
+
confidence,
|
|
307
|
+
vuln: `Unused ${kind}`,
|
|
308
|
+
description: `vulture reports \`${name}\` as unused (${conf}% confidence).`,
|
|
309
|
+
remediation: `Delete \`${name}\` or annotate with # noqa if it is intentionally retained.`,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return findings;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function parseGoDeadcodeOutput(text, cwd) {
|
|
316
|
+
const findings = [];
|
|
317
|
+
for (const line of String(text || '').split('\n')) {
|
|
318
|
+
const m = line.match(/^(.+?):(\d+):(\d+):\s*(.+)$/);
|
|
319
|
+
if (!m) continue;
|
|
320
|
+
const [, file, lineNo, , msg] = m;
|
|
321
|
+
const nameMatch = msg.match(/^(?:unreachable\s+)?(?:function|method)\s+([^\s]+)/i);
|
|
322
|
+
findings.push({
|
|
323
|
+
family: 'dead-code',
|
|
324
|
+
kind: 'unused-function',
|
|
325
|
+
severity: 'info',
|
|
326
|
+
file: path.relative(cwd, file),
|
|
327
|
+
line: parseInt(lineNo, 10),
|
|
328
|
+
name: nameMatch ? nameMatch[1] : msg,
|
|
329
|
+
key: `${file}::${nameMatch ? nameMatch[1] : msg}`,
|
|
330
|
+
tierHint: 'safe',
|
|
331
|
+
vuln: 'Unused Go declaration',
|
|
332
|
+
description: msg,
|
|
333
|
+
remediation: `Delete the declaration or wire it into a call site.`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
return findings;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function parseCargoUdepsOutput(jsonText, cwd) {
|
|
340
|
+
try {
|
|
341
|
+
const obj = JSON.parse(jsonText);
|
|
342
|
+
const findings = [];
|
|
343
|
+
const unused = (obj?.unused_deps && Object.values(obj.unused_deps)) || [];
|
|
344
|
+
for (const pkg of unused) {
|
|
345
|
+
const deps = [].concat(pkg.normal || [], pkg.development || [], pkg.build || []);
|
|
346
|
+
for (const d of deps) {
|
|
347
|
+
findings.push({
|
|
348
|
+
family: 'dead-code',
|
|
349
|
+
kind: 'unused-dependency',
|
|
350
|
+
severity: 'info',
|
|
351
|
+
file: 'Cargo.toml',
|
|
352
|
+
line: 1,
|
|
353
|
+
name: d,
|
|
354
|
+
key: `cargo::${d}`,
|
|
355
|
+
tierHint: 'safe',
|
|
356
|
+
vuln: 'Unused Cargo dependency',
|
|
357
|
+
description: `\`${d}\` is declared in Cargo.toml but never used.`,
|
|
358
|
+
remediation: `cargo remove ${d}`,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return findings;
|
|
363
|
+
} catch (_e) {
|
|
364
|
+
return [];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Categorize a dead-code finding into a risk tier based on:
|
|
370
|
+
* - tierHint (from detector)
|
|
371
|
+
* - file context (entry point → danger)
|
|
372
|
+
* - export visibility (re-exported from index.js → caution)
|
|
373
|
+
*
|
|
374
|
+
* Tier:
|
|
375
|
+
* safe — clear to delete
|
|
376
|
+
* caution — needs human review (public API, dynamic-ref signal)
|
|
377
|
+
* danger — do not delete from this run
|
|
378
|
+
*/
|
|
379
|
+
export function classifyTier(finding, ctx = {}) {
|
|
380
|
+
if (!finding) return 'caution';
|
|
381
|
+
if (isEntryPoint(finding.file)) return 'danger';
|
|
382
|
+
if (finding.tierHint) return finding.tierHint;
|
|
383
|
+
return 'caution';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Master entrypoint. Returns dead-code findings across every supported
|
|
388
|
+
* language for the project.
|
|
389
|
+
*
|
|
390
|
+
* opts.languages? — restrict to a subset ['js','ts','py','go','rust']
|
|
391
|
+
* opts.skipDynamicCheck? — disable dynamic-reference filter (faster, more FPs)
|
|
392
|
+
* opts.callgraph? — pre-built JS/TS call graph (avoid rebuild)
|
|
393
|
+
* opts.fileContents? — Map<relPath, content> for the dynamic-ref filter
|
|
394
|
+
*/
|
|
395
|
+
export function scanDeadCode(projectRoot, opts = {}) {
|
|
396
|
+
const findings = [];
|
|
397
|
+
const langs = new Set(opts.languages || ['js', 'ts', 'py', 'go', 'rust']);
|
|
398
|
+
|
|
399
|
+
// JS/TS — IR-based (requires callgraph + file contents to be passed in)
|
|
400
|
+
if ((langs.has('js') || langs.has('ts')) && opts.callgraph && opts.fileContents) {
|
|
401
|
+
let jsFindings = [];
|
|
402
|
+
jsFindings = jsFindings.concat(detectDeadJsTs(projectRoot, opts.fileContents, opts.callgraph));
|
|
403
|
+
jsFindings = jsFindings.concat(detectWrapperFns(opts.callgraph));
|
|
404
|
+
if (!opts.skipDynamicCheck) {
|
|
405
|
+
const { kept } = filterDynamicReferences(jsFindings, opts.fileContents);
|
|
406
|
+
jsFindings = kept;
|
|
407
|
+
}
|
|
408
|
+
findings.push(...jsFindings);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// External tools (best-effort, silent on absence).
|
|
412
|
+
if (langs.has('py')) findings.push(...runExternalDeadCodeTool('vulture', projectRoot));
|
|
413
|
+
if (langs.has('go')) findings.push(...runExternalDeadCodeTool('deadcode', projectRoot));
|
|
414
|
+
if (langs.has('rust')) findings.push(...runExternalDeadCodeTool('cargo-udeps', projectRoot));
|
|
415
|
+
|
|
416
|
+
// Final tier classification.
|
|
417
|
+
for (const f of findings) f.tier = classifyTier(f);
|
|
418
|
+
|
|
419
|
+
return findings;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Bucket findings by tier — convenient for the apply path. */
|
|
423
|
+
export function groupByTier(findings) {
|
|
424
|
+
const out = { safe: [], caution: [], danger: [] };
|
|
425
|
+
for (const f of findings || []) {
|
|
426
|
+
const t = f.tier || classifyTier(f);
|
|
427
|
+
(out[t] || out.caution).push(f);
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Blue-team / defender agent — Phase 2 of the three-agent review pipeline.
|
|
2
|
+
//
|
|
3
|
+
// Reads the red team's attack transcript (from adversary-agent.js) and
|
|
4
|
+
// proposes hardening: which controls would have blocked each tool call,
|
|
5
|
+
// what code changes mitigate the chain, what runtime guard would have
|
|
6
|
+
// fired, and which deferred items can be closed under the current threat
|
|
7
|
+
// posture.
|
|
8
|
+
//
|
|
9
|
+
// Interface mirrors adversary-agent.js — bounded LLM invocations, ACL'd
|
|
10
|
+
// tool set (read-only this time: no http.post, no db writes), hash-chained
|
|
11
|
+
// transcript. Without a configured LLM endpoint, runDefender short-circuits
|
|
12
|
+
// to a structured "no-llm-endpoint" output that still includes the static
|
|
13
|
+
// hardening recommendations derived from the attack transcript.
|
|
14
|
+
|
|
15
|
+
import * as crypto from 'node:crypto';
|
|
16
|
+
|
|
17
|
+
const TOOL_ACL = new Set([
|
|
18
|
+
'read_finding',
|
|
19
|
+
'read_control_inventory',
|
|
20
|
+
'recommend_hardening',
|
|
21
|
+
'record_defense',
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
const STATIC_HARDENING_BY_FAMILY = {
|
|
25
|
+
'sql-injection': [
|
|
26
|
+
'Switch to parameterized queries (placeholder via ? or $1) — never concatenate user-controlled strings into SQL.',
|
|
27
|
+
'Add a runtime WAF rule for SQLi shape at the edge.',
|
|
28
|
+
'Add a per-request audit log entry that includes the bound parameter set.',
|
|
29
|
+
],
|
|
30
|
+
'command-injection': [
|
|
31
|
+
'Replace exec/system with spawn/execFile passing argv as an array.',
|
|
32
|
+
'Add an allow-list of permitted commands; reject any value containing `;` `|` `&` ``` $`.',
|
|
33
|
+
'Run the receiving service under a non-root user with a sealed PATH.',
|
|
34
|
+
],
|
|
35
|
+
'xss': [
|
|
36
|
+
'Output-encode at the sink — DOMPurify or templating-engine auto-escape.',
|
|
37
|
+
'Set Content-Security-Policy headers with a strict default-src.',
|
|
38
|
+
'For React: never call dangerouslySetInnerHTML on user input.',
|
|
39
|
+
],
|
|
40
|
+
'ssrf': [
|
|
41
|
+
'Resolve user-supplied URLs to an IP and reject 169.254.169.254, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7.',
|
|
42
|
+
'Pin the resolver before the HTTP client uses it (TOCTOU window).',
|
|
43
|
+
'Block the cloud-metadata IP at the egress network policy layer.',
|
|
44
|
+
],
|
|
45
|
+
'idor': [
|
|
46
|
+
'Compare the requested resource owner to req.user.id on every read.',
|
|
47
|
+
'Use an ORM scope that filters by tenantId/orgId at the query level — not at the controller.',
|
|
48
|
+
'Add an integration test that authenticates as user A and tries to read user B\'s resource.',
|
|
49
|
+
],
|
|
50
|
+
'broken-auth': [
|
|
51
|
+
'Enforce JWT algorithm allow-list (HS256 OR RS256, never `none`).',
|
|
52
|
+
'Verify signature BEFORE reading claims.',
|
|
53
|
+
'Rotate the signing key on a schedule and revoke old keys.',
|
|
54
|
+
],
|
|
55
|
+
'hardcoded-secret': [
|
|
56
|
+
'Rotate the leaked credential immediately.',
|
|
57
|
+
'Move all secrets to a vault and reference via env-var.',
|
|
58
|
+
'Add a pre-commit hook that runs `/scan --secrets` on the diff.',
|
|
59
|
+
],
|
|
60
|
+
'hook-command-injection': [
|
|
61
|
+
'Pass agent-controlled values via stdin or a sandboxed env var, never shell-interpolation.',
|
|
62
|
+
'Wrap the receiving program in single-quotes if the value must appear on the command line.',
|
|
63
|
+
'Validate the value against a strict allow-list before the hook runs.',
|
|
64
|
+
],
|
|
65
|
+
'harness-config-permissions': [
|
|
66
|
+
'Replace wildcard rules (Bash(*), *) with scoped allow-list entries.',
|
|
67
|
+
'Add a deny-list with at least: Bash(rm -rf *), Bash(curl * | sh), Bash(sudo *), Bash(git push --force origin main).',
|
|
68
|
+
'Remove dangerouslySkipPermissions / bypassAll / autoApprove flags.',
|
|
69
|
+
],
|
|
70
|
+
'agent-prompt-injection': [
|
|
71
|
+
'Quarantine instruction files (CLAUDE.md, AGENTS.md) sourced from untrusted origin.',
|
|
72
|
+
'Remove override / role-rewriting directives.',
|
|
73
|
+
'Audit instruction files in CI with /scan --harness on every PR.',
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function _familyOf(f) {
|
|
78
|
+
if (!f) return null;
|
|
79
|
+
if (f.family) return String(f.family).toLowerCase();
|
|
80
|
+
const v = (f.vuln || '').toLowerCase();
|
|
81
|
+
if (/sql.*injection/.test(v)) return 'sql-injection';
|
|
82
|
+
if (/command.*injection/.test(v)) return 'command-injection';
|
|
83
|
+
if (/xss/.test(v)) return 'xss';
|
|
84
|
+
if (/ssrf/.test(v)) return 'ssrf';
|
|
85
|
+
if (/idor/.test(v)) return 'idor';
|
|
86
|
+
if (/broken.auth|jwt/.test(v)) return 'broken-auth';
|
|
87
|
+
if (/hardcoded/.test(v)) return 'hardcoded-secret';
|
|
88
|
+
if (/hook.*command/.test(v)) return 'hook-command-injection';
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function chainHash(prev, entry) {
|
|
93
|
+
const h = crypto.createHash('sha256');
|
|
94
|
+
h.update(prev || '');
|
|
95
|
+
h.update(JSON.stringify(entry));
|
|
96
|
+
return h.digest('hex').slice(0, 16);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function staticHardeningFor(finding) {
|
|
100
|
+
const fam = _familyOf(finding);
|
|
101
|
+
if (!fam) return [];
|
|
102
|
+
return STATIC_HARDENING_BY_FAMILY[fam] || [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function startDefenderTranscript(finding, redTeamTranscript) {
|
|
106
|
+
const seed = {
|
|
107
|
+
seedFinding: {
|
|
108
|
+
stableId: finding?.stableId || null,
|
|
109
|
+
file: finding?.file || null,
|
|
110
|
+
line: finding?.line || null,
|
|
111
|
+
vuln: finding?.vuln || null,
|
|
112
|
+
family: finding?.family || null,
|
|
113
|
+
},
|
|
114
|
+
redOutcome: redTeamTranscript?.outcome || null,
|
|
115
|
+
redCallCount: (redTeamTranscript?.entries || []).filter(e => e.tool).length,
|
|
116
|
+
startedAt: new Date().toISOString(),
|
|
117
|
+
entries: [],
|
|
118
|
+
chainHead: '',
|
|
119
|
+
};
|
|
120
|
+
seed.chainHead = chainHash('', seed.seedFinding);
|
|
121
|
+
return seed;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function appendDefenderEntry(transcript, entry) {
|
|
125
|
+
if (!transcript || !entry) return;
|
|
126
|
+
if (entry.tool && !TOOL_ACL.has(entry.tool)) {
|
|
127
|
+
entry = { ...entry, refused: true, refusedReason: `tool '${entry.tool}' not in defender ACL` };
|
|
128
|
+
}
|
|
129
|
+
transcript.chainHead = chainHash(transcript.chainHead, entry);
|
|
130
|
+
transcript.entries.push({ ...entry, hash: transcript.chainHead });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Run defender on a finding + the red team's transcript. Without an LLM
|
|
134
|
+
// endpoint, returns the static-hardening list (still useful — it's the
|
|
135
|
+
// minimum baseline guidance).
|
|
136
|
+
export async function runDefender(finding, redTeamTranscript, opts = {}) {
|
|
137
|
+
const transcript = startDefenderTranscript(finding, redTeamTranscript);
|
|
138
|
+
const staticAdvice = staticHardeningFor(finding);
|
|
139
|
+
appendDefenderEntry(transcript, {
|
|
140
|
+
phase: 'static-analysis',
|
|
141
|
+
family: _familyOf(finding),
|
|
142
|
+
recommendations: staticAdvice,
|
|
143
|
+
});
|
|
144
|
+
if (typeof opts.llmInvoke !== 'function' || !process.env.AGENTIC_SECURITY_LLM_ENDPOINT) {
|
|
145
|
+
appendDefenderEntry(transcript, { phase: 'init', reason: 'no llmInvoke supplied / AGENTIC_SECURITY_LLM_ENDPOINT not set — static hardening only' });
|
|
146
|
+
return { transcript, recommendations: staticAdvice, mode: 'static-only' };
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const llmRec = await opts.llmInvoke(transcript);
|
|
150
|
+
appendDefenderEntry(transcript, { phase: 'llm-defense', recommendations: llmRec });
|
|
151
|
+
return { transcript, recommendations: [...staticAdvice, ...(Array.isArray(llmRec) ? llmRec : [])], mode: 'llm-augmented' };
|
|
152
|
+
} catch (e) {
|
|
153
|
+
appendDefenderEntry(transcript, { phase: 'llm-error', error: String(e?.message || e) });
|
|
154
|
+
return { transcript, recommendations: staticAdvice, mode: 'static-fallback' };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export { TOOL_ACL };
|