@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,126 @@
|
|
|
1
|
+
// FR-PROD-4 — Network policy import.
|
|
2
|
+
//
|
|
3
|
+
// Parse k8s NetworkPolicy YAML (multi-document) and AWS Security Group JSON;
|
|
4
|
+
// build a coarse map: workload → ingress sources. If the only ingress for a
|
|
5
|
+
// workload is from internal-only ranges (10.0.0.0/8, 172.16.0.0/12,
|
|
6
|
+
// 192.168.0.0/16, fc00::/7, or named `internal`/`vpc-private`), findings on
|
|
7
|
+
// that workload are demoted to `mitigated-by-network`. Conservative: anything
|
|
8
|
+
// not unambiguously internal-only leaves the finding untouched.
|
|
9
|
+
//
|
|
10
|
+
// Inputs (any combination of):
|
|
11
|
+
// k8s/*.yaml, k8s/*.yml — multi-doc kube manifests
|
|
12
|
+
// infra/k8s/ — recursive search
|
|
13
|
+
// .agentic-security/network-policy.json — normalized digest
|
|
14
|
+
//
|
|
15
|
+
// Normalized digest shape:
|
|
16
|
+
// {
|
|
17
|
+
// "workloads": {
|
|
18
|
+
// "api-server": { "exposure": "public", "ingressFrom": ["0.0.0.0/0"] },
|
|
19
|
+
// "admin-panel": { "exposure": "internal", "ingressFrom": ["10.0.0.0/8"] },
|
|
20
|
+
// "worker": { "exposure": "internal", "ingressFrom": [] }
|
|
21
|
+
// }
|
|
22
|
+
// }
|
|
23
|
+
//
|
|
24
|
+
// Mapping workload → finding is heuristic: we look for the workload name in
|
|
25
|
+
// the file path.
|
|
26
|
+
|
|
27
|
+
import * as fs from 'node:fs';
|
|
28
|
+
import * as path from 'node:path';
|
|
29
|
+
|
|
30
|
+
const CANDIDATE_DIGEST = '.agentic-security/network-policy.json';
|
|
31
|
+
const K8S_DIRS = ['k8s', 'infra/k8s', 'deploy/k8s', 'kubernetes', 'manifests'];
|
|
32
|
+
|
|
33
|
+
const INTERNAL_CIDRS = [
|
|
34
|
+
/^10\./, /^172\.(?:1[6-9]|2\d|3[01])\./, /^192\.168\./,
|
|
35
|
+
/^fc/i, /^fd/i,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function isInternal(cidr) {
|
|
39
|
+
if (!cidr) return false;
|
|
40
|
+
if (/internal|vpc-private|private/i.test(cidr)) return true;
|
|
41
|
+
return INTERNAL_CIDRS.some(re => re.test(cidr));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseKubeManifests(scanRoot) {
|
|
45
|
+
const workloads = {};
|
|
46
|
+
const root = scanRoot || process.cwd();
|
|
47
|
+
for (const d of K8S_DIRS) {
|
|
48
|
+
const dp = path.join(root, d);
|
|
49
|
+
if (!fs.existsSync(dp)) continue;
|
|
50
|
+
try {
|
|
51
|
+
const files = fs.readdirSync(dp, { withFileTypes: true });
|
|
52
|
+
for (const f of files) {
|
|
53
|
+
if (!f.isFile() || !/\.(ya?ml)$/i.test(f.name)) continue;
|
|
54
|
+
const text = fs.readFileSync(path.join(dp, f.name), 'utf8');
|
|
55
|
+
// Multi-doc split by `---` then look for NetworkPolicy entries.
|
|
56
|
+
const docs = text.split(/^---\s*$/m);
|
|
57
|
+
for (const doc of docs) {
|
|
58
|
+
if (!/kind:\s*NetworkPolicy/i.test(doc)) continue;
|
|
59
|
+
const podSelM = /podSelector:[\s\S]*?matchLabels:[\s\S]*?app:\s*['"]?(\w[\w-]*)['"]?/i.exec(doc);
|
|
60
|
+
const workload = podSelM ? podSelM[1] : 'unknown';
|
|
61
|
+
// Default-deny check: presence of `ingress: []` or empty ingress block.
|
|
62
|
+
const isDeny = /policyTypes:\s*\[Ingress\][\s\S]*?ingress:\s*\[\]/i.test(doc) ||
|
|
63
|
+
/policyTypes:[\s\S]*?-\s+Ingress[\s\S]*?ingress:\s*\[\]/i.test(doc);
|
|
64
|
+
const cidrs = [...doc.matchAll(/ipBlock:[\s\S]*?cidr:\s*['"]?([\d./a-fA-F:]+)['"]?/g)].map(m => m[1]);
|
|
65
|
+
const externalAllowed = cidrs.filter(c => !isInternal(c));
|
|
66
|
+
let exposure;
|
|
67
|
+
if (isDeny && cidrs.length === 0) exposure = 'internal';
|
|
68
|
+
else if (cidrs.length === 0) exposure = 'unknown';
|
|
69
|
+
else if (externalAllowed.length === 0) exposure = 'internal';
|
|
70
|
+
else exposure = 'public';
|
|
71
|
+
workloads[workload] = {
|
|
72
|
+
exposure,
|
|
73
|
+
ingressFrom: cidrs.length ? cidrs : (isDeny ? [] : ['unspecified']),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch {}
|
|
78
|
+
}
|
|
79
|
+
return workloads;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function loadNetworkPosture(scanRoot) {
|
|
83
|
+
const root = scanRoot || process.cwd();
|
|
84
|
+
const digestPath = path.join(root, CANDIDATE_DIGEST);
|
|
85
|
+
if (fs.existsSync(digestPath)) {
|
|
86
|
+
try {
|
|
87
|
+
const data = JSON.parse(fs.readFileSync(digestPath, 'utf8'));
|
|
88
|
+
if (data && data.workloads) return data;
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
const workloads = parseKubeManifests(scanRoot);
|
|
92
|
+
if (Object.keys(workloads).length === 0) return null;
|
|
93
|
+
return { workloads };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function matchWorkload(workloads, filePath) {
|
|
97
|
+
if (!workloads || !filePath) return null;
|
|
98
|
+
// Match by the longest workload name that occurs as a path segment.
|
|
99
|
+
const segs = filePath.split('/');
|
|
100
|
+
let best = null;
|
|
101
|
+
for (const name of Object.keys(workloads)) {
|
|
102
|
+
if (segs.includes(name) || segs.some(s => s.startsWith(name + '-'))) {
|
|
103
|
+
if (!best || name.length > best.length) best = name;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return best ? { name: best, info: workloads[best] } : null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function annotateNetworkMitigation(findings, scanRoot) {
|
|
110
|
+
if (!Array.isArray(findings)) return findings;
|
|
111
|
+
const posture = loadNetworkPosture(scanRoot);
|
|
112
|
+
if (!posture || !posture.workloads) return findings;
|
|
113
|
+
for (const f of findings) {
|
|
114
|
+
if (!f || typeof f !== 'object') continue;
|
|
115
|
+
const w = matchWorkload(posture.workloads, f.file || '');
|
|
116
|
+
if (!w) continue;
|
|
117
|
+
if (w.info.exposure === 'internal') {
|
|
118
|
+
f.mitigatedByNetwork = true;
|
|
119
|
+
f.networkPolicyName = w.name;
|
|
120
|
+
f.networkExposure = 'internal';
|
|
121
|
+
} else if (w.info.exposure === 'public') {
|
|
122
|
+
f.networkExposure = 'public';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return findings;
|
|
126
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// SentQL path-constraint predicate catalog.
|
|
2
|
+
//
|
|
3
|
+
// When a custom rule declares `path: { must_traverse: [...], must_not_traverse: [...] }`
|
|
4
|
+
// the runtime checks each predicate against the finding's path metadata (route
|
|
5
|
+
// reachability, sanitizer presence, auth-guard presence, etc.) and drops the
|
|
6
|
+
// finding when the constraint is violated.
|
|
7
|
+
//
|
|
8
|
+
// A predicate is a string identifier. The dispatch table below maps each
|
|
9
|
+
// identifier to a function `(finding) → bool` that tests the finding.
|
|
10
|
+
//
|
|
11
|
+
// New predicates can be added without changing the SentQL grammar — author
|
|
12
|
+
// names them in YAML, this file resolves the name.
|
|
13
|
+
|
|
14
|
+
// Each predicate returns TRUE iff the finding satisfies the predicate.
|
|
15
|
+
const PREDICATES = {
|
|
16
|
+
// The finding's source is a route handler (HTTP entry point).
|
|
17
|
+
is_http_route(f) {
|
|
18
|
+
if (f.routeRooted === true) return true;
|
|
19
|
+
if (f.source && f.source.category && /HTTP|URL|Form|DOM/i.test(f.source.category)) return true;
|
|
20
|
+
return false;
|
|
21
|
+
},
|
|
22
|
+
// Inverse — finding doesn't come from a route handler.
|
|
23
|
+
not_http_route(f) { return !PREDICATES.is_http_route(f); },
|
|
24
|
+
|
|
25
|
+
// A sanitizer was detected on the path.
|
|
26
|
+
is_sanitized(f) { return f.isSanitized === true; },
|
|
27
|
+
not_sanitized(f) { return !PREDICATES.is_sanitized(f); },
|
|
28
|
+
|
|
29
|
+
// An auth guard is present on the path.
|
|
30
|
+
is_auth_guarded(f) { return Array.isArray(f.guards) && f.guards.length > 0; },
|
|
31
|
+
not_auth_guarded(f) { return !PREDICATES.is_auth_guarded(f); },
|
|
32
|
+
|
|
33
|
+
// Reachable from an unauthenticated route (engine annotation).
|
|
34
|
+
has_unauth_route(f) {
|
|
35
|
+
if (Array.isArray(f.exploitabilityFactors) && f.exploitabilityFactors.includes('unauth-route-reachable')) return true;
|
|
36
|
+
return false;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Cross-file taint chain (multi-hop, not single-file).
|
|
40
|
+
is_cross_file(f) {
|
|
41
|
+
if (f.isCrossFile === true) return true;
|
|
42
|
+
const srcFile = f.source && f.source.file;
|
|
43
|
+
const sinkFile = (f.sink && f.sink.file) || f.file;
|
|
44
|
+
return !!(srcFile && sinkFile && srcFile !== sinkFile);
|
|
45
|
+
},
|
|
46
|
+
not_cross_file(f) { return !PREDICATES.is_cross_file(f); },
|
|
47
|
+
|
|
48
|
+
// Reachable at all (call graph said yes).
|
|
49
|
+
is_reachable(f) { return f.reachable === true; },
|
|
50
|
+
not_reachable(f) { return f.reachable === false; },
|
|
51
|
+
|
|
52
|
+
// Finding has a path / chain of length > 1.
|
|
53
|
+
has_multi_step_path(f) {
|
|
54
|
+
if (Array.isArray(f.chain) && f.chain.length > 1) return true;
|
|
55
|
+
if (Array.isArray(f.pathSteps) && f.pathSteps.length > 1) return true;
|
|
56
|
+
return false;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// Drop findings whose path violates the rule's constraints.
|
|
61
|
+
// - must_traverse: ALL predicates must return true
|
|
62
|
+
// - must_not_traverse: NONE may return true
|
|
63
|
+
// Returns { kept: Finding[], dropped: SuppressionEntry[] }.
|
|
64
|
+
export function applyPathConstraints(findings) {
|
|
65
|
+
const kept = [];
|
|
66
|
+
const dropped = [];
|
|
67
|
+
for (const f of (findings || [])) {
|
|
68
|
+
const c = f && f._pathConstraints;
|
|
69
|
+
if (!c) { kept.push(f); continue; }
|
|
70
|
+
let drop = null;
|
|
71
|
+
if (Array.isArray(c.mustTraverse) && c.mustTraverse.length) {
|
|
72
|
+
for (const name of c.mustTraverse) {
|
|
73
|
+
const pred = PREDICATES[name];
|
|
74
|
+
// Unknown predicates fail-open: don't drop the finding just because the
|
|
75
|
+
// rule named a predicate we don't know about. Log silently.
|
|
76
|
+
if (!pred) continue;
|
|
77
|
+
if (!pred(f)) { drop = `must-traverse-failed:${name}`; break; }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!drop && Array.isArray(c.mustNotTraverse) && c.mustNotTraverse.length) {
|
|
81
|
+
for (const name of c.mustNotTraverse) {
|
|
82
|
+
const pred = PREDICATES[name];
|
|
83
|
+
if (!pred) continue;
|
|
84
|
+
if (pred(f)) { drop = `must-not-traverse-hit:${name}`; break; }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (drop) {
|
|
88
|
+
dropped.push({
|
|
89
|
+
vuln: f.vuln, file: f.file, line: f.line, snippet: f.snippet || '',
|
|
90
|
+
reason: 'sentql-path-constraint:' + drop,
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
kept.push(f);
|
|
95
|
+
}
|
|
96
|
+
return { kept, dropped };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const _internalPredicateNames = Object.keys(PREDICATES);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// FR-ADV-2 + FR-LOGIC-10 — Per-persona prioritization.
|
|
2
|
+
//
|
|
3
|
+
// Replace CVSS-style flat severity with a per-attacker-persona severity
|
|
4
|
+
// matrix. The same finding can be `critical` for a script kiddie (drive-by
|
|
5
|
+
// exploit) and `low` for an APT (they already have credentialed shell).
|
|
6
|
+
// Five personas, each with a weighting profile over existing finding fields.
|
|
7
|
+
//
|
|
8
|
+
// Inputs (read from each finding when present):
|
|
9
|
+
// - severity, family / vuln
|
|
10
|
+
// - reachable (true/false), routeRooted (true/false)
|
|
11
|
+
// - exposedInProd / mitigatedInProd / unreachableInProd (FR-PROD-7)
|
|
12
|
+
// - guards (auth/RBAC), kev, epssPercentile
|
|
13
|
+
// - crownJewelScore, featureFlagState
|
|
14
|
+
// - parser, validator_verdict
|
|
15
|
+
//
|
|
16
|
+
// Output: f.personaScores = { [persona]: { score, tier, factors[] } }
|
|
17
|
+
// f.personaTopTwo = ['opportunistic', 'apt'] for UI compactness.
|
|
18
|
+
|
|
19
|
+
const PERSONAS = [
|
|
20
|
+
'script-kiddie',
|
|
21
|
+
'opportunistic-criminal',
|
|
22
|
+
'apt-nation-state',
|
|
23
|
+
'supply-chain-attacker',
|
|
24
|
+
'malicious-insider',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const FAMILY_PERSONA_BIAS = {
|
|
28
|
+
'sql-injection': { 'script-kiddie': +0.20, 'opportunistic-criminal': +0.25 },
|
|
29
|
+
'command-injection': { 'script-kiddie': +0.25, 'opportunistic-criminal': +0.30, 'apt-nation-state': +0.20 },
|
|
30
|
+
'xss': { 'script-kiddie': +0.15, 'opportunistic-criminal': +0.20 },
|
|
31
|
+
'ssrf': { 'opportunistic-criminal': +0.20, 'apt-nation-state': +0.30 },
|
|
32
|
+
'path-traversal': { 'script-kiddie': +0.20, 'opportunistic-criminal': +0.20 },
|
|
33
|
+
'idor': { 'opportunistic-criminal': +0.25, 'malicious-insider': +0.30 },
|
|
34
|
+
'missing-authz': { 'opportunistic-criminal': +0.20, 'malicious-insider': +0.35 },
|
|
35
|
+
'broken-auth': { 'script-kiddie': +0.15, 'opportunistic-criminal': +0.30 },
|
|
36
|
+
'hardcoded-secret': { 'opportunistic-criminal': +0.25, 'malicious-insider': +0.20, 'supply-chain-attacker': +0.20 },
|
|
37
|
+
'webhook-no-signature': { 'opportunistic-criminal': +0.20, 'apt-nation-state': +0.10 },
|
|
38
|
+
'unsafe-deserialization': { 'apt-nation-state': +0.35, 'supply-chain-attacker': +0.30 },
|
|
39
|
+
'prototype-pollution': { 'apt-nation-state': +0.30, 'supply-chain-attacker': +0.20 },
|
|
40
|
+
'jndi': { 'apt-nation-state': +0.35, 'opportunistic-criminal': +0.20 },
|
|
41
|
+
'mass-assignment': { 'opportunistic-criminal': +0.20, 'malicious-insider': +0.25 },
|
|
42
|
+
'csrf': { 'script-kiddie': +0.10, 'opportunistic-criminal': +0.15 },
|
|
43
|
+
'open-redirect': { 'script-kiddie': +0.15, 'opportunistic-criminal': +0.15 },
|
|
44
|
+
'prompt-injection': { 'script-kiddie': +0.25, 'apt-nation-state': +0.20 },
|
|
45
|
+
'llm-output-trusted': { 'apt-nation-state': +0.30, 'malicious-insider': +0.20 },
|
|
46
|
+
'unbounded-llm': { 'opportunistic-criminal': +0.20 },
|
|
47
|
+
'sca-cve': { 'opportunistic-criminal': +0.25, 'apt-nation-state': +0.20 },
|
|
48
|
+
'install-script': { 'supply-chain-attacker': +0.50 },
|
|
49
|
+
'typosquat': { 'supply-chain-attacker': +0.40 },
|
|
50
|
+
'mass-merge': { 'supply-chain-attacker': +0.30 },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const SEVERITY_BASE = { critical: 0.85, high: 0.65, medium: 0.40, low: 0.20, info: 0.10 };
|
|
54
|
+
|
|
55
|
+
function familyKey(f) {
|
|
56
|
+
if (f.family) return String(f.family).toLowerCase().replace(/[\s_]+/g, '-');
|
|
57
|
+
const v = (f.vuln || f.title || '').toLowerCase();
|
|
58
|
+
if (/sql.*injection/.test(v)) return 'sql-injection';
|
|
59
|
+
if (/command.*injection|os.command|shell.exec/.test(v)) return 'command-injection';
|
|
60
|
+
if (/cross.site script|xss/.test(v)) return 'xss';
|
|
61
|
+
if (/ssrf|server.side request/.test(v)) return 'ssrf';
|
|
62
|
+
if (/path traversal|zip.slip|directory traversal/.test(v)) return 'path-traversal';
|
|
63
|
+
if (/idor|insecure direct object/.test(v)) return 'idor';
|
|
64
|
+
if (/missing auth|broken access/.test(v)) return 'missing-authz';
|
|
65
|
+
if (/broken auth|jwt|session/.test(v)) return 'broken-auth';
|
|
66
|
+
if (/hardcoded|secret in source|api key/.test(v)) return 'hardcoded-secret';
|
|
67
|
+
if (/webhook.*sign|signature/.test(v)) return 'webhook-no-signature';
|
|
68
|
+
if (/deserial/.test(v)) return 'unsafe-deserialization';
|
|
69
|
+
if (/prototype pollution/.test(v)) return 'prototype-pollution';
|
|
70
|
+
if (/jndi|log4shell/.test(v)) return 'jndi';
|
|
71
|
+
if (/mass assignment/.test(v)) return 'mass-assignment';
|
|
72
|
+
if (/csrf/.test(v)) return 'csrf';
|
|
73
|
+
if (/open redirect/.test(v)) return 'open-redirect';
|
|
74
|
+
if (/prompt injection/.test(v)) return 'prompt-injection';
|
|
75
|
+
if (/llm output/.test(v)) return 'llm-output-trusted';
|
|
76
|
+
if (/max_tokens|unbounded/.test(v)) return 'unbounded-llm';
|
|
77
|
+
if (/install.script|postinstall/.test(v)) return 'install-script';
|
|
78
|
+
if (/typosquat/.test(v)) return 'typosquat';
|
|
79
|
+
if (/cve-|kev/.test(v)) return 'sca-cve';
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function scoreOne(f, persona) {
|
|
84
|
+
const factors = [];
|
|
85
|
+
let s = SEVERITY_BASE[f.severity] ?? 0.30;
|
|
86
|
+
factors.push(`sev:${f.severity || 'unknown'}`);
|
|
87
|
+
|
|
88
|
+
const fam = familyKey(f);
|
|
89
|
+
const bias = fam && FAMILY_PERSONA_BIAS[fam] ? (FAMILY_PERSONA_BIAS[fam][persona] || 0) : 0;
|
|
90
|
+
if (bias) { s += bias; factors.push(`bias:${fam}+${bias.toFixed(2)}`); }
|
|
91
|
+
|
|
92
|
+
// Persona-specific modifiers.
|
|
93
|
+
if (persona === 'script-kiddie') {
|
|
94
|
+
if (f.exposedInProd) { s += 0.15; factors.push('exposed-in-prod'); }
|
|
95
|
+
if (f.mitigatedInProd) { s -= 0.30; factors.push('mitigated-in-prod'); }
|
|
96
|
+
if (f.guards && f.guards.length) { s -= 0.20; factors.push(`auth-gated:${f.guards.length}`); }
|
|
97
|
+
if (f.kev) { s += 0.20; factors.push('kev'); }
|
|
98
|
+
} else if (persona === 'opportunistic-criminal') {
|
|
99
|
+
if (f.exposedInProd) { s += 0.10; factors.push('exposed-in-prod'); }
|
|
100
|
+
if (f.mitigatedInProd) { s -= 0.15; factors.push('mitigated-in-prod'); }
|
|
101
|
+
if (f.crownJewelScore >= 0.4) { s += 0.20; factors.push('crown-jewel-adj'); }
|
|
102
|
+
if (typeof f.epssPercentile === 'number' && f.epssPercentile >= 0.95) { s += 0.15; factors.push('epss>=p95'); }
|
|
103
|
+
} else if (persona === 'apt-nation-state') {
|
|
104
|
+
// APTs care less about easy exploits and more about high-value targets +
|
|
105
|
+
// persistence; reachability/auth gating matters much less to them.
|
|
106
|
+
if (f.crownJewelScore >= 0.5) { s += 0.25; factors.push('crown-jewel-target'); }
|
|
107
|
+
if (fam === 'unsafe-deserialization' || fam === 'jndi' || fam === 'prompt-injection') { s += 0.10; factors.push('apt-favored-family'); }
|
|
108
|
+
if (f.guards && f.guards.length) { s -= 0.05; factors.push('minor-auth-cost'); } // they have creds
|
|
109
|
+
} else if (persona === 'supply-chain-attacker') {
|
|
110
|
+
if (f.parser === 'SCA' || /sca|cve|kev/i.test(fam || '')) { s += 0.20; factors.push('sca-finding'); }
|
|
111
|
+
if (fam === 'install-script' || fam === 'typosquat') { s += 0.30; factors.push('classic-supply-chain'); }
|
|
112
|
+
if (f.provenance === 'ai-likely') { s += 0.10; factors.push('ai-generated'); }
|
|
113
|
+
} else if (persona === 'malicious-insider') {
|
|
114
|
+
if (f.exposedInProd || f.guards?.length) { s -= 0.10; factors.push('insider-bypasses-edge'); }
|
|
115
|
+
if (fam === 'missing-authz' || fam === 'idor' || fam === 'mass-assignment') { s += 0.20; factors.push('authz-bypass-favored'); }
|
|
116
|
+
if (f.crownJewelScore >= 0.5) { s += 0.15; factors.push('insider-target'); }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Universal feature-flag dampener.
|
|
120
|
+
if (f.featureFlagState === 'gated-off') { s = Math.min(s, 0.15); factors.push('flag-gated-off'); }
|
|
121
|
+
else if (f.featureFlagState === 'partial-rollout') { s -= 0.10; factors.push('partial-rollout'); }
|
|
122
|
+
|
|
123
|
+
s = Math.max(0, Math.min(1, s));
|
|
124
|
+
return { score: Number(s.toFixed(2)), factors };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function tierOf(s) {
|
|
128
|
+
if (s >= 0.80) return 'critical';
|
|
129
|
+
if (s >= 0.60) return 'high';
|
|
130
|
+
if (s >= 0.35) return 'medium';
|
|
131
|
+
if (s >= 0.15) return 'low';
|
|
132
|
+
return 'info';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function annotatePersonaScores(findings) {
|
|
136
|
+
if (!Array.isArray(findings)) return findings;
|
|
137
|
+
for (const f of findings) {
|
|
138
|
+
if (!f || typeof f !== 'object') continue;
|
|
139
|
+
const scores = {};
|
|
140
|
+
for (const p of PERSONAS) {
|
|
141
|
+
const { score, factors } = scoreOne(f, p);
|
|
142
|
+
scores[p] = { score, tier: tierOf(score), factors };
|
|
143
|
+
}
|
|
144
|
+
f.personaScores = scores;
|
|
145
|
+
const ranked = Object.entries(scores).sort((a, b) => b[1].score - a[1].score);
|
|
146
|
+
f.personaTopTwo = ranked.slice(0, 2).map(([p]) => p);
|
|
147
|
+
f.personaMaxScore = ranked[0][1].score;
|
|
148
|
+
f.personaMaxName = ranked[0][0];
|
|
149
|
+
}
|
|
150
|
+
return findings;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { PERSONAS };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// CWE ↔ family lookup tables for the PoC generator (P1.1).
|
|
2
|
+
//
|
|
3
|
+
// Kept separate from poc-generator.js so the templates can be ergonomic
|
|
4
|
+
// (small, focused) and the mapping table can grow independently as we add
|
|
5
|
+
// CWE coverage. Both files exported as a single contract.
|
|
6
|
+
|
|
7
|
+
export const CWE_TO_FAMILY = Object.freeze({
|
|
8
|
+
'CWE-89': 'sql-injection',
|
|
9
|
+
'CWE-78': 'command-injection',
|
|
10
|
+
'CWE-79': 'xss',
|
|
11
|
+
'CWE-22': 'path-traversal',
|
|
12
|
+
'CWE-918': 'ssrf',
|
|
13
|
+
'CWE-94': 'code-injection',
|
|
14
|
+
'CWE-352': 'csrf',
|
|
15
|
+
'CWE-601': 'open-redirect',
|
|
16
|
+
'CWE-611': 'xxe',
|
|
17
|
+
'CWE-502': 'insecure-deserialization',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const FAMILY_TO_PRIMARY_CWE = Object.freeze(
|
|
21
|
+
Object.fromEntries(Object.entries(CWE_TO_FAMILY).map(([k, v]) => [v, k]))
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Families for which v1 explicitly does NOT generate a PoC. The verifier
|
|
25
|
+
// marks these `unverified-by-design`. Documented here so the choice is
|
|
26
|
+
// auditable rather than buried in template absence. Read via the
|
|
27
|
+
// `isExplicitlyNoPoc()` helper; not exported directly.
|
|
28
|
+
const NO_POC_FAMILIES = Object.freeze(new Set([
|
|
29
|
+
'timing-oracle', // requires statistical analysis, not a single request
|
|
30
|
+
'data-exposure', // visual-inspection-shaped; no single demonstrable request
|
|
31
|
+
'log-injection', // sink is the log file; out-of-band verification needed
|
|
32
|
+
'audit-logging', // absence-of-a-thing; harder to PoC
|
|
33
|
+
'header-hardening', // configuration finding, not flow-based
|
|
34
|
+
'hardcoded-secret', // proof = grep, not a request
|
|
35
|
+
'vulnerable-dep', // proof = OSV lookup, not a runtime PoC
|
|
36
|
+
'jwt-no-exp', // requires waiting; not single-shot
|
|
37
|
+
'orm-no-pagination', // resource exhaustion shape; sandbox-unsafe to PoC
|
|
38
|
+
'weak-rng', // statistical; not a single request
|
|
39
|
+
'weak-crypto', // statistical; not a single request
|
|
40
|
+
]));
|
|
41
|
+
|
|
42
|
+
export function isPocSupported(familyOrCwe) {
|
|
43
|
+
if (!familyOrCwe) return false;
|
|
44
|
+
if (CWE_TO_FAMILY[familyOrCwe]) return true;
|
|
45
|
+
if (FAMILY_TO_PRIMARY_CWE[familyOrCwe]) return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isExplicitlyNoPoc(family) {
|
|
50
|
+
return NO_POC_FAMILIES.has(family);
|
|
51
|
+
}
|