@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,110 @@
|
|
|
1
|
+
// FR-PROD-5 — Crown-jewel / business-impact mapping.
|
|
2
|
+
//
|
|
3
|
+
// Score each file (and, where possible, function) by the loss exposure if it
|
|
4
|
+
// is compromised. CVSS knows nothing about your business. A SQL-injection in
|
|
5
|
+
// a public health-check is `info`; the same SQLi in a Stripe webhook handler
|
|
6
|
+
// is "you just lost the customer's revenue stream + their trust." We map
|
|
7
|
+
// files to a 0..1 business-impact score so findings can be ranked by
|
|
8
|
+
// proximity to actual crown-jewel paths.
|
|
9
|
+
//
|
|
10
|
+
// Signals (each pushes the score up, capped at 1.0):
|
|
11
|
+
//
|
|
12
|
+
// Path patterns:
|
|
13
|
+
// - /admin/, /internal/, /backoffice/ → 0.30
|
|
14
|
+
// - /billing/, /checkout/, /webhooks/, /payment/, /stripe/ → 0.40
|
|
15
|
+
// - /auth/, /login/, /session/, /tokens/, /password/ → 0.35
|
|
16
|
+
// - /users/, /accounts/, /profiles/ → 0.20
|
|
17
|
+
// - /api/v\d+/ → +0.05 (public API surface)
|
|
18
|
+
//
|
|
19
|
+
// File content:
|
|
20
|
+
// - imports stripe / paddle / braintree → +0.30
|
|
21
|
+
// - imports auth0 / clerk / next-auth / @auth0 → +0.25
|
|
22
|
+
// - references DROP TABLE / DELETE FROM / TRUNCATE → +0.15
|
|
23
|
+
// - references private keys / KMS / signing keys → +0.25
|
|
24
|
+
// - schema files (prisma, drizzle, sequelize) → +0.20
|
|
25
|
+
//
|
|
26
|
+
// Filename hints:
|
|
27
|
+
// - *.health.* / */health/* / readiness → -0.30 (deliberate downgrade)
|
|
28
|
+
// - *.test.*, */tests/* → -0.50 (test code, no prod blast radius)
|
|
29
|
+
// - */docs/, README → -0.50
|
|
30
|
+
//
|
|
31
|
+
// The score is ordinal — use it to rank findings, not as a probability.
|
|
32
|
+
|
|
33
|
+
const PATH_BUMPS = [
|
|
34
|
+
[/\/(?:admin|internal|backoffice|staff)\b/i, 0.30, 'admin-path'],
|
|
35
|
+
[/\/(?:billing|checkout|webhooks?|payment|stripe|paddle|braintree)\b/i, 0.40, 'revenue-path'],
|
|
36
|
+
[/\/(?:auth|login|session|tokens?|password|oauth)\b/i, 0.35, 'auth-path'],
|
|
37
|
+
[/\/(?:users?|accounts?|profiles?|members?)\b/i, 0.20, 'identity-path'],
|
|
38
|
+
[/\/api\/v\d+\b/, 0.05, 'public-api-surface'],
|
|
39
|
+
[/\/(?:secrets?|keys?|kms|vault)\b/i, 0.30, 'secrets-path'],
|
|
40
|
+
[/\/(?:health|healthz|readiness|liveness|ping|status|metrics)\b/i, -0.30, 'health-check'],
|
|
41
|
+
[/(?:\.test\.|\.spec\.|\/tests?\/|__tests__\/|\/fixtures?\/|\/mocks?\/)/i, -0.50, 'test-code'],
|
|
42
|
+
[/(?:\/docs?\/|README|CHANGELOG)/i, -0.50, 'docs-code'],
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const CONTENT_BUMPS = [
|
|
46
|
+
[/(?:require|import).{0,40}\b(?:stripe|@paddle|braintree|@chargebee|adyen|@lemonsqueezy)\b/i, 0.30, 'imports-payments'],
|
|
47
|
+
[/(?:require|import).{0,40}\b(?:auth0|@clerk\/|next-auth|@auth\/|lucia-auth|workos|@okta\/)\b/i, 0.25, 'imports-auth'],
|
|
48
|
+
[/\b(?:DROP\s+TABLE|DELETE\s+FROM|TRUNCATE\s+TABLE|GRANT\s+ALL)\b/i, 0.15, 'destructive-sql'],
|
|
49
|
+
[/(?:PRIVATE\s+KEY|@aws-crypto|@google-cloud\/kms|@azure\/keyvault|jsonwebtoken|jose|signKey|signingKey)/, 0.25, 'crypto-keys'],
|
|
50
|
+
[/datasource\s+db|generator\s+client|model\s+\w+\s*\{|drizzle\.config|@sequelize/, 0.20, 'schema-file'],
|
|
51
|
+
[/req\.(?:user|account|tenant)\.(?:id|email|role|tier)/, 0.10, 'session-context'],
|
|
52
|
+
[/\b(?:exec|spawn|spawnSync|child_process)\b/, 0.15, 'shell-execution'],
|
|
53
|
+
[/process\.env\.(?:[A-Z_]*_KEY|[A-Z_]*_SECRET|[A-Z_]*_TOKEN)/, 0.10, 'reads-secret-env'],
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export function scoreFile(filePath, content) {
|
|
57
|
+
let score = 0;
|
|
58
|
+
const factors = [];
|
|
59
|
+
if (filePath) {
|
|
60
|
+
for (const [re, bump, label] of PATH_BUMPS) {
|
|
61
|
+
if (re.test(filePath)) { score += bump; factors.push(label); }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (content && typeof content === 'string') {
|
|
65
|
+
const sample = content.length > 32_000 ? content.slice(0, 32_000) : content;
|
|
66
|
+
for (const [re, bump, label] of CONTENT_BUMPS) {
|
|
67
|
+
if (re.test(sample)) { score += bump; factors.push(label); }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
score = Math.max(0, Math.min(1, score));
|
|
71
|
+
let tier;
|
|
72
|
+
if (score >= 0.65) tier = 'crown-jewel';
|
|
73
|
+
else if (score >= 0.40) tier = 'high-value';
|
|
74
|
+
else if (score >= 0.20) tier = 'standard';
|
|
75
|
+
else if (score === 0) tier = 'unknown';
|
|
76
|
+
else tier = 'low-value';
|
|
77
|
+
return { score: Number(score.toFixed(2)), tier, factors };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function mapCrownJewels(fileContents) {
|
|
81
|
+
if (!fileContents || typeof fileContents !== 'object') return {};
|
|
82
|
+
const map = {};
|
|
83
|
+
for (const [fp, content] of Object.entries(fileContents)) {
|
|
84
|
+
const r = scoreFile(fp, content);
|
|
85
|
+
if (r.score > 0 || r.factors.length > 0) map[fp] = r;
|
|
86
|
+
}
|
|
87
|
+
return map;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function annotateCrownJewelScores(findings, fileContents) {
|
|
91
|
+
if (!Array.isArray(findings)) return findings;
|
|
92
|
+
const map = mapCrownJewels(fileContents || {});
|
|
93
|
+
for (const f of findings) {
|
|
94
|
+
if (!f || typeof f !== 'object') continue;
|
|
95
|
+
const fp = f.file;
|
|
96
|
+
if (!fp) continue;
|
|
97
|
+
const r = map[fp];
|
|
98
|
+
if (!r) {
|
|
99
|
+
const fallback = scoreFile(fp, fileContents?.[fp] || '');
|
|
100
|
+
f.crownJewelScore = fallback.score;
|
|
101
|
+
f.crownJewelTier = fallback.tier;
|
|
102
|
+
f.crownJewelFactors = fallback.factors;
|
|
103
|
+
} else {
|
|
104
|
+
f.crownJewelScore = r.score;
|
|
105
|
+
f.crownJewelTier = r.tier;
|
|
106
|
+
f.crownJewelFactors = r.factors;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return findings;
|
|
110
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// Custom pattern-rule DSL — Semgrep-lite for in-repo rules.
|
|
2
|
+
//
|
|
3
|
+
// Lives alongside the existing source/sink/sanitizer custom rules in
|
|
4
|
+
// engine.js's `_loadCustomRules`. Pattern rules are simpler: a regex (or a
|
|
5
|
+
// list of regexes that must ALL match within N lines), a severity, a CWE,
|
|
6
|
+
// and a remediation hint. Authors get findings produced directly without
|
|
7
|
+
// needing to model dataflow.
|
|
8
|
+
//
|
|
9
|
+
// File location: .agentic-security/rules/*.yml
|
|
10
|
+
//
|
|
11
|
+
// Example rule:
|
|
12
|
+
// id: my-org/no-eval
|
|
13
|
+
// title: "Use of eval() is forbidden"
|
|
14
|
+
// severity: high
|
|
15
|
+
// cwe: CWE-95
|
|
16
|
+
// languages: [javascript, typescript]
|
|
17
|
+
// match:
|
|
18
|
+
// pattern: "\\beval\\s*\\("
|
|
19
|
+
// message: "eval() bypasses our static-analysis controls; use JSON.parse or a sandbox."
|
|
20
|
+
//
|
|
21
|
+
// Supports:
|
|
22
|
+
// match.pattern — single regex
|
|
23
|
+
// match.allOf: [regex…] — every regex must match somewhere in the file
|
|
24
|
+
// match.notMatch: regex — kill if this regex matches
|
|
25
|
+
// match.window: N — when allOf is set, all matches must fall within N lines
|
|
26
|
+
//
|
|
27
|
+
// `agentic-security rule test <fixture-glob>` runs every rule against every
|
|
28
|
+
// file and prints PASS/FAIL.
|
|
29
|
+
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import * as yaml from 'js-yaml';
|
|
33
|
+
import fg from 'fast-glob';
|
|
34
|
+
import { loadTrustedKeys, verifyRulePack } from './rule-pack-signing.js';
|
|
35
|
+
|
|
36
|
+
const LANG_EXTS = {
|
|
37
|
+
javascript: ['.js', '.mjs', '.cjs', '.jsx'],
|
|
38
|
+
typescript: ['.ts', '.tsx'],
|
|
39
|
+
python: ['.py'],
|
|
40
|
+
go: ['.go'],
|
|
41
|
+
ruby: ['.rb'],
|
|
42
|
+
java: ['.java'],
|
|
43
|
+
rust: ['.rs'],
|
|
44
|
+
csharp: ['.cs'],
|
|
45
|
+
php: ['.php'],
|
|
46
|
+
yaml: ['.yml', '.yaml'],
|
|
47
|
+
any: null,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function rulesDir(scanRoot) {
|
|
51
|
+
return path.join(scanRoot, '.agentic-security', 'rules');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function loadCustomRules(scanRoot) {
|
|
55
|
+
const dir = rulesDir(scanRoot);
|
|
56
|
+
const out = [];
|
|
57
|
+
if (!fs.existsSync(dir)) return out;
|
|
58
|
+
let files = [];
|
|
59
|
+
try {
|
|
60
|
+
files = fs.readdirSync(dir)
|
|
61
|
+
.filter(f => /\.(ya?ml)$/i.test(f))
|
|
62
|
+
.map(f => path.join(dir, f));
|
|
63
|
+
} catch { return out; }
|
|
64
|
+
const trustedKeys = loadTrustedKeys(scanRoot);
|
|
65
|
+
for (const fp of files) {
|
|
66
|
+
// Signature verification (PRD FR-DSL-2). Default: refuse unsigned rules
|
|
67
|
+
// unless AGENTIC_SECURITY_ALLOW_UNSIGNED_PACKS=1.
|
|
68
|
+
let unsignedAllowed = false;
|
|
69
|
+
let passThroughSigning = false;
|
|
70
|
+
const r = verifyRulePack(fp, trustedKeys);
|
|
71
|
+
if (r.ok && r.passThrough) {
|
|
72
|
+
// Pass-through mode (premortem 3R-4): empty bundled trust root, no
|
|
73
|
+
// project keys configured. Rule is accepted but tagged so SARIF can
|
|
74
|
+
// surface the audit gap.
|
|
75
|
+
passThroughSigning = true;
|
|
76
|
+
} else if (!r.ok) {
|
|
77
|
+
if (r.reason === 'unsigned' && r.allowUnsigned) {
|
|
78
|
+
console.error(`agentic-security: WARNING — loading UNSIGNED rule pack ${path.basename(fp)} (AGENTIC_SECURITY_ALLOW_UNSIGNED_PACKS=1). Findings will be tagged _unsigned:true.`);
|
|
79
|
+
unsignedAllowed = true;
|
|
80
|
+
} else if (r.reason === 'no-trusted-keys') {
|
|
81
|
+
if (process.env.AGENTIC_SECURITY_ALLOW_UNSIGNED_PACKS === '1') {
|
|
82
|
+
console.error(`agentic-security: WARNING — no trusted-keys.json; loading ${path.basename(fp)} unsigned-allowed.`);
|
|
83
|
+
unsignedAllowed = true;
|
|
84
|
+
} else {
|
|
85
|
+
console.error(`agentic-security: REFUSED rule pack ${path.basename(fp)} — no .agentic-security/trusted-keys.json. Set AGENTIC_SECURITY_ALLOW_UNSIGNED_PACKS=1 to override (audit-logged).`);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
console.error(`agentic-security: REFUSED rule pack ${path.basename(fp)} — ${r.reason}.`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
let raw;
|
|
94
|
+
try { raw = yaml.load(fs.readFileSync(fp, 'utf8')); } catch (e) {
|
|
95
|
+
console.error(`agentic-security: bad YAML in ${path.basename(fp)}: ${e.message}`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const list = Array.isArray(raw) ? raw : (raw?.rules ? raw.rules : [raw]);
|
|
99
|
+
for (const r of list) {
|
|
100
|
+
const norm = normalizeRule(r, fp);
|
|
101
|
+
if (norm) {
|
|
102
|
+
if (unsignedAllowed) norm._unsigned = true;
|
|
103
|
+
if (passThroughSigning) norm._passThroughSigning = true;
|
|
104
|
+
out.push(norm);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Reject patterns with nested quantifiers — the most common ReDoS shape.
|
|
112
|
+
// Catches (a+)+, (a*)*, (.+)+, (\w+)+, (a|b)+, (a+|b)* etc.
|
|
113
|
+
// Not exhaustive but eliminates the vast majority of catastrophic patterns.
|
|
114
|
+
const _REDOS_NESTED_RE = /\([^)]*[+*][^)]*\)[+*?{]/;
|
|
115
|
+
function _isSafePattern(p) {
|
|
116
|
+
return !_REDOS_NESTED_RE.test(p);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizeRule(r, fp) {
|
|
120
|
+
if (!r || !r.id || !r.match) return null;
|
|
121
|
+
const m = r.match;
|
|
122
|
+
let patterns = [];
|
|
123
|
+
if (typeof m.pattern === 'string') patterns = [m.pattern];
|
|
124
|
+
else if (Array.isArray(m.allOf)) patterns = m.allOf;
|
|
125
|
+
else return null;
|
|
126
|
+
|
|
127
|
+
// ReDoS guard: reject patterns with nested quantifiers before compiling.
|
|
128
|
+
for (const p of patterns) {
|
|
129
|
+
if (!_isSafePattern(p)) {
|
|
130
|
+
console.error(`agentic-security: rejected potentially unsafe regex in ${r.id} (nested quantifiers): ${p.slice(0, 80)}`);
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
let regexes;
|
|
135
|
+
try { regexes = patterns.map(p => new RegExp(p, 'gm')); }
|
|
136
|
+
catch (e) {
|
|
137
|
+
console.error(`agentic-security: invalid regex in ${r.id}: ${e.message}`);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
let notMatch = null;
|
|
141
|
+
if (m.notMatch) { try { notMatch = new RegExp(m.notMatch, 'm'); } catch {} }
|
|
142
|
+
|
|
143
|
+
// SentQL extensions (Sentinel-parity FR-DSL-1):
|
|
144
|
+
// llm_validate: { prompt, min_confidence }
|
|
145
|
+
// path: { must_traverse: [...], must_not_traverse: [...] }
|
|
146
|
+
let llmValidate = null;
|
|
147
|
+
if (r.llm_validate && typeof r.llm_validate === 'object') {
|
|
148
|
+
const minC = typeof r.llm_validate.min_confidence === 'number' ? r.llm_validate.min_confidence : 0.7;
|
|
149
|
+
llmValidate = {
|
|
150
|
+
prompt: String(r.llm_validate.prompt || 'Is this exploitable as described? Reply yes|no|maybe.').slice(0, 1000),
|
|
151
|
+
minConfidence: Math.max(0, Math.min(1, minC)),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
let pathConstraints = null;
|
|
155
|
+
if (r.path && typeof r.path === 'object') {
|
|
156
|
+
pathConstraints = {
|
|
157
|
+
mustTraverse: Array.isArray(r.path.must_traverse) ? r.path.must_traverse.map(String) : [],
|
|
158
|
+
mustNotTraverse: Array.isArray(r.path.must_not_traverse) ? r.path.must_not_traverse.map(String) : [],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
id: r.id,
|
|
163
|
+
title: r.title || r.id,
|
|
164
|
+
severity: r.severity || 'medium',
|
|
165
|
+
cwe: r.cwe || '',
|
|
166
|
+
message: r.message || r.title || r.id,
|
|
167
|
+
remediation: r.remediation || '',
|
|
168
|
+
languages: Array.isArray(r.languages) ? r.languages : (r.languages ? [r.languages] : ['any']),
|
|
169
|
+
shadow: r.shadow === true,
|
|
170
|
+
regexes,
|
|
171
|
+
notMatch,
|
|
172
|
+
requireAll: Array.isArray(m.allOf),
|
|
173
|
+
windowLines: m.window || 50,
|
|
174
|
+
sourceFile: fp,
|
|
175
|
+
llmValidate,
|
|
176
|
+
pathConstraints,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function fileMatchesLang(file, languages) {
|
|
181
|
+
if (!languages || languages.includes('any')) return true;
|
|
182
|
+
const ext = path.extname(file).toLowerCase();
|
|
183
|
+
for (const lang of languages) {
|
|
184
|
+
const exts = LANG_EXTS[lang];
|
|
185
|
+
if (!exts) continue;
|
|
186
|
+
if (exts.includes(ext)) return true;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Run a rule against a (file, content) pair and return Finding[].
|
|
192
|
+
export function runRule(rule, file, content) {
|
|
193
|
+
if (!fileMatchesLang(file, rule.languages)) return [];
|
|
194
|
+
if (rule.notMatch && rule.notMatch.test(content)) return [];
|
|
195
|
+
|
|
196
|
+
// Pre-compute line-offset table for fast line-number lookups.
|
|
197
|
+
const lines = content.split('\n');
|
|
198
|
+
const lineStarts = [0];
|
|
199
|
+
for (let i = 0; i < lines.length; i++) lineStarts.push(lineStarts[i] + lines[i].length + 1);
|
|
200
|
+
const offsetToLine = (off) => {
|
|
201
|
+
let lo = 0, hi = lineStarts.length - 1;
|
|
202
|
+
while (lo < hi) {
|
|
203
|
+
const mid = (lo + hi) >> 1;
|
|
204
|
+
if (lineStarts[mid + 1] <= off) lo = mid + 1; else hi = mid;
|
|
205
|
+
}
|
|
206
|
+
return lo + 1;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const EXEC_TIMEOUT_MS = 200;
|
|
210
|
+
const matches = rule.regexes.map(rx => {
|
|
211
|
+
const list = [];
|
|
212
|
+
rx.lastIndex = 0;
|
|
213
|
+
const deadline = Date.now() + EXEC_TIMEOUT_MS;
|
|
214
|
+
let m; while ((m = rx.exec(content)) !== null) {
|
|
215
|
+
list.push({ index: m.index, line: offsetToLine(m.index), text: m[0] });
|
|
216
|
+
if (m.index === rx.lastIndex) rx.lastIndex++;
|
|
217
|
+
if (Date.now() > deadline) {
|
|
218
|
+
console.error(`agentic-security: custom rule ${rule.id} regex timed out (>${EXEC_TIMEOUT_MS}ms) on ${file} — skipping`);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return list;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
const findings = [];
|
|
226
|
+
if (rule.requireAll) {
|
|
227
|
+
if (matches.some(l => l.length === 0)) return [];
|
|
228
|
+
// Pick the earliest match in the first list, verify the others fall within window.
|
|
229
|
+
const anchor = matches[0][0];
|
|
230
|
+
const close = matches.slice(1).every(list =>
|
|
231
|
+
list.some(x => Math.abs(x.line - anchor.line) <= rule.windowLines)
|
|
232
|
+
);
|
|
233
|
+
if (!close) return [];
|
|
234
|
+
findings.push(toFinding(rule, file, anchor));
|
|
235
|
+
} else {
|
|
236
|
+
for (const m of matches[0]) findings.push(toFinding(rule, file, m));
|
|
237
|
+
}
|
|
238
|
+
return findings;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function toFinding(rule, file, m) {
|
|
242
|
+
return {
|
|
243
|
+
id: `custom:${rule.id}:${file}:${m.line}`,
|
|
244
|
+
title: rule.title,
|
|
245
|
+
vuln: rule.title,
|
|
246
|
+
severity: rule.severity,
|
|
247
|
+
file,
|
|
248
|
+
line: m.line,
|
|
249
|
+
cwe: rule.cwe,
|
|
250
|
+
description: rule.message,
|
|
251
|
+
remediation: rule.remediation,
|
|
252
|
+
snippet: m.text,
|
|
253
|
+
confidence: 0.9,
|
|
254
|
+
source: 'custom-rule',
|
|
255
|
+
customRuleId: rule.id,
|
|
256
|
+
// SentQL extensions — annotate the finding so the engine's downstream
|
|
257
|
+
// LLM-validator and reachability-filter passes can act on them.
|
|
258
|
+
...(rule.llmValidate ? { _llmValidate: rule.llmValidate } : {}),
|
|
259
|
+
...(rule.pathConstraints ? { _pathConstraints: rule.pathConstraints } : {}),
|
|
260
|
+
...(rule.shadow ? { _shadow: true } : {}),
|
|
261
|
+
// Premortem 2R3.4 / 2R-8: carry the rule's unsigned tag onto the finding
|
|
262
|
+
// so SARIF emit / report renderers can show provenance.
|
|
263
|
+
...(rule._unsigned ? { _unsigned: true } : {}),
|
|
264
|
+
// Premortem 3R-4: same channel for pass-through signing.
|
|
265
|
+
...(rule._passThroughSigning ? { _passThroughSigning: true } : {}),
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Run every loaded rule across every file. Used by the engine as a final pass.
|
|
270
|
+
// Returns only non-shadow findings. Shadow findings (rule.shadow=true) are
|
|
271
|
+
// written to .agentic-security/shadow-findings.json so they can be reviewed
|
|
272
|
+
// without blocking CI gates or polluting the main findings list.
|
|
273
|
+
// Premortem 4R-12: the deadline is now per-scanRoot, accumulating across
|
|
274
|
+
// `applyCustomRules()` calls within a single process. A long-running scanner
|
|
275
|
+
// (LSP server, MCP daemon) that calls applyCustomRules per file would
|
|
276
|
+
// otherwise get a fresh 30s budget per call, defeating the global cap.
|
|
277
|
+
// Call `resetCustomRulesBudget(scanRoot)` between scan sessions.
|
|
278
|
+
const _customRulesBudget = new Map(); // scanRoot → { startedAt, exhausted }
|
|
279
|
+
|
|
280
|
+
export function resetCustomRulesBudget(scanRoot) {
|
|
281
|
+
if (scanRoot) _customRulesBudget.delete(scanRoot);
|
|
282
|
+
else _customRulesBudget.clear();
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function applyCustomRules(scanRoot, fileContents) {
|
|
286
|
+
const rules = loadCustomRules(scanRoot);
|
|
287
|
+
if (!rules.length) return [];
|
|
288
|
+
const out = [];
|
|
289
|
+
const shadow = [];
|
|
290
|
+
// Premortem 3R-8: a global per-scan deadline at the top of the outer for.
|
|
291
|
+
// Each rule's regex carries a 200ms per-regex budget (runRule), but in the
|
|
292
|
+
// worst case (100 files × N rules × 200ms ReDoS), the wall time blows up.
|
|
293
|
+
// Cap the total at 30s by default, configurable via env. Surfaces an audit
|
|
294
|
+
// line when exhausted so an operator can spot the runaway rule.
|
|
295
|
+
const globalDeadlineMs = parseInt(process.env.AGENTIC_SECURITY_CUSTOM_RULES_BUDGET_MS || '30000', 10);
|
|
296
|
+
let state = _customRulesBudget.get(scanRoot);
|
|
297
|
+
if (!state) {
|
|
298
|
+
state = { startedAt: Date.now(), exhausted: false };
|
|
299
|
+
_customRulesBudget.set(scanRoot, state);
|
|
300
|
+
}
|
|
301
|
+
const startedAt = state.startedAt;
|
|
302
|
+
let exhausted = state.exhausted;
|
|
303
|
+
for (const [file, content] of Object.entries(fileContents)) {
|
|
304
|
+
if (Date.now() - startedAt > globalDeadlineMs) {
|
|
305
|
+
if (!exhausted) {
|
|
306
|
+
console.error(`agentic-security: custom-rules global deadline (${globalDeadlineMs}ms) exhausted — skipping remaining files. Investigate slow rules or raise AGENTIC_SECURITY_CUSTOM_RULES_BUDGET_MS.`);
|
|
307
|
+
exhausted = true;
|
|
308
|
+
state.exhausted = true;
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
if (!content || content.length > 500_000) continue;
|
|
313
|
+
for (const r of rules) {
|
|
314
|
+
const found = runRule(r, file, content);
|
|
315
|
+
if (r.shadow) shadow.push(...found);
|
|
316
|
+
else out.push(...found);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (shadow.length) {
|
|
320
|
+
try {
|
|
321
|
+
const stateDir = path.join(scanRoot, '.agentic-security');
|
|
322
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
323
|
+
fs.writeFileSync(
|
|
324
|
+
path.join(stateDir, 'shadow-findings.json'),
|
|
325
|
+
JSON.stringify({ generatedAt: new Date().toISOString(), findings: shadow }, null, 2),
|
|
326
|
+
);
|
|
327
|
+
} catch { /* non-fatal */ }
|
|
328
|
+
}
|
|
329
|
+
return out;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// `agentic-security rule test <fixture-glob>` — runs all loaded rules against
|
|
333
|
+
// the given fixture files and prints which rule fired on which file.
|
|
334
|
+
export async function runRuleTests(scanRoot, fixtureGlob) {
|
|
335
|
+
const rules = loadCustomRules(scanRoot);
|
|
336
|
+
if (!rules.length) {
|
|
337
|
+
console.log(`No custom rules found in ${rulesDir(scanRoot)}`);
|
|
338
|
+
return { ok: true, rules: 0, fired: 0 };
|
|
339
|
+
}
|
|
340
|
+
const files = await fg(fixtureGlob, { dot: false, onlyFiles: true });
|
|
341
|
+
console.log(`Loaded ${rules.length} rule(s); testing against ${files.length} file(s).\n`);
|
|
342
|
+
let fired = 0;
|
|
343
|
+
for (const fp of files) {
|
|
344
|
+
let content = '';
|
|
345
|
+
try { content = fs.readFileSync(fp, 'utf8'); } catch { continue; }
|
|
346
|
+
const expectFire = /vulnerable/i.test(fp);
|
|
347
|
+
const expectClean = /clean/i.test(fp);
|
|
348
|
+
for (const r of rules) {
|
|
349
|
+
const findings = runRule(r, fp, content);
|
|
350
|
+
if (findings.length) {
|
|
351
|
+
fired++;
|
|
352
|
+
const verdict = expectClean ? 'FAIL (false positive)' : 'PASS';
|
|
353
|
+
console.log(` [${verdict}] ${r.id} → ${fp}:${findings[0].line}`);
|
|
354
|
+
} else if (expectFire) {
|
|
355
|
+
console.log(` [FAIL (missed)] ${r.id} → ${fp}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
console.log(`\n${fired} match(es) across ${files.length} file(s).`);
|
|
360
|
+
return { ok: true, rules: rules.length, fired };
|
|
361
|
+
}
|