@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,198 @@
|
|
|
1
|
+
// Rule pack overrides (R9). Pro users edit .agentic-security/rules.yml to:
|
|
2
|
+
// - severityOverrides: per-rule severity remap
|
|
3
|
+
// - disable: list of rule vuln strings or rule IDs to skip entirely
|
|
4
|
+
// - custom: user-defined regex rules (with vuln/severity/cwe/fix metadata)
|
|
5
|
+
// - version: pin to a specific scanner version for reproducibility
|
|
6
|
+
//
|
|
7
|
+
// Engine integration: scanner consults this module after producing findings.
|
|
8
|
+
// `applyOverrides(findings, scanRoot)` returns a filtered/remapped list.
|
|
9
|
+
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import * as yaml from 'js-yaml';
|
|
13
|
+
import { verifyLastScan } from './integrity.js';
|
|
14
|
+
|
|
15
|
+
const OVERRIDES_PATH = '.agentic-security/rules.yml';
|
|
16
|
+
|
|
17
|
+
function _path(scanRoot) {
|
|
18
|
+
return path.join(scanRoot || process.cwd(), OVERRIDES_PATH);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function loadOverrides(scanRoot) {
|
|
22
|
+
const fp = _path(scanRoot);
|
|
23
|
+
if (!fs.existsSync(fp)) return {};
|
|
24
|
+
try {
|
|
25
|
+
const raw = yaml.load(fs.readFileSync(fp, 'utf8')) || {};
|
|
26
|
+
return {
|
|
27
|
+
version: raw.version || null,
|
|
28
|
+
severityOverrides: raw.severityOverrides || {},
|
|
29
|
+
disable: Array.isArray(raw.disable) ? raw.disable : [],
|
|
30
|
+
custom: Array.isArray(raw.custom) ? raw.custom : [],
|
|
31
|
+
ignorePaths: Array.isArray(raw.ignorePaths) ? raw.ignorePaths : [],
|
|
32
|
+
};
|
|
33
|
+
} catch (_) { return {}; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validate the user's rules.yml. Returns { ok, errors[] }.
|
|
37
|
+
export function validateOverrides(scanRoot) {
|
|
38
|
+
const errors = [];
|
|
39
|
+
const o = loadOverrides(scanRoot);
|
|
40
|
+
if (o.severityOverrides) {
|
|
41
|
+
for (const [vuln, sev] of Object.entries(o.severityOverrides)) {
|
|
42
|
+
if (!['critical', 'high', 'medium', 'low', 'info'].includes(sev)) {
|
|
43
|
+
errors.push(`severityOverrides["${vuln}"]: invalid severity "${sev}"`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (o.custom) {
|
|
48
|
+
for (let i = 0; i < o.custom.length; i++) {
|
|
49
|
+
const c = o.custom[i];
|
|
50
|
+
if (!c.id) errors.push(`custom[${i}]: missing id`);
|
|
51
|
+
if (!c.regex) errors.push(`custom[${i}]: missing regex`);
|
|
52
|
+
else { try { new RegExp(c.regex); } catch (e) { errors.push(`custom[${i}]: bad regex: ${e.message}`); } }
|
|
53
|
+
if (!c.vuln) errors.push(`custom[${i}]: missing vuln`);
|
|
54
|
+
if (!c.severity) errors.push(`custom[${i}]: missing severity`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { ok: errors.length === 0, errors };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Premortem #4 — gate disable: entries on a sibling signature or explicit
|
|
61
|
+
// opt-out. `disable:` silently suppresses rules; without a guard, a PR that
|
|
62
|
+
// adds `disable: [cmd-injection-nodejs]` ships and the scanner stops firing
|
|
63
|
+
// on that family. By default we now REFUSE to honor `disable:` unless:
|
|
64
|
+
// 1. .agentic-security/rules.yml.sig exists and verifies the file body
|
|
65
|
+
// with the same HMAC key the rest of the engine uses, OR
|
|
66
|
+
// 2. AGENTIC_SECURITY_RULES_UNSIGNED=1 is set (developer escape hatch —
|
|
67
|
+
// this should NEVER be set in CI for an outside contribution).
|
|
68
|
+
// `severityOverrides`, `custom`, and `ignorePaths` remain in effect either
|
|
69
|
+
// way: they don't reduce coverage, they only add or remap.
|
|
70
|
+
function _disableAllowed(scanRoot) {
|
|
71
|
+
if (process.env.AGENTIC_SECURITY_RULES_UNSIGNED === '1') return { ok: true, reason: 'unsigned-opt-in' };
|
|
72
|
+
const fp = _path(scanRoot);
|
|
73
|
+
if (!fs.existsSync(fp)) return { ok: true, reason: 'no-rules-file' };
|
|
74
|
+
let body;
|
|
75
|
+
try { body = fs.readFileSync(fp, 'utf8'); }
|
|
76
|
+
catch { return { ok: false, reason: 'rules-unreadable' }; }
|
|
77
|
+
const sigPath = fp + '.sig';
|
|
78
|
+
if (!fs.existsSync(sigPath)) return { ok: false, reason: 'no-signature' };
|
|
79
|
+
const ok = verifyLastScan(body, sigPath);
|
|
80
|
+
return { ok: ok === true, reason: ok === true ? 'signed' : 'bad-signature' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Apply severity overrides + disable filter to a finding list.
|
|
84
|
+
// Harness-engineering note (post-derived): validate the overrides file BEFORE
|
|
85
|
+
// honoring any of it. If the YAML has a malformed severity, a broken custom
|
|
86
|
+
// regex, or any other syntax error, refuse the entire file and surface the
|
|
87
|
+
// reason on stderr. The previous shape "load, ignore broken entries, apply
|
|
88
|
+
// the rest" is exactly the silent-failure mode the post warns against.
|
|
89
|
+
export function applyOverrides(findings, scanRoot) {
|
|
90
|
+
const o = loadOverrides(scanRoot);
|
|
91
|
+
if (!o || (!o.severityOverrides && !o.disable?.length)) return findings;
|
|
92
|
+
const v = validateOverrides(scanRoot);
|
|
93
|
+
if (!v.ok) {
|
|
94
|
+
if (!globalThis.__as_overrides_warned) {
|
|
95
|
+
globalThis.__as_overrides_warned = true;
|
|
96
|
+
try {
|
|
97
|
+
process.stderr.write(`agentic-security: ignoring .agentic-security/rules.yml — validation failed:\n`);
|
|
98
|
+
for (const e of v.errors) process.stderr.write(` · ${e}\n`);
|
|
99
|
+
process.stderr.write(` Fix the errors above, or remove rules.yml. Findings will be returned unfiltered.\n`);
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
return findings;
|
|
103
|
+
}
|
|
104
|
+
let disable;
|
|
105
|
+
if (o.disable?.length) {
|
|
106
|
+
const gate = _disableAllowed(scanRoot);
|
|
107
|
+
if (gate.ok) {
|
|
108
|
+
disable = new Set(o.disable);
|
|
109
|
+
} else {
|
|
110
|
+
// Refused. Surface the reason on stderr once per process so operators
|
|
111
|
+
// who *meant* to disable a rule see why it didn't take.
|
|
112
|
+
if (!globalThis.__as_disable_warned) {
|
|
113
|
+
globalThis.__as_disable_warned = true;
|
|
114
|
+
try {
|
|
115
|
+
process.stderr.write(`agentic-security: ignoring 'disable:' in rules.yml — ${gate.reason}. Sign rules.yml with \`agentic-security rules sign\` or set AGENTIC_SECURITY_RULES_UNSIGNED=1 to opt in.\n`);
|
|
116
|
+
} catch {}
|
|
117
|
+
}
|
|
118
|
+
disable = new Set();
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
disable = new Set();
|
|
122
|
+
}
|
|
123
|
+
const sevMap = o.severityOverrides || {};
|
|
124
|
+
return findings
|
|
125
|
+
.filter(f => !disable.has(f.vuln) && !disable.has(f.id))
|
|
126
|
+
.map(f => sevMap[f.vuln] ? { ...f, severity: sevMap[f.vuln] } : f);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Cache: compiled custom rules per scanRoot. Validated at first call;
|
|
130
|
+
// subsequent calls for the same scan reuse the compiled regexes.
|
|
131
|
+
const _compiledCustomRules = new Map(); // scanRoot → { compiled[], errors[] }
|
|
132
|
+
|
|
133
|
+
function _compileCustomRules(scanRoot) {
|
|
134
|
+
if (_compiledCustomRules.has(scanRoot)) return _compiledCustomRules.get(scanRoot);
|
|
135
|
+
const o = loadOverrides(scanRoot);
|
|
136
|
+
const result = { compiled: [], errors: [] };
|
|
137
|
+
if (!o.custom || !o.custom.length) {
|
|
138
|
+
_compiledCustomRules.set(scanRoot, result);
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
for (let i = 0; i < o.custom.length; i++) {
|
|
142
|
+
const rule = o.custom[i];
|
|
143
|
+
if (!rule.id) { result.errors.push(`custom[${i}]: missing id`); continue; }
|
|
144
|
+
if (!rule.regex) { result.errors.push(`custom[${i}] (${rule.id}): missing regex`); continue; }
|
|
145
|
+
if (!rule.vuln) { result.errors.push(`custom[${i}] (${rule.id}): missing vuln`); continue; }
|
|
146
|
+
if (!rule.severity) { result.errors.push(`custom[${i}] (${rule.id}): missing severity`); continue; }
|
|
147
|
+
try {
|
|
148
|
+
const re = new RegExp(rule.regex, rule.flags || 'g');
|
|
149
|
+
result.compiled.push({ rule, re });
|
|
150
|
+
} catch (e) {
|
|
151
|
+
result.errors.push(`custom[${i}] (${rule.id}): bad regex: ${e.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
_compiledCustomRules.set(scanRoot, result);
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Run user-defined custom regex rules against a file. Returns custom findings.
|
|
159
|
+
//
|
|
160
|
+
// Harness-engineering note (post-derived): rules are compiled and validated
|
|
161
|
+
// at first invocation per scan; if ANY rule fails to compile, that rule is
|
|
162
|
+
// excluded AND surfaced via stderr — never silently skipped per-call.
|
|
163
|
+
export function runCustomRules(filePath, fileContent, scanRoot) {
|
|
164
|
+
const { compiled, errors } = _compileCustomRules(scanRoot);
|
|
165
|
+
if (errors.length && !globalThis.__as_custom_rules_warned) {
|
|
166
|
+
globalThis.__as_custom_rules_warned = true;
|
|
167
|
+
try {
|
|
168
|
+
process.stderr.write(`agentic-security: ${errors.length} custom rule(s) in .agentic-security/rules.yml failed to compile and were skipped:\n`);
|
|
169
|
+
for (const e of errors) process.stderr.write(` · ${e}\n`);
|
|
170
|
+
} catch {}
|
|
171
|
+
}
|
|
172
|
+
if (!compiled.length) return [];
|
|
173
|
+
const lines = fileContent.split('\n');
|
|
174
|
+
const out = [];
|
|
175
|
+
for (const { rule, re } of compiled) {
|
|
176
|
+
re.lastIndex = 0; // each file starts fresh for global regexes
|
|
177
|
+
let m;
|
|
178
|
+
while ((m = re.exec(fileContent)) !== null) {
|
|
179
|
+
const lineNum = fileContent.substring(0, m.index).split('\n').length;
|
|
180
|
+
out.push({
|
|
181
|
+
id: `custom:${rule.id}:${filePath}:${lineNum}`,
|
|
182
|
+
vuln: rule.vuln,
|
|
183
|
+
severity: rule.severity,
|
|
184
|
+
cwe: rule.cwe || '',
|
|
185
|
+
stride: rule.stride || '',
|
|
186
|
+
file: filePath,
|
|
187
|
+
line: lineNum,
|
|
188
|
+
snippet: lines[lineNum - 1]?.trim() || m[0],
|
|
189
|
+
fix: rule.fix || '',
|
|
190
|
+
description: rule.description || '',
|
|
191
|
+
custom: true,
|
|
192
|
+
parser: 'CUSTOM_RULE',
|
|
193
|
+
});
|
|
194
|
+
if (!re.global) break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Signed rule-pack verification (Sentinel-parity PRD FR-DSL-2; hardened in
|
|
2
|
+
// premortems R3.1, 2R3.1, 2R3.2).
|
|
3
|
+
//
|
|
4
|
+
// Threat model: a malicious PR drops a `.agentic-security/rules/foo.yml`
|
|
5
|
+
// into the repo. The next scanner run loads it and:
|
|
6
|
+
// - The rule's regex contains a ReDoS payload → hangs CI.
|
|
7
|
+
// - The rule fires custom-rule findings with attacker-controlled fix
|
|
8
|
+
// replacement strings → potential supply-chain attack via /fix.
|
|
9
|
+
// - The rule's llm_validate prompt exfiltrates context to an attacker
|
|
10
|
+
// endpoint.
|
|
11
|
+
//
|
|
12
|
+
// Defense (multi-layer):
|
|
13
|
+
//
|
|
14
|
+
// 1. Every rule file must be Ed25519-signed; the signature lives at
|
|
15
|
+
// `<rulefile>.sig` (raw 64 bytes).
|
|
16
|
+
//
|
|
17
|
+
// 2. The TRUST ROOT is bundled with the scanner code (BUNDLED_OFFICIAL_KEYS
|
|
18
|
+
// below), NOT read from the project tree by default. An attacker who
|
|
19
|
+
// can drop a `.agentic-security/trusted-keys.json` cannot bootstrap
|
|
20
|
+
// their own key into trust. Project-local keys are honored ONLY when
|
|
21
|
+
// AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1 (audit-logged).
|
|
22
|
+
//
|
|
23
|
+
// 3. Keys carry an optional `revokedAt` timestamp. A signature is rejected
|
|
24
|
+
// when the rule-file's mtime postdates the revocation, OR the signature's
|
|
25
|
+
// SHA-256 hash appears in the project's `crl[]` array.
|
|
26
|
+
//
|
|
27
|
+
// 4. Unsigned packs refused unless AGENTIC_SECURITY_ALLOW_UNSIGNED_PACKS=1
|
|
28
|
+
// (audit-logged + findings tagged `_unsigned: true`).
|
|
29
|
+
|
|
30
|
+
import * as fs from 'node:fs';
|
|
31
|
+
import * as path from 'node:path';
|
|
32
|
+
import * as crypto from 'node:crypto';
|
|
33
|
+
|
|
34
|
+
const TRUSTED_KEYS_FILE = '.agentic-security/trusted-keys.json';
|
|
35
|
+
|
|
36
|
+
// Built-in trust root. These are the keys the maintainers of agentic-security
|
|
37
|
+
// use to sign official rule packs. Production deployment requires the
|
|
38
|
+
// maintainers to generate a real keypair, distribute the private key offline,
|
|
39
|
+
// and ship the corresponding public key here on a release. Until then the
|
|
40
|
+
// effective behavior is "no official keys, unsigned-only via the opt-in env."
|
|
41
|
+
export const BUNDLED_OFFICIAL_KEYS = [
|
|
42
|
+
// {
|
|
43
|
+
// id: 'agentic-security-official-2026-q1',
|
|
44
|
+
// alg: 'ed25519',
|
|
45
|
+
// publicKey: '<base64-32-bytes>',
|
|
46
|
+
// issuedAt: '2026-01-01T00:00:00Z',
|
|
47
|
+
// revokedAt: null,
|
|
48
|
+
// },
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function _trustedKeysPath(scanRoot) {
|
|
52
|
+
return path.join(scanRoot || process.cwd(), TRUSTED_KEYS_FILE);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Load the EFFECTIVE trusted-key set. Composition:
|
|
56
|
+
// 1. Always: BUNDLED_OFFICIAL_KEYS (built into the scanner code).
|
|
57
|
+
// 2. When AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1: union with
|
|
58
|
+
// .agentic-security/trusted-keys.json from the project tree (logged).
|
|
59
|
+
//
|
|
60
|
+
// CRL: trusted-keys.json may also carry a top-level `crl` array of revoked
|
|
61
|
+
// signature hashes (sha256 of the signature bytes). These apply project-
|
|
62
|
+
// locally regardless of opt-in.
|
|
63
|
+
export function loadTrustedKeys(scanRoot) {
|
|
64
|
+
const keys = [...BUNDLED_OFFICIAL_KEYS];
|
|
65
|
+
let projectCrl = [];
|
|
66
|
+
const fp = _trustedKeysPath(scanRoot);
|
|
67
|
+
if (fs.existsSync(fp)) {
|
|
68
|
+
let data;
|
|
69
|
+
try { data = JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { data = null; }
|
|
70
|
+
if (data && Array.isArray(data.crl)) projectCrl = data.crl.filter(x => typeof x === 'string');
|
|
71
|
+
if (data && Array.isArray(data.keys)) {
|
|
72
|
+
if (process.env.AGENTIC_SECURITY_ALLOW_PROJECT_KEYS === '1') {
|
|
73
|
+
console.error('agentic-security: WARNING — project-local trusted-keys.json honored (AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1). An attacker who can write to .agentic-security/ can bypass signing — use only on trusted workstations.');
|
|
74
|
+
for (const k of data.keys) {
|
|
75
|
+
if (k && k.publicKey && k.alg === 'ed25519') keys.push(k);
|
|
76
|
+
}
|
|
77
|
+
} else if (data.keys.length > 0) {
|
|
78
|
+
console.error('agentic-security: ignoring project-local trusted-keys.json (set AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1 to honor; audit-logged).');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
Object.defineProperty(keys, '_crl', { value: projectCrl, enumerable: false });
|
|
83
|
+
return keys;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Pass-through warning issued at most once per process.
|
|
87
|
+
let _passThroughWarned = false;
|
|
88
|
+
|
|
89
|
+
// Verify a rule-pack file. Returns one of:
|
|
90
|
+
// { ok: true, keyId: '<id>' } // signature valid
|
|
91
|
+
// { ok: true, passThrough: true } // bundled trust root empty AND no project keys — pass-through (premortem 3R3.1)
|
|
92
|
+
// { ok: false, reason: 'unsigned', allowUnsigned: bool } // no sig file
|
|
93
|
+
// { ok: false, reason: 'bad-signature' } // sig present but invalid
|
|
94
|
+
// { ok: false, reason: 'no-trusted-keys' } // no keys configured (no bundled, no project)
|
|
95
|
+
// { ok: false, reason: 'revoked-key', keyId } // key revoked + rule mtime > revokedAt
|
|
96
|
+
// { ok: false, reason: 'revoked-signature' } // signature SHA in project CRL
|
|
97
|
+
// { ok: false, reason: 'read-error' }
|
|
98
|
+
//
|
|
99
|
+
// PREMORTEM 3R3.1 — pass-through mode. When BUNDLED_OFFICIAL_KEYS is empty
|
|
100
|
+
// (today's reality during product bring-up) AND the operator has not opted
|
|
101
|
+
// into project-local keys, REFUSING every rule pack trains operators to set
|
|
102
|
+
// AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1 permanently — which recreates the
|
|
103
|
+
// exact threat model signing was supposed to defend. Instead we accept
|
|
104
|
+
// rule packs in pass-through mode with a one-time warning + a _passThrough
|
|
105
|
+
// flag on each accepted rule. Operators get visibility AND the gate doesn't
|
|
106
|
+
// train them to bypass.
|
|
107
|
+
export function verifyRulePack(rulePackPath, trustedKeys) {
|
|
108
|
+
const sigPath = rulePackPath + '.sig';
|
|
109
|
+
if (!fs.existsSync(sigPath)) {
|
|
110
|
+
return { ok: false, reason: 'unsigned', allowUnsigned: process.env.AGENTIC_SECURITY_ALLOW_UNSIGNED_PACKS === '1' };
|
|
111
|
+
}
|
|
112
|
+
if (!Array.isArray(trustedKeys) || trustedKeys.length === 0) {
|
|
113
|
+
// Pass-through mode: empty bundled trust root + no project keys.
|
|
114
|
+
// Issue ONE warning per process, then accept with passThrough flag.
|
|
115
|
+
//
|
|
116
|
+
// Premortem 4R-1: CI mode is fail-closed. CI environments are the place
|
|
117
|
+
// where supply-chain compromise gets weaponized, and the per-session stderr
|
|
118
|
+
// warning is invisible there. So when CI=true (or any common CI env-var is
|
|
119
|
+
// set), refuse pass-through entirely. Operators can opt-in by setting
|
|
120
|
+
// AGENTIC_SECURITY_ALLOW_PASSTHROUGH_IN_CI=1 — making the bypass an
|
|
121
|
+
// intentional, auditable decision rather than the silent default.
|
|
122
|
+
const inCi = !!(
|
|
123
|
+
process.env.CI ||
|
|
124
|
+
process.env.GITHUB_ACTIONS ||
|
|
125
|
+
process.env.GITLAB_CI ||
|
|
126
|
+
process.env.BUILDKITE ||
|
|
127
|
+
process.env.CIRCLECI ||
|
|
128
|
+
process.env.JENKINS_URL ||
|
|
129
|
+
process.env.TF_BUILD
|
|
130
|
+
);
|
|
131
|
+
const allowPassThroughInCi = process.env.AGENTIC_SECURITY_ALLOW_PASSTHROUGH_IN_CI === '1';
|
|
132
|
+
if (BUNDLED_OFFICIAL_KEYS.length === 0 && process.env.AGENTIC_SECURITY_STRICT_SIGNING !== '1') {
|
|
133
|
+
if (inCi && !allowPassThroughInCi) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: 'pass-through-disabled-in-ci',
|
|
137
|
+
remediation: 'CI run detected with no signing keys configured. Either (a) set AGENTIC_SECURITY_ALLOW_PASSTHROUGH_IN_CI=1 to accept unsigned rule packs in this CI run, or (b) configure project keys in .agentic-security/trusted-keys.json and set AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1.',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (!_passThroughWarned) {
|
|
141
|
+
_passThroughWarned = true;
|
|
142
|
+
console.error('agentic-security: signed-rule-pack defense in PASS-THROUGH mode.');
|
|
143
|
+
console.error(' · No bundled official keys are baked into this release.');
|
|
144
|
+
console.error(' · Rule packs will be ACCEPTED, tagged _passThroughSigning:true.');
|
|
145
|
+
console.error(' · To switch to refuse-mode set AGENTIC_SECURITY_STRICT_SIGNING=1.');
|
|
146
|
+
console.error(' · To honor project-local trusted-keys.json, set AGENTIC_SECURITY_ALLOW_PROJECT_KEYS=1.');
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, passThrough: true };
|
|
149
|
+
}
|
|
150
|
+
return { ok: false, reason: 'no-trusted-keys' };
|
|
151
|
+
}
|
|
152
|
+
let body, sig, ruleMtime;
|
|
153
|
+
try {
|
|
154
|
+
body = fs.readFileSync(rulePackPath);
|
|
155
|
+
sig = fs.readFileSync(sigPath);
|
|
156
|
+
ruleMtime = fs.statSync(rulePackPath).mtime;
|
|
157
|
+
} catch { return { ok: false, reason: 'read-error' }; }
|
|
158
|
+
// CRL check first — independent of which key signed.
|
|
159
|
+
const sigHash = crypto.createHash('sha256').update(sig).digest('hex');
|
|
160
|
+
const crl = trustedKeys._crl || [];
|
|
161
|
+
if (crl.includes(sigHash)) return { ok: false, reason: 'revoked-signature' };
|
|
162
|
+
for (const k of trustedKeys) {
|
|
163
|
+
try {
|
|
164
|
+
const keyBytes = Buffer.from(k.publicKey, 'base64');
|
|
165
|
+
if (keyBytes.length !== 32) continue;
|
|
166
|
+
const keyObj = crypto.createPublicKey({
|
|
167
|
+
key: { kty: 'OKP', crv: 'Ed25519', x: keyBytes.toString('base64url') },
|
|
168
|
+
format: 'jwk',
|
|
169
|
+
});
|
|
170
|
+
const valid = crypto.verify(null, body, keyObj, sig);
|
|
171
|
+
if (!valid) continue;
|
|
172
|
+
// Signature is valid by this key. Check revocation.
|
|
173
|
+
if (k.revokedAt) {
|
|
174
|
+
const revokedAt = new Date(k.revokedAt);
|
|
175
|
+
if (Number.isFinite(revokedAt.getTime()) && ruleMtime > revokedAt) {
|
|
176
|
+
return { ok: false, reason: 'revoked-key', keyId: k.id || '(unnamed)' };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return { ok: true, keyId: k.id || '(unnamed)' };
|
|
180
|
+
} catch { /* try next key */ }
|
|
181
|
+
}
|
|
182
|
+
return { ok: false, reason: 'bad-signature' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// CLI helper — generate an Ed25519 key pair. Returns { publicKey, privateKey }
|
|
186
|
+
// as base64 strings.
|
|
187
|
+
export function keygen() {
|
|
188
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
189
|
+
const pubRaw = publicKey.export({ format: 'jwk' }).x; // base64url
|
|
190
|
+
const privRaw = privateKey.export({ format: 'jwk' }).d; // base64url
|
|
191
|
+
return {
|
|
192
|
+
publicKey: Buffer.from(pubRaw, 'base64url').toString('base64'),
|
|
193
|
+
privateKey: Buffer.from(privRaw, 'base64url').toString('base64'),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Sign a rule-pack file. Writes <path>.sig as raw 64 bytes.
|
|
198
|
+
export function signRulePack(rulePackPath, privateKeyB64) {
|
|
199
|
+
const body = fs.readFileSync(rulePackPath);
|
|
200
|
+
const privBytes = Buffer.from(privateKeyB64, 'base64');
|
|
201
|
+
if (privBytes.length !== 32) throw new Error('private key must be 32 bytes (raw ed25519)');
|
|
202
|
+
const keyObj = crypto.createPrivateKey({
|
|
203
|
+
key: { kty: 'OKP', crv: 'Ed25519', d: privBytes.toString('base64url') },
|
|
204
|
+
format: 'jwk',
|
|
205
|
+
});
|
|
206
|
+
const sig = crypto.sign(null, body, keyObj);
|
|
207
|
+
fs.writeFileSync(rulePackPath + '.sig', sig);
|
|
208
|
+
return sig;
|
|
209
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Curated rule packs. Each pack is a focused view of the engine's findings
|
|
2
|
+
// based on a CWE allowlist. Activated via `--pack <name>` on the CLI.
|
|
3
|
+
//
|
|
4
|
+
// Packs do not change *detection* — they filter the scan output to the rules
|
|
5
|
+
// the user wants to focus on, and optionally bump severity for matching rules.
|
|
6
|
+
|
|
7
|
+
export const PACKS = {
|
|
8
|
+
'owasp-top-10': {
|
|
9
|
+
description: 'OWASP Top 10 (2021) — A01 through A10',
|
|
10
|
+
cwes: [
|
|
11
|
+
// A01 Broken Access Control
|
|
12
|
+
'CWE-22', 'CWE-200', 'CWE-269', 'CWE-284', 'CWE-352', 'CWE-639', 'CWE-732', 'CWE-862', 'CWE-863',
|
|
13
|
+
// A02 Cryptographic Failures
|
|
14
|
+
'CWE-261', 'CWE-296', 'CWE-310', 'CWE-319', 'CWE-321', 'CWE-322', 'CWE-323', 'CWE-324',
|
|
15
|
+
'CWE-325', 'CWE-326', 'CWE-327', 'CWE-328', 'CWE-329', 'CWE-330', 'CWE-331', 'CWE-335',
|
|
16
|
+
'CWE-336', 'CWE-337', 'CWE-338', 'CWE-340', 'CWE-347', 'CWE-523', 'CWE-720', 'CWE-757',
|
|
17
|
+
'CWE-759', 'CWE-760', 'CWE-780', 'CWE-818', 'CWE-916',
|
|
18
|
+
// A03 Injection
|
|
19
|
+
'CWE-20', 'CWE-74', 'CWE-77', 'CWE-78', 'CWE-79', 'CWE-83', 'CWE-87', 'CWE-88', 'CWE-89',
|
|
20
|
+
'CWE-90', 'CWE-91', 'CWE-93', 'CWE-94', 'CWE-95', 'CWE-96', 'CWE-97', 'CWE-98', 'CWE-99',
|
|
21
|
+
'CWE-100', 'CWE-113', 'CWE-116', 'CWE-138', 'CWE-184', 'CWE-470', 'CWE-471', 'CWE-564',
|
|
22
|
+
'CWE-610', 'CWE-643', 'CWE-644', 'CWE-652', 'CWE-917', 'CWE-1336', 'CWE-1427',
|
|
23
|
+
// A04 Insecure Design
|
|
24
|
+
'CWE-209', 'CWE-256', 'CWE-501', 'CWE-522',
|
|
25
|
+
// A05 Security Misconfiguration
|
|
26
|
+
'CWE-2', 'CWE-11', 'CWE-13', 'CWE-15', 'CWE-16', 'CWE-260', 'CWE-315', 'CWE-520',
|
|
27
|
+
'CWE-526', 'CWE-537', 'CWE-541', 'CWE-547', 'CWE-611', 'CWE-614', 'CWE-756', 'CWE-776',
|
|
28
|
+
'CWE-942', 'CWE-1004', 'CWE-1032', 'CWE-1174',
|
|
29
|
+
// A06 Vulnerable and Outdated Components
|
|
30
|
+
'CWE-937', 'CWE-1035', 'CWE-1104',
|
|
31
|
+
// A07 Identification and Authentication Failures
|
|
32
|
+
'CWE-255', 'CWE-259', 'CWE-287', 'CWE-288', 'CWE-290', 'CWE-294', 'CWE-295', 'CWE-297',
|
|
33
|
+
'CWE-300', 'CWE-302', 'CWE-304', 'CWE-306', 'CWE-307', 'CWE-346', 'CWE-384', 'CWE-521',
|
|
34
|
+
'CWE-613', 'CWE-620', 'CWE-640', 'CWE-798',
|
|
35
|
+
// A08 Software and Data Integrity Failures
|
|
36
|
+
'CWE-345', 'CWE-353', 'CWE-426', 'CWE-494', 'CWE-502', 'CWE-565', 'CWE-784', 'CWE-829',
|
|
37
|
+
'CWE-830', 'CWE-915',
|
|
38
|
+
// A09 Security Logging and Monitoring Failures
|
|
39
|
+
'CWE-117', 'CWE-223', 'CWE-532', 'CWE-778',
|
|
40
|
+
// A10 Server-Side Request Forgery
|
|
41
|
+
'CWE-918',
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
'cwe-top-25': {
|
|
46
|
+
description: 'CWE Top 25 Most Dangerous Software Weaknesses (2023)',
|
|
47
|
+
cwes: [
|
|
48
|
+
'CWE-787', 'CWE-79', 'CWE-89', 'CWE-416', 'CWE-78', 'CWE-20', 'CWE-125', 'CWE-22',
|
|
49
|
+
'CWE-352', 'CWE-434', 'CWE-862', 'CWE-476', 'CWE-287', 'CWE-190', 'CWE-502', 'CWE-77',
|
|
50
|
+
'CWE-119', 'CWE-798', 'CWE-918', 'CWE-306', 'CWE-362', 'CWE-269', 'CWE-94', 'CWE-863',
|
|
51
|
+
'CWE-276',
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
'llm-security': {
|
|
56
|
+
description: 'LLM / agent / prompt-injection risks (OWASP LLM Top 10)',
|
|
57
|
+
cwes: [
|
|
58
|
+
'CWE-20', // Improper Input Validation (prompts)
|
|
59
|
+
'CWE-74', // Injection family
|
|
60
|
+
'CWE-77', // Command Injection (via tools)
|
|
61
|
+
'CWE-78', // OS Command Injection (via tools)
|
|
62
|
+
'CWE-79', // XSS (LLM-generated output)
|
|
63
|
+
'CWE-94', // Code Injection
|
|
64
|
+
'CWE-200', // Information Disclosure (model leakage)
|
|
65
|
+
'CWE-285', // Improper Authorization (over-privileged agents)
|
|
66
|
+
'CWE-285',
|
|
67
|
+
'CWE-352', // CSRF (agent state)
|
|
68
|
+
'CWE-494', // Download of Code Without Integrity Check (model weights)
|
|
69
|
+
'CWE-502', // Insecure Deserialization (pickle / torch.load)
|
|
70
|
+
'CWE-732', // Incorrect Permission Assignment (MCP tools)
|
|
71
|
+
'CWE-798', // Hardcoded credentials (in prompts / tools)
|
|
72
|
+
'CWE-918', // SSRF (LLM-controlled requests)
|
|
73
|
+
'CWE-1336', // Improper Neutralization of Special Elements (template injection)
|
|
74
|
+
'CWE-1357', // Reliance on Insufficiently Trustworthy Component
|
|
75
|
+
'CWE-1427', // LLM prompt injection (CWE assigned 2025)
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
'supply-chain': {
|
|
80
|
+
description: 'Dependency vulnerabilities, dep-confusion, pipeline & container risks',
|
|
81
|
+
cwes: [
|
|
82
|
+
'CWE-494', // Download of Code Without Integrity Check
|
|
83
|
+
'CWE-502', // Insecure Deserialization
|
|
84
|
+
'CWE-532', // Insertion of Sensitive Information into Log File
|
|
85
|
+
'CWE-693', // Protection Mechanism Failure
|
|
86
|
+
'CWE-829', // Inclusion of Functionality from Untrusted Control Sphere
|
|
87
|
+
'CWE-830', // Inclusion of Web Functionality from Untrusted Source
|
|
88
|
+
'CWE-915', // Improperly Controlled Modification of Dynamically-Determined Object Attributes
|
|
89
|
+
'CWE-937', // Using Components with Known Vulnerabilities
|
|
90
|
+
'CWE-1035', // OWASP A06 — Vulnerable and Outdated Components
|
|
91
|
+
'CWE-1104', // Use of Unmaintained Third-Party Components
|
|
92
|
+
'CWE-1188', // Insecure Default Initialization of Resource
|
|
93
|
+
'CWE-1336', // Template Injection
|
|
94
|
+
'CWE-1357', // Reliance on Insufficiently Trustworthy Component
|
|
95
|
+
'CWE-272', // Least Privilege Violation (CI permissions)
|
|
96
|
+
'CWE-78', // OS Command Injection (CI script injection)
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export function listPacks() {
|
|
102
|
+
return Object.entries(PACKS).map(([name, p]) => ({
|
|
103
|
+
name,
|
|
104
|
+
description: p.description,
|
|
105
|
+
cweCount: p.cwes.length,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function loadPack(name) {
|
|
110
|
+
const p = PACKS[name];
|
|
111
|
+
if (!p) {
|
|
112
|
+
const known = Object.keys(PACKS).join(', ');
|
|
113
|
+
throw new Error(`Unknown pack "${name}". Known: ${known}`);
|
|
114
|
+
}
|
|
115
|
+
return { name, ...p };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Return the set of CWE IDs covered by the given pack names.
|
|
119
|
+
// Multiple packs union their CWE sets.
|
|
120
|
+
export function packsCweSet(names) {
|
|
121
|
+
const set = new Set();
|
|
122
|
+
for (const n of names) {
|
|
123
|
+
const p = loadPack(n);
|
|
124
|
+
for (const c of p.cwes) set.add(c);
|
|
125
|
+
}
|
|
126
|
+
return set;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Filter a scan object's findings/secrets/supplyChain arrays to those whose CWE
|
|
130
|
+
// is present in the union of `packNames`' CWE sets. Returns a new scan object.
|
|
131
|
+
// If `packNames` is empty/falsy, returns the scan unchanged.
|
|
132
|
+
export function applyPacks(scan, packNames) {
|
|
133
|
+
if (!packNames || !packNames.length) return scan;
|
|
134
|
+
const cwes = packsCweSet(packNames);
|
|
135
|
+
const keep = (f) => f && f.cwe && cwes.has(f.cwe);
|
|
136
|
+
return {
|
|
137
|
+
...scan,
|
|
138
|
+
findings: (scan.findings || []).filter(keep),
|
|
139
|
+
secrets: (scan.secrets || []).filter(keep),
|
|
140
|
+
logicVulns: (scan.logicVulns || []).filter(keep),
|
|
141
|
+
supplyChain: (scan.supplyChain || []).filter(keep),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Auto-rule synthesis from repeated FPs (FR-LEARN-6).
|
|
2
|
+
//
|
|
3
|
+
// Reads `.agentic-security/triage-feedback.json` (populated by the /triage
|
|
4
|
+
// slash command). When 5+ findings sharing a similar shape get marked FP,
|
|
5
|
+
// propose a YAML suppression rule and write it to
|
|
6
|
+
// `.agentic-security/rules-proposed/auto-<timestamp>.yml`. The operator
|
|
7
|
+
// reviews and either drops it into `rules/` (active) or deletes it.
|
|
8
|
+
//
|
|
9
|
+
// Honest scope:
|
|
10
|
+
// - We propose, we don't auto-activate. The customer decides.
|
|
11
|
+
// - Similar-shape = same (rule_id or vuln-family) AND same file glob root.
|
|
12
|
+
// - Threshold = 5 occurrences by default (env override).
|
|
13
|
+
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
|
|
17
|
+
const TRIAGE_PATH = path.join('.agentic-security', 'triage-feedback.json');
|
|
18
|
+
const PROPOSED_DIR = path.join('.agentic-security', 'rules-proposed');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_FP_THRESHOLD = 5;
|
|
21
|
+
|
|
22
|
+
function _readTriage(scanRoot) {
|
|
23
|
+
const fp = path.join(scanRoot || process.cwd(), TRIAGE_PATH);
|
|
24
|
+
if (!fs.existsSync(fp)) return null;
|
|
25
|
+
try { return JSON.parse(fs.readFileSync(fp, 'utf8')); } catch { return null; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function _shapeKey(entry) {
|
|
29
|
+
// Group by rule/family + the dir prefix (first two path segments) so we
|
|
30
|
+
// suggest rules scoped to the same module — never project-wide.
|
|
31
|
+
const fam = entry.family || entry.cwe || entry.vuln || 'unknown';
|
|
32
|
+
const file = entry.file || '';
|
|
33
|
+
const dir = file.split('/').slice(0, 2).join('/') || '.';
|
|
34
|
+
return `${fam}::${dir}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function _summarizeGroup(entries) {
|
|
38
|
+
const e0 = entries[0];
|
|
39
|
+
return {
|
|
40
|
+
family: e0.family || null,
|
|
41
|
+
rule: e0.cwe || e0.vuln || 'unknown',
|
|
42
|
+
dirGlob: (e0.file || '').split('/').slice(0, 2).join('/') + '/**',
|
|
43
|
+
count: entries.length,
|
|
44
|
+
examples: entries.slice(0, 3).map(e => `${e.file}:${e.line}`),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _yamlProposal(group) {
|
|
49
|
+
const ruleId = `auto-suppress-${group.family || group.rule}-${Date.now().toString(36)}`;
|
|
50
|
+
return `# Auto-synthesised suppression proposal (FR-LEARN-6).
|
|
51
|
+
# Generated from ${group.count} false-positive verdicts on ${group.family || group.rule}
|
|
52
|
+
# in ${group.dirGlob}.
|
|
53
|
+
#
|
|
54
|
+
# Examples:
|
|
55
|
+
${group.examples.map(e => '# - ' + e).join('\n')}
|
|
56
|
+
#
|
|
57
|
+
# Review carefully BEFORE moving into rules/. This is a PROPOSAL.
|
|
58
|
+
|
|
59
|
+
- id: ${ruleId}
|
|
60
|
+
title: "Auto-suppress: ${group.family || group.rule}"
|
|
61
|
+
description: "Repeated FP verdicts in ${group.dirGlob}"
|
|
62
|
+
shadow: true # never blocks CI; safe by default
|
|
63
|
+
match:
|
|
64
|
+
family: ${group.family || group.rule}
|
|
65
|
+
paths:
|
|
66
|
+
- "${group.dirGlob}"
|
|
67
|
+
action: suppress
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Public entry: scan the triage history and emit a proposal YAML for any
|
|
73
|
+
* group with ≥ threshold FP verdicts. Returns the list of proposals written.
|
|
74
|
+
*/
|
|
75
|
+
export function synthesizeRules(scanRoot, opts = {}) {
|
|
76
|
+
const threshold = parseInt(opts.threshold || process.env.AGENTIC_SECURITY_RULE_SYNTHESIS_THRESHOLD || String(DEFAULT_FP_THRESHOLD), 10);
|
|
77
|
+
const triage = _readTriage(scanRoot);
|
|
78
|
+
if (!triage) return [];
|
|
79
|
+
// triage format (per v0.46): { verdicts: [{file, line, vuln, family, verdict, ...}] }
|
|
80
|
+
const verdicts = triage.verdicts || [];
|
|
81
|
+
const fps = verdicts.filter(v => v.verdict === 'fp' || v.verdict === 'false-positive');
|
|
82
|
+
if (!fps.length) return [];
|
|
83
|
+
const groups = new Map();
|
|
84
|
+
for (const e of fps) {
|
|
85
|
+
const k = _shapeKey(e);
|
|
86
|
+
if (!groups.has(k)) groups.set(k, []);
|
|
87
|
+
groups.get(k).push(e);
|
|
88
|
+
}
|
|
89
|
+
const proposals = [];
|
|
90
|
+
const dir = path.join(scanRoot || process.cwd(), PROPOSED_DIR);
|
|
91
|
+
for (const [, group] of groups) {
|
|
92
|
+
if (group.length < threshold) continue;
|
|
93
|
+
const summary = _summarizeGroup(group);
|
|
94
|
+
const yaml = _yamlProposal(summary);
|
|
95
|
+
const name = `auto-${(summary.family || summary.rule).replace(/[^a-zA-Z0-9_-]/g, '-')}-${Date.now().toString(36)}.yml`;
|
|
96
|
+
const fp = path.join(dir, name);
|
|
97
|
+
if (!opts.dryRun) {
|
|
98
|
+
try {
|
|
99
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
100
|
+
fs.writeFileSync(fp, yaml);
|
|
101
|
+
} catch { /* non-fatal */ }
|
|
102
|
+
}
|
|
103
|
+
proposals.push({ ...summary, file: fp, yaml });
|
|
104
|
+
}
|
|
105
|
+
return proposals;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const _internals = { DEFAULT_FP_THRESHOLD, TRIAGE_PATH, PROPOSED_DIR };
|