@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,149 @@
|
|
|
1
|
+
// Container runtime config audit (FR-XSAT-8).
|
|
2
|
+
//
|
|
3
|
+
// Flags dangerous combinations in Dockerfile, Kubernetes manifests, and
|
|
4
|
+
// ECS task definitions. Each rule names one specific misconfiguration; we
|
|
5
|
+
// stay narrow (high precision) and let the curated list grow over time.
|
|
6
|
+
//
|
|
7
|
+
// Coverage:
|
|
8
|
+
// - Dockerfile USER root, no USER directive, ADD with URL,
|
|
9
|
+
// --privileged in HEALTHCHECK
|
|
10
|
+
// - Kubernetes manifest privileged: true, hostNetwork: true,
|
|
11
|
+
// hostPID: true, runAsUser: 0, allowPrivilegeEscalation: true,
|
|
12
|
+
// bind-mount of /var/run/docker.sock, capabilities ALL/SYS_ADMIN
|
|
13
|
+
// - ECS task definition privileged: true, host network mode, root user
|
|
14
|
+
|
|
15
|
+
const DOCKERFILE_NAME_RE = /(?:^|\/)Dockerfile(?:\.[\w.-]+)?$/i;
|
|
16
|
+
const K8S_YAML_RE = /\.(?:ya?ml)$/i;
|
|
17
|
+
const ECS_TASK_DEF_RE = /(?:^|\/)task[-_]?definition[s]?(?:[-_.][\w-]*)?\.json$/i;
|
|
18
|
+
|
|
19
|
+
function _lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
|
|
20
|
+
|
|
21
|
+
// ─── Dockerfile rules ──────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const DOCKERFILE_RULES = [
|
|
24
|
+
{
|
|
25
|
+
re: /^\s*ADD\s+(?:https?:\/\/|ftp:\/\/)/gmi,
|
|
26
|
+
vuln: 'Dockerfile ADD with remote URL (TLS/MITM exposure; prefer COPY + verified checksum)',
|
|
27
|
+
severity: 'medium', cwe: 'CWE-494', family: 'container-runtime',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
re: /^\s*USER\s+root\b/gmi,
|
|
31
|
+
vuln: 'Dockerfile USER root (container runs with root privileges)',
|
|
32
|
+
severity: 'high', cwe: 'CWE-250', family: 'container-runtime',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
re: /--privileged\b/gi,
|
|
36
|
+
vuln: 'Dockerfile flag --privileged (full host privileges in container)',
|
|
37
|
+
severity: 'critical', cwe: 'CWE-250', family: 'container-runtime',
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
function scanDockerfile(file, raw) {
|
|
42
|
+
const findings = [];
|
|
43
|
+
for (const rule of DOCKERFILE_RULES) {
|
|
44
|
+
const r = new RegExp(rule.re.source, rule.re.flags);
|
|
45
|
+
let m;
|
|
46
|
+
while ((m = r.exec(raw))) {
|
|
47
|
+
findings.push(_finding(rule, file, _lineOf(raw, m.index), m[0]));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Special check: no USER directive at all.
|
|
51
|
+
if (!/^\s*USER\s+/mi.test(raw)) {
|
|
52
|
+
findings.push(_finding(
|
|
53
|
+
{ vuln: 'Dockerfile has no USER directive (defaults to root)', severity: 'medium', cwe: 'CWE-250', family: 'container-runtime' },
|
|
54
|
+
file, 1, '(no USER directive present)'
|
|
55
|
+
));
|
|
56
|
+
}
|
|
57
|
+
return findings;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Kubernetes manifest rules ────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
const K8S_RULES = [
|
|
63
|
+
{ re: /\bprivileged\s*:\s*true\b/gi, vuln: 'K8s container privileged: true', severity: 'critical', cwe: 'CWE-250' },
|
|
64
|
+
{ re: /\bhostNetwork\s*:\s*true\b/gi, vuln: 'K8s pod hostNetwork: true (host network namespace)', severity: 'high', cwe: 'CWE-732' },
|
|
65
|
+
{ re: /\bhostPID\s*:\s*true\b/gi, vuln: 'K8s pod hostPID: true (host PID namespace)', severity: 'high', cwe: 'CWE-732' },
|
|
66
|
+
{ re: /\bhostIPC\s*:\s*true\b/gi, vuln: 'K8s pod hostIPC: true (host IPC namespace)', severity: 'high', cwe: 'CWE-732' },
|
|
67
|
+
{ re: /\ballowPrivilegeEscalation\s*:\s*true\b/gi, vuln: 'K8s container allowPrivilegeEscalation: true', severity: 'high', cwe: 'CWE-250' },
|
|
68
|
+
{ re: /\brunAsUser\s*:\s*0\b/gi, vuln: 'K8s container runAsUser: 0 (root UID)', severity: 'high', cwe: 'CWE-250' },
|
|
69
|
+
// capabilities: add: [ALL] or SYS_ADMIN
|
|
70
|
+
{ re: /\badd\s*:\s*\[\s*['"]?ALL['"]?/gi, vuln: 'K8s securityContext capabilities.add: ALL', severity: 'critical', cwe: 'CWE-250' },
|
|
71
|
+
{ re: /\badd\s*:\s*\[\s*['"]?SYS_ADMIN['"]?/gi, vuln: 'K8s securityContext capabilities.add: SYS_ADMIN', severity: 'high', cwe: 'CWE-250' },
|
|
72
|
+
// docker.sock bind-mount
|
|
73
|
+
{ re: /\bpath\s*:\s*['"]?\/var\/run\/docker\.sock/gi, vuln: 'K8s hostPath /var/run/docker.sock (container escape primitive)', severity: 'critical', cwe: 'CWE-250' },
|
|
74
|
+
// readOnlyRootFilesystem: false (or missing — only checked when present and false)
|
|
75
|
+
{ re: /\breadOnlyRootFilesystem\s*:\s*false\b/gi, vuln: 'K8s readOnlyRootFilesystem: false (writable root FS)', severity: 'low', cwe: 'CWE-732' },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
function scanK8sYaml(file, raw) {
|
|
79
|
+
// Heuristic: looks like a k8s manifest if it has apiVersion + kind.
|
|
80
|
+
if (!/\bapiVersion\s*:/.test(raw) || !/\bkind\s*:/.test(raw)) return [];
|
|
81
|
+
const findings = [];
|
|
82
|
+
for (const rule of K8S_RULES) {
|
|
83
|
+
const r = new RegExp(rule.re.source, rule.re.flags);
|
|
84
|
+
let m;
|
|
85
|
+
while ((m = r.exec(raw))) {
|
|
86
|
+
findings.push(_finding({ ...rule, family: 'container-runtime' }, file, _lineOf(raw, m.index), m[0]));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return findings;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── ECS task definition rules ────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
const ECS_RULES = [
|
|
95
|
+
{ re: /"privileged"\s*:\s*true\b/g, vuln: 'ECS container privileged: true', severity: 'critical', cwe: 'CWE-250' },
|
|
96
|
+
{ re: /"networkMode"\s*:\s*"host"\b/g, vuln: 'ECS task networkMode: host (host network namespace)', severity: 'high', cwe: 'CWE-732' },
|
|
97
|
+
{ re: /"user"\s*:\s*"(?:root|0)"/g, vuln: 'ECS container user: root/0', severity: 'high', cwe: 'CWE-250' },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
function scanEcsTaskDef(file, raw) {
|
|
101
|
+
const findings = [];
|
|
102
|
+
for (const rule of ECS_RULES) {
|
|
103
|
+
const r = new RegExp(rule.re.source, rule.re.flags);
|
|
104
|
+
let m;
|
|
105
|
+
while ((m = r.exec(raw))) {
|
|
106
|
+
findings.push(_finding({ ...rule, family: 'container-runtime' }, file, _lineOf(raw, m.index), m[0]));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return findings;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ─── Common ────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
function _finding(rule, file, line, snippet) {
|
|
115
|
+
return {
|
|
116
|
+
id: `container-runtime:${file}:${line}:${(rule.vuln || '').slice(0, 40)}`,
|
|
117
|
+
file, line,
|
|
118
|
+
vuln: rule.vuln,
|
|
119
|
+
severity: rule.severity,
|
|
120
|
+
cwe: rule.cwe || 'CWE-250',
|
|
121
|
+
family: rule.family || 'container-runtime',
|
|
122
|
+
stride: 'Elevation of Privilege',
|
|
123
|
+
parser: 'CONTAINER-RUNTIME',
|
|
124
|
+
confidence: 0.85,
|
|
125
|
+
snippet: typeof snippet === 'string' ? snippet.slice(0, 200) : '',
|
|
126
|
+
remediation: 'Container-runtime misconfig: review the highlighted directive. Default to non-root user, drop all capabilities and add back specifically what you need, set readOnlyRootFilesystem: true, never mount the docker socket, and avoid host namespaces unless explicitly required.',
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Module entry point.
|
|
132
|
+
*/
|
|
133
|
+
export function scanContainerRuntime(fileContents) {
|
|
134
|
+
if (!fileContents || typeof fileContents !== 'object') return [];
|
|
135
|
+
const out = [];
|
|
136
|
+
for (const [fp, raw] of Object.entries(fileContents)) {
|
|
137
|
+
if (typeof raw !== 'string' || raw.length === 0 || raw.length > 500_000) continue;
|
|
138
|
+
if (DOCKERFILE_NAME_RE.test(fp)) {
|
|
139
|
+
out.push(...scanDockerfile(fp, raw));
|
|
140
|
+
} else if (ECS_TASK_DEF_RE.test(fp)) {
|
|
141
|
+
out.push(...scanEcsTaskDef(fp, raw));
|
|
142
|
+
} else if (K8S_YAML_RE.test(fp)) {
|
|
143
|
+
out.push(...scanK8sYaml(fp, raw));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const _internals = { DOCKERFILE_RULES, K8S_RULES, ECS_RULES };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// FR-LOGIC-9 — Counterfactual analysis (reverse blast radius for controls).
|
|
2
|
+
//
|
|
3
|
+
// For each defensive control found in the code (auth check, sanitizer guard,
|
|
4
|
+
// CSRF middleware, rate-limit middleware, type guard), compute the reverse
|
|
5
|
+
// blast radius: how many existing findings would become critical or exposed
|
|
6
|
+
// if THIS control were removed or bypassed?
|
|
7
|
+
//
|
|
8
|
+
// The output is a list of `single-point-of-failure` records — controls whose
|
|
9
|
+
// removal would expose ≥ 3 findings at high+. These are the controls that
|
|
10
|
+
// most deserve hardening attention BEFORE an attacker tests them. The output
|
|
11
|
+
// also feeds the trust-boundary diagram.
|
|
12
|
+
//
|
|
13
|
+
// We deliberately do NOT modify the findings themselves; we emit a separate
|
|
14
|
+
// `counterfactualReport` artifact that lives on the scan object.
|
|
15
|
+
|
|
16
|
+
const CONTROL_PATTERNS = [
|
|
17
|
+
// Each entry: [name, regex, family of finding this control mitigates]
|
|
18
|
+
['auth-middleware', /(?:requireAuth|authMiddleware|isAuthenticated|verifyJWT|@login_required|protect|requireLogin|getServerSession)\s*\(/g, ['missing-authz', 'broken-auth', 'idor']],
|
|
19
|
+
['csrf-middleware', /(?:csrf|csurf|csrfProtection|CSRFProtect|CsrfFilter)\s*\(/g, ['csrf']],
|
|
20
|
+
['rate-limiter', /(?:rateLimit|expressRateLimit|RateLimiter|rateLimiterFlexible)\s*\(/g, ['unbounded-llm', 'broken-auth']],
|
|
21
|
+
['xss-sanitizer', /(?:DOMPurify\.sanitize|sanitizeHtml|escape|bleach\.clean|html_safe)\s*\(/g, ['xss']],
|
|
22
|
+
['sql-param', /\.(?:prepare|query)\s*\([^)]{0,40}\$\d|\?|placeholder/g, ['sql-injection']],
|
|
23
|
+
['url-validator', /(?:validateUrl|isValidUrl|url\.parse|new URL\()/g, ['ssrf', 'open-redirect']],
|
|
24
|
+
['path-validator', /(?:path\.normalize|path\.resolve|isPathInside|safe_join)\s*\(/g, ['path-traversal']],
|
|
25
|
+
['signature-verify', /(?:hmac|createVerify|timingSafeEqual|verify_signature|stripe\.webhooks\.constructEvent)\s*\(/g, ['webhook-no-signature']],
|
|
26
|
+
['admin-gate', /(?:isAdmin|requireAdmin|hasRole\(['"]admin['"])/g, ['idor', 'missing-authz']],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function detectControls(fileContents) {
|
|
30
|
+
const controls = [];
|
|
31
|
+
if (!fileContents) return controls;
|
|
32
|
+
for (const [fp, text] of Object.entries(fileContents)) {
|
|
33
|
+
if (!text || typeof text !== 'string') continue;
|
|
34
|
+
for (const [name, re, mitigatesFamilies] of CONTROL_PATTERNS) {
|
|
35
|
+
re.lastIndex = 0;
|
|
36
|
+
let m;
|
|
37
|
+
while ((m = re.exec(text))) {
|
|
38
|
+
const line = text.slice(0, m.index).split('\n').length;
|
|
39
|
+
controls.push({ name, file: fp, line, mitigates: mitigatesFamilies });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return controls;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function familyOfFinding(f) {
|
|
47
|
+
if (f.family) return String(f.family).toLowerCase();
|
|
48
|
+
const v = (f.vuln || '').toLowerCase();
|
|
49
|
+
if (/sql.*injection/.test(v)) return 'sql-injection';
|
|
50
|
+
if (/command.*injection/.test(v)) return 'command-injection';
|
|
51
|
+
if (/xss|cross.site/.test(v)) return 'xss';
|
|
52
|
+
if (/ssrf/.test(v)) return 'ssrf';
|
|
53
|
+
if (/idor/.test(v)) return 'idor';
|
|
54
|
+
if (/missing.auth/.test(v)) return 'missing-authz';
|
|
55
|
+
if (/broken.auth|jwt|session/.test(v)) return 'broken-auth';
|
|
56
|
+
if (/csrf/.test(v)) return 'csrf';
|
|
57
|
+
if (/path.travers/.test(v)) return 'path-traversal';
|
|
58
|
+
if (/open.redirect/.test(v)) return 'open-redirect';
|
|
59
|
+
if (/webhook.*sign/.test(v)) return 'webhook-no-signature';
|
|
60
|
+
if (/max_tokens|unbounded/.test(v)) return 'unbounded-llm';
|
|
61
|
+
return 'unknown';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function runCounterfactual(findings, fileContents) {
|
|
65
|
+
const controls = detectControls(fileContents);
|
|
66
|
+
if (!controls.length) return { spofControls: [], note: 'no-controls-detected' };
|
|
67
|
+
|
|
68
|
+
// For each control, count how many findings it currently mitigates and
|
|
69
|
+
// how many would become exposed if it were removed.
|
|
70
|
+
const byMitigates = new Map();
|
|
71
|
+
for (const c of controls) {
|
|
72
|
+
const key = `${c.name}@${c.file}:${c.line}`;
|
|
73
|
+
if (!byMitigates.has(key)) byMitigates.set(key, { control: c, exposedIfRemoved: [] });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (Array.isArray(findings)) {
|
|
77
|
+
for (const f of findings) {
|
|
78
|
+
if (!f || typeof f !== 'object') continue;
|
|
79
|
+
const fam = familyOfFinding(f);
|
|
80
|
+
if (fam === 'unknown') continue;
|
|
81
|
+
// A control that's in the same file as the finding (proxy for "this
|
|
82
|
+
// route uses this control") and that lists `fam` in its mitigates set
|
|
83
|
+
// is treated as a current mitigator.
|
|
84
|
+
for (const [key, rec] of byMitigates) {
|
|
85
|
+
if (rec.control.file !== f.file) continue;
|
|
86
|
+
if (!rec.control.mitigates.includes(fam)) continue;
|
|
87
|
+
// Only flag the finding as "exposed if removed" if it currently has
|
|
88
|
+
// severity high+ — small bugs don't deserve a SPOF alarm.
|
|
89
|
+
if (['critical', 'high'].includes(f.severity)) {
|
|
90
|
+
rec.exposedIfRemoved.push({ family: fam, file: f.file, line: f.line, severity: f.severity });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const spofControls = [];
|
|
97
|
+
for (const [, rec] of byMitigates) {
|
|
98
|
+
if (rec.exposedIfRemoved.length >= 3) {
|
|
99
|
+
spofControls.push({
|
|
100
|
+
control: rec.control.name,
|
|
101
|
+
location: `${rec.control.file}:${rec.control.line}`,
|
|
102
|
+
wouldExpose: rec.exposedIfRemoved.length,
|
|
103
|
+
examples: rec.exposedIfRemoved.slice(0, 5),
|
|
104
|
+
recommendation: `${rec.control.name} at ${rec.control.file}:${rec.control.line} is a single point of failure for ${rec.exposedIfRemoved.length} high+ findings. Consider redundant defense-in-depth.`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { spofControls, controlsDetected: controls.length };
|
|
109
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { isChainWorthy, familyForBoundary } from './cross-lang-meta.js';
|
|
2
|
+
|
|
3
|
+
// GraphQL resolver-to-resolver cross-language taint (Sentinel-parity FR-DET-3).
|
|
4
|
+
//
|
|
5
|
+
// Parses GraphQL SDL files (.graphql / .gql / .schema.graphql) and detects
|
|
6
|
+
// resolver implementations. When a high+ finding sits inside one resolver,
|
|
7
|
+
// and another resolver references the same Type/field via parent/context,
|
|
8
|
+
// the engine reports a cross-resolver chain.
|
|
9
|
+
//
|
|
10
|
+
// We also catch client query call sites (Apollo Client, urql, graphql-request)
|
|
11
|
+
// referencing a Query/Mutation by name and pair them with the server resolver
|
|
12
|
+
// the same way the OpenAPI module pairs HTTP routes.
|
|
13
|
+
|
|
14
|
+
function parseSchemas(fileContents) {
|
|
15
|
+
// Returns { types: Map<typeName, {fields: Map<fieldName, {file, line, returnType?}>}>,
|
|
16
|
+
// queries: Map<queryName, {file, line, returnType?}>,
|
|
17
|
+
// mutations: Map<mutationName, ...> }
|
|
18
|
+
const types = new Map();
|
|
19
|
+
const queries = new Map();
|
|
20
|
+
const mutations = new Map();
|
|
21
|
+
for (const [fp, c] of Object.entries(fileContents || {})) {
|
|
22
|
+
if (!/\.(?:graphql|gql|schema\.graphql)$/i.test(fp)) continue;
|
|
23
|
+
if (typeof c !== 'string' || c.length > 500_000) continue;
|
|
24
|
+
// Type declarations: type X { field: T field2(arg: Y): Z }
|
|
25
|
+
const typeRe = /\btype\s+(\w+)\s*(?:implements\s+[^{]+)?\{([\s\S]*?)\}/g;
|
|
26
|
+
let tm;
|
|
27
|
+
while ((tm = typeRe.exec(c))) {
|
|
28
|
+
const tName = tm[1];
|
|
29
|
+
const body = tm[2];
|
|
30
|
+
const fields = new Map();
|
|
31
|
+
const fieldRe = /^\s*(\w+)\s*(?:\([^)]*\))?\s*:\s*([\w!\[\]]+)/gm;
|
|
32
|
+
let fm;
|
|
33
|
+
while ((fm = fieldRe.exec(body))) {
|
|
34
|
+
const line = c.substring(0, tm.index + (fm.index || 0)).split('\n').length;
|
|
35
|
+
fields.set(fm[1], { file: fp, line, returnType: fm[2] });
|
|
36
|
+
}
|
|
37
|
+
if (tName === 'Query') for (const [k, v] of fields) queries.set(k, v);
|
|
38
|
+
else if (tName === 'Mutation') for (const [k, v] of fields) mutations.set(k, v);
|
|
39
|
+
else types.set(tName, { fields });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { types, queries, mutations };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Detect server resolvers. Patterns vary by framework:
|
|
46
|
+
// Apollo Server: const resolvers = { Query: { foo(parent, args, ctx) {...} } }
|
|
47
|
+
// GraphQL-JS: new GraphQLSchema({ types: ..., resolve(parent, args, ctx) }
|
|
48
|
+
// NestJS: @Query() foo() {} / @Mutation() / @ResolveField()
|
|
49
|
+
// Hot Chocolate (.NET): [ExtendObjectType(Name="Query")] ... GetFoo(...)
|
|
50
|
+
// Strawberry / Graphene (Python): @strawberry.field def foo(): ...
|
|
51
|
+
function findResolvers(fileContents, schema) {
|
|
52
|
+
const found = [];
|
|
53
|
+
const allKnown = new Set([
|
|
54
|
+
...schema.queries.keys(),
|
|
55
|
+
...schema.mutations.keys(),
|
|
56
|
+
]);
|
|
57
|
+
for (const t of schema.types.values()) for (const k of t.fields.keys()) allKnown.add(k);
|
|
58
|
+
if (!allKnown.size) return found;
|
|
59
|
+
|
|
60
|
+
for (const [fp, c] of Object.entries(fileContents || {})) {
|
|
61
|
+
if (!c || typeof c !== 'string') continue;
|
|
62
|
+
if (c.length > 500_000) continue;
|
|
63
|
+
if (!/\.(?:js|jsx|ts|tsx|mjs|cjs|py|cs|rb|go|java|kt)$/i.test(fp)) continue;
|
|
64
|
+
// Only spend regex budget on files that look graphql-related. Heuristics:
|
|
65
|
+
// - content references graphql / apollo / gql backticks / NestJS / Strawberry / Graphene
|
|
66
|
+
// - filename contains "resolver", "schema", "graphql", "mutation", "query"
|
|
67
|
+
const looksGraphqlByContent = /(?:graphql|GraphQL|apollo|@nestjs\/graphql|strawberry|graphene|HotChocolate|gql\s*`|const\s+resolvers\s*=)/i.test(c);
|
|
68
|
+
const looksGraphqlByName = /(?:resolver|schema|graphql|mutations?|queries?)\b/i.test(fp);
|
|
69
|
+
if (!looksGraphqlByContent && !looksGraphqlByName) continue;
|
|
70
|
+
|
|
71
|
+
for (const fieldName of allKnown) {
|
|
72
|
+
// Property-style resolver: fieldName(parent, args, ctx) { ... }
|
|
73
|
+
// Or: fieldName: async (parent, args, ctx) => ...
|
|
74
|
+
const reA = new RegExp(`\\b${fieldName}\\s*\\(\\s*\\w+\\s*,\\s*\\w+\\s*,\\s*\\w+\\s*\\)\\s*\\{`, 'g');
|
|
75
|
+
const reB = new RegExp(`\\b${fieldName}\\s*:\\s*(?:async\\s+)?\\(?\\s*\\w+\\s*,\\s*\\w+\\s*,\\s*\\w+\\s*\\)?\\s*=>`, 'g');
|
|
76
|
+
// NestJS decorator: @Query(() => X) async fieldName(...)
|
|
77
|
+
const reC = new RegExp(`@(?:Query|Mutation|ResolveField)\\s*\\([^)]*\\)\\s*\\n?\\s*(?:async\\s+)?${fieldName}\\s*\\(`, 'g');
|
|
78
|
+
// Python decorator: @strawberry.field def field_name(...)
|
|
79
|
+
const snakeName = fieldName.replace(/[A-Z]/g, (s, i) => (i ? '_' : '') + s.toLowerCase());
|
|
80
|
+
const reD = new RegExp(`@strawberry\\.field\\s*(?:\\([^)]*\\))?\\s*def\\s+${snakeName}\\s*\\(`, 'g');
|
|
81
|
+
|
|
82
|
+
for (const re of [reA, reB, reC, reD]) {
|
|
83
|
+
let m;
|
|
84
|
+
while ((m = re.exec(c))) {
|
|
85
|
+
const line = c.substring(0, m.index).split('\n').length;
|
|
86
|
+
found.push({ file: fp, line, field: fieldName,
|
|
87
|
+
snippet: (c.split('\n')[line - 1] || '').trim().slice(0, 200) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Client query call sites. Apollo / urql / graphql-request style:
|
|
96
|
+
// useQuery(GET_USER) / client.query({query: GET_USER}) / gql`query { user { ... } }`
|
|
97
|
+
function findClientQueries(fileContents, schema) {
|
|
98
|
+
const allKnown = new Set([...schema.queries.keys(), ...schema.mutations.keys()]);
|
|
99
|
+
if (!allKnown.size) return [];
|
|
100
|
+
const found = [];
|
|
101
|
+
for (const [fp, c] of Object.entries(fileContents || {})) {
|
|
102
|
+
if (!c || typeof c !== 'string') continue;
|
|
103
|
+
if (c.length > 500_000) continue;
|
|
104
|
+
if (!/\.(?:js|jsx|ts|tsx|mjs|cjs|py|go)$/i.test(fp)) continue;
|
|
105
|
+
if (!/(?:gql\s*`|useQuery|useMutation|client\.query\(|client\.mutate\(|graphql\s*`)/i.test(c)) continue;
|
|
106
|
+
// Look for `gql`query { fieldName ...`` patterns or `mutation { fieldName ...`
|
|
107
|
+
for (const queryName of allKnown) {
|
|
108
|
+
const re = new RegExp(`(?:query|mutation)\\b[^\`]{0,400}?\\b${queryName}\\s*[\\(\\{]`, 'g');
|
|
109
|
+
let m;
|
|
110
|
+
while ((m = re.exec(c))) {
|
|
111
|
+
const line = c.substring(0, m.index).split('\n').length;
|
|
112
|
+
found.push({ file: fp, line, query: queryName,
|
|
113
|
+
snippet: (c.split('\n')[line - 1] || '').trim().slice(0, 200) });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return found;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function scanCrossLangGraphql(fileContents, existingFindings) {
|
|
121
|
+
const schema = parseSchemas(fileContents);
|
|
122
|
+
const totalDecls = schema.queries.size + schema.mutations.size + schema.types.size;
|
|
123
|
+
if (totalDecls === 0) return [];
|
|
124
|
+
|
|
125
|
+
const resolvers = findResolvers(fileContents, schema);
|
|
126
|
+
if (resolvers.length === 0) return [];
|
|
127
|
+
const clients = findClientQueries(fileContents, schema);
|
|
128
|
+
|
|
129
|
+
const findingsByFile = new Map();
|
|
130
|
+
for (const f of existingFindings || []) {
|
|
131
|
+
if (!f.file || !/critical|high/i.test(f.severity || '')) continue;
|
|
132
|
+
if (!isChainWorthy(f)) continue; // FR-CHAIN-FILTER
|
|
133
|
+
if (!findingsByFile.has(f.file)) findingsByFile.set(f.file, []);
|
|
134
|
+
findingsByFile.get(f.file).push(f);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const c of clients) {
|
|
139
|
+
const impls = resolvers.filter(r => r.field === c.query);
|
|
140
|
+
for (const impl of impls) {
|
|
141
|
+
const fs = findingsByFile.get(impl.file) || [];
|
|
142
|
+
if (!fs.length) continue;
|
|
143
|
+
const seed = fs[0];
|
|
144
|
+
out.push({
|
|
145
|
+
id: `xlang-graphql:${c.file}:${c.line}:${c.query}`,
|
|
146
|
+
file: c.file, line: c.line,
|
|
147
|
+
vuln: `Cross-Language Taint (GraphQL): query ${c.query} → resolver in ${impl.file}:${impl.line} carries ${seed.severity}`,
|
|
148
|
+
severity: 'high',
|
|
149
|
+
cwe: seed.cwe || 'CWE-862',
|
|
150
|
+
snippet: c.snippet,
|
|
151
|
+
remediation: `The GraphQL field "${c.query}" is resolved at ${impl.file}:${impl.line} where "${seed.vuln}" was reported. Fix the resolver-side finding first; any client that consumes the response inherits the underlying risk.`,
|
|
152
|
+
parser: 'XLANG-GRAPHQL',
|
|
153
|
+
family: familyForBoundary('graphql'), // FR-FAMILY-REGISTRY
|
|
154
|
+
confidence: 0.6,
|
|
155
|
+
cross_language: true,
|
|
156
|
+
chain: [
|
|
157
|
+
{ file: c.file, line: c.line, label: `query ${c.query}` },
|
|
158
|
+
{ file: impl.file, line: impl.line, label: `resolver ${impl.field}` },
|
|
159
|
+
{ file: seed.file, line: seed.line, label: seed.vuln },
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { isChainWorthy, familyForBoundary } from './cross-lang-meta.js';
|
|
2
|
+
|
|
3
|
+
// gRPC / .proto cross-language taint propagation (Sentinel-parity FR-DET-3).
|
|
4
|
+
//
|
|
5
|
+
// When a project ships .proto files alongside server impls and client stubs,
|
|
6
|
+
// the engine can correlate them:
|
|
7
|
+
//
|
|
8
|
+
// service UserService {
|
|
9
|
+
// rpc GetUser(GetUserRequest) returns (User);
|
|
10
|
+
// }
|
|
11
|
+
//
|
|
12
|
+
// // server (Go / Java / Python / Node)
|
|
13
|
+
// func (s *userServer) GetUser(ctx, req) (*User, error) { ... }
|
|
14
|
+
//
|
|
15
|
+
// // client
|
|
16
|
+
// resp, err := client.GetUser(ctx, &pb.GetUserRequest{...})
|
|
17
|
+
//
|
|
18
|
+
// We pair the GetUser call site (client) with the GetUser implementation
|
|
19
|
+
// (server). When the server implementation file has high+ findings, we emit
|
|
20
|
+
// a cross_language:true chain to the client call site so engineers see the
|
|
21
|
+
// transitive risk.
|
|
22
|
+
|
|
23
|
+
function parseProtoFiles(fileContents) {
|
|
24
|
+
// Returns { services: Map<serviceName, {file, methods: Set<methodName>}> }
|
|
25
|
+
const services = new Map();
|
|
26
|
+
for (const [fp, c] of Object.entries(fileContents || {})) {
|
|
27
|
+
if (!fp.endsWith('.proto')) continue;
|
|
28
|
+
if (typeof c !== 'string' || c.length > 500_000) continue;
|
|
29
|
+
const blockRe = /\bservice\s+(\w+)\s*\{([^}]*)\}/g;
|
|
30
|
+
let bm;
|
|
31
|
+
while ((bm = blockRe.exec(c))) {
|
|
32
|
+
const svcName = bm[1];
|
|
33
|
+
const body = bm[2] || '';
|
|
34
|
+
const methods = new Set();
|
|
35
|
+
const methodRe = /\brpc\s+(\w+)\s*\(/g;
|
|
36
|
+
let mm;
|
|
37
|
+
while ((mm = methodRe.exec(body))) methods.add(mm[1]);
|
|
38
|
+
services.set(svcName, { file: fp, methods });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return services;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Find client-side gRPC call sites. The generated stubs call <ServiceName>Client
|
|
45
|
+
// (Go / Java / Node) or <service_name>Stub (Python) — but the method name on
|
|
46
|
+
// the receiver is what we match.
|
|
47
|
+
function findClientCalls(fileContents, services) {
|
|
48
|
+
const allMethods = new Set();
|
|
49
|
+
for (const s of services.values()) for (const m of s.methods) allMethods.add(m);
|
|
50
|
+
if (!allMethods.size) return [];
|
|
51
|
+
const found = [];
|
|
52
|
+
for (const [fp, c] of Object.entries(fileContents || {})) {
|
|
53
|
+
if (!c || typeof c !== 'string') continue;
|
|
54
|
+
if (c.length > 500_000) continue;
|
|
55
|
+
if (!/\.(?:js|jsx|ts|tsx|mjs|cjs|py|go|java|kt|rb|cs)$/i.test(fp)) continue;
|
|
56
|
+
// <receiver>.<MethodName>(<ctx>, <req>) — generic client invocation.
|
|
57
|
+
// We look for any of the proto-declared method names being called on an
|
|
58
|
+
// object that looks like a client (camelCase identifier ending in "Client"
|
|
59
|
+
// or "Stub", OR any identifier when the method name is uncommon enough).
|
|
60
|
+
const re = /\b(\w+)\s*\.\s*(\w+)\s*\(/g;
|
|
61
|
+
let m;
|
|
62
|
+
while ((m = re.exec(c))) {
|
|
63
|
+
const methodName = m[2];
|
|
64
|
+
if (!allMethods.has(methodName)) continue;
|
|
65
|
+
const recv = m[1];
|
|
66
|
+
// Heuristic: receiver name suggests "client" or "stub" — reduces FPs.
|
|
67
|
+
// Skip generic names that are too common (.map, .filter, etc.).
|
|
68
|
+
if (!/(?:client|stub|conn|svc|service)/i.test(recv)) continue;
|
|
69
|
+
const line = c.substring(0, m.index).split('\n').length;
|
|
70
|
+
found.push({ file: fp, line, method: methodName, receiver: recv,
|
|
71
|
+
snippet: (c.split('\n')[line - 1] || '').trim().slice(0, 200) });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return found;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Find server impls — methods on a struct/class that match a proto method.
|
|
78
|
+
function findServerImpls(fileContents, services) {
|
|
79
|
+
const allMethods = new Set();
|
|
80
|
+
for (const s of services.values()) for (const m of s.methods) allMethods.add(m);
|
|
81
|
+
if (!allMethods.size) return [];
|
|
82
|
+
const found = [];
|
|
83
|
+
// Patterns vary by language:
|
|
84
|
+
// Go: func (s *userServer) GetUser(ctx, req) (*User, error) {
|
|
85
|
+
// Java: public User getUser(GetUserRequest req) { (note: lower-cased)
|
|
86
|
+
// Python: def GetUser(self, request, context):
|
|
87
|
+
// Node: async getUser(call, callback) { (lower-camel)
|
|
88
|
+
// We match on either the exact proto name OR its lower-camel variant.
|
|
89
|
+
function expand(m) {
|
|
90
|
+
return new Set([m, m.charAt(0).toLowerCase() + m.slice(1)]);
|
|
91
|
+
}
|
|
92
|
+
for (const [fp, c] of Object.entries(fileContents || {})) {
|
|
93
|
+
if (!c || typeof c !== 'string') continue;
|
|
94
|
+
if (c.length > 500_000) continue;
|
|
95
|
+
if (!/\.(?:go|java|kt|py|rb|js|ts|cs)$/i.test(fp)) continue;
|
|
96
|
+
for (const method of allMethods) {
|
|
97
|
+
const variants = [...expand(method)];
|
|
98
|
+
// Approximate detection: a function/method definition whose name is
|
|
99
|
+
// one of the variants, AND the file has 'pb' / 'proto' / 'grpc' import.
|
|
100
|
+
if (!/(?:grpc|pb|protobuf|protoc-gen|@grpc\/grpc-js|google\.protobuf)/i.test(c)) continue;
|
|
101
|
+
const altList = variants.map(v => v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
102
|
+
// Accept any function-definition shape (Go, Java, Python, JS function /
|
|
103
|
+
// arrow / class method) whose name is one of the proto-method variants
|
|
104
|
+
// AND whose arg list starts with `<ident>,` (the gRPC call / context).
|
|
105
|
+
const defRe = new RegExp(
|
|
106
|
+
`\\b(?:func\\s+\\([^)]*\\)\\s+|public\\s+\\w[\\w<>,\\s]*\\s+|async\\s+|def\\s+|function\\s+|exports\\.\\s*)?` +
|
|
107
|
+
`(?:${altList})\\s*\\(\\s*\\w+\\s*,`,
|
|
108
|
+
'g'
|
|
109
|
+
);
|
|
110
|
+
let dm;
|
|
111
|
+
while ((dm = defRe.exec(c))) {
|
|
112
|
+
const line = c.substring(0, dm.index).split('\n').length;
|
|
113
|
+
found.push({ file: fp, line, method,
|
|
114
|
+
snippet: (c.split('\n')[line - 1] || '').trim().slice(0, 200) });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return found;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function scanCrossLangGrpc(fileContents, existingFindings) {
|
|
122
|
+
const services = parseProtoFiles(fileContents);
|
|
123
|
+
if (services.size === 0) return [];
|
|
124
|
+
const clients = findClientCalls(fileContents, services);
|
|
125
|
+
if (clients.length === 0) return [];
|
|
126
|
+
const servers = findServerImpls(fileContents, services);
|
|
127
|
+
if (servers.length === 0) return [];
|
|
128
|
+
|
|
129
|
+
// Index findings by file — high+ AND chain-worthy only (FR-CHAIN-FILTER).
|
|
130
|
+
const findingsByFile = new Map();
|
|
131
|
+
for (const f of existingFindings || []) {
|
|
132
|
+
if (!f.file || !/critical|high/i.test(f.severity || '')) continue;
|
|
133
|
+
if (!isChainWorthy(f)) continue;
|
|
134
|
+
if (!findingsByFile.has(f.file)) findingsByFile.set(f.file, []);
|
|
135
|
+
findingsByFile.get(f.file).push(f);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const out = [];
|
|
139
|
+
for (const c of clients) {
|
|
140
|
+
const impls = servers.filter(s => s.method === c.method);
|
|
141
|
+
for (const impl of impls) {
|
|
142
|
+
const fs = findingsByFile.get(impl.file) || [];
|
|
143
|
+
if (!fs.length) continue;
|
|
144
|
+
const seed = fs[0];
|
|
145
|
+
out.push({
|
|
146
|
+
id: `xlang-grpc:${c.file}:${c.line}:${c.method}`,
|
|
147
|
+
file: c.file, line: c.line,
|
|
148
|
+
vuln: `Cross-Language Taint (gRPC): client call → ${c.method} (server impl in ${impl.file}:${impl.line} carries ${seed.severity})`,
|
|
149
|
+
severity: 'high',
|
|
150
|
+
cwe: seed.cwe || 'CWE-862',
|
|
151
|
+
snippet: c.snippet,
|
|
152
|
+
remediation: `The gRPC method ${c.method} is implemented in ${impl.file}:${impl.line} where "${seed.vuln}" was reported. Any client that propagates the response into a sensitive sink inherits the underlying risk. Fix the server-side finding first.`,
|
|
153
|
+
parser: 'XLANG-GRPC',
|
|
154
|
+
family: familyForBoundary('grpc'), // FR-FAMILY-REGISTRY
|
|
155
|
+
confidence: 0.65,
|
|
156
|
+
cross_language: true,
|
|
157
|
+
chain: [
|
|
158
|
+
{ file: c.file, line: c.line, label: `client.${c.method}` },
|
|
159
|
+
{ file: impl.file, line: impl.line, label: `impl ${impl.method}` },
|
|
160
|
+
{ file: seed.file, line: seed.line, label: seed.vuln },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|