@clear-capabilities/agentic-security-scanner 0.175.0 → 0.177.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.
Files changed (110) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/bin/agentic-security.js +206 -28
  3. package/dist/1919.index.js +1 -1
  4. package/dist/2066.index.js +851 -0
  5. package/dist/2199.index.js +1 -1
  6. package/dist/2584.index.js +3350 -0
  7. package/dist/2923.index.js +13 -0
  8. package/dist/2946.index.js +81 -0
  9. package/dist/3196.index.js +27 -0
  10. package/dist/3727.index.js +292 -0
  11. package/dist/37d64df006576248d0d8.json +286 -0
  12. package/dist/4168.index.js +509 -0
  13. package/dist/4281.index.js +993 -0
  14. package/dist/5099.index.js +96 -0
  15. package/dist/5456.index.js +291 -0
  16. package/dist/5616.index.js +1471 -0
  17. package/dist/5620.index.js +2494 -0
  18. package/dist/5817.index.js +714 -0
  19. package/dist/5937.index.js +672 -0
  20. package/dist/6333.index.js +98 -0
  21. package/dist/6655.index.js +3221 -0
  22. package/dist/7838.index.js +491 -974
  23. package/dist/818.index.js +4 -3
  24. package/dist/83.index.js +159 -0
  25. package/dist/8551.index.js +82 -0
  26. package/dist/8633.index.js +93 -0
  27. package/dist/8714.index.js +76 -76
  28. package/dist/8825.index.js +84 -0
  29. package/dist/932.index.js +186 -0
  30. package/dist/9560.index.js +358 -19
  31. package/dist/9800.index.js +6 -1
  32. package/dist/agentic-security.mjs +10 -10
  33. package/dist/agentic-security.mjs.sha256 +1 -1
  34. package/dist/b4289e728659450b5e19.js +32 -0
  35. package/package.json +8 -4
  36. package/src/artifact/CLAUDE.md +4 -0
  37. package/src/artifact/artifact-evidence.js +63 -0
  38. package/src/artifact/cosign.js +61 -0
  39. package/src/artifact/engine-source.js +72 -0
  40. package/src/artifact/oci-layout.js +47 -0
  41. package/src/artifact/registry.js +213 -0
  42. package/src/capabilities/tool-registry.js +2 -0
  43. package/src/lineage/CLAUDE.md +4 -0
  44. package/src/lineage/identity-token.js +11 -0
  45. package/src/lineage/observation-adapters.js +13 -0
  46. package/src/lineage/runtime-edge-states.js +139 -0
  47. package/src/lineage/runtime-explorer-view.js +82 -0
  48. package/src/lineage/runtime-observation.js +11 -0
  49. package/src/mcp/CLAUDE.md +3 -1
  50. package/src/mcp/proof-tools.js +132 -0
  51. package/src/mcp/server.js +1 -1
  52. package/src/mcp/tools.js +3 -1
  53. package/src/posture/CLAUDE.md +23 -0
  54. package/src/posture/artifact-registry.js +4 -0
  55. package/src/posture/custom-rules.js +54 -8
  56. package/src/posture/dnr/attack-facts.js +31 -0
  57. package/src/posture/dnr/cli.js +138 -0
  58. package/src/posture/dnr/detection-ir.js +123 -0
  59. package/src/posture/dnr/exploit-correlation.js +25 -0
  60. package/src/posture/dnr/exposure-window.js +134 -0
  61. package/src/posture/dnr/hunt.js +84 -0
  62. package/src/posture/dnr/log-reader.js +189 -0
  63. package/src/posture/dnr/matchers.js +90 -0
  64. package/src/posture/dnr/outputs.js +262 -0
  65. package/src/posture/dnr/payload-classes.js +120 -0
  66. package/src/posture/dnr/redact.js +25 -0
  67. package/src/posture/dnr/store.js +57 -0
  68. package/src/posture/dnr/validate.js +64 -0
  69. package/src/posture/dnr/view.js +51 -0
  70. package/src/posture/evaluation/lab/arm-ollama.js +88 -0
  71. package/src/posture/evaluation/lab/arms.js +142 -0
  72. package/src/posture/evaluation/lab/cli.js +135 -0
  73. package/src/posture/evaluation/lab/dataset.js +139 -0
  74. package/src/posture/evaluation/lab/materialize.js +44 -0
  75. package/src/posture/evaluation/lab/neutralize.js +144 -0
  76. package/src/posture/evaluation/lab/report.js +194 -0
  77. package/src/posture/evaluation/lab/run.js +225 -0
  78. package/src/posture/evaluation/lab/score.js +135 -0
  79. package/src/posture/evaluation/lab/scoreboard.js +48 -0
  80. package/src/posture/evaluation/lab/stats.js +41 -0
  81. package/src/posture/evaluation/lab/store.js +53 -0
  82. package/src/posture/fleet/cli.js +260 -63
  83. package/src/posture/fleet/discover-edges.js +135 -0
  84. package/src/posture/fleet/enterprise-graph.js +189 -0
  85. package/src/posture/fleet/fleet-graph.js +9 -4
  86. package/src/posture/fleet/fleet-manifest.js +50 -5
  87. package/src/posture/fleet/job-runner.js +153 -0
  88. package/src/posture/fleet/job-store.js +138 -0
  89. package/src/posture/fleet/lineage-summary.js +55 -0
  90. package/src/posture/fleet/org-health.js +56 -0
  91. package/src/posture/fleet/scan-cache.js +112 -0
  92. package/src/posture/fleet/scan-child.js +32 -0
  93. package/src/posture/proof-fabric/cli.js +34 -6
  94. package/src/posture/proof-fabric/container.js +57 -0
  95. package/src/posture/proof-fabric/fix-proof.js +2 -1
  96. package/src/posture/proof-fabric/generate-python.js +129 -0
  97. package/src/posture/proof-fabric/generate.js +214 -0
  98. package/src/posture/proof-fabric/reproducer.js +3 -0
  99. package/src/posture/proof-fabric/runner.js +44 -13
  100. package/src/posture/proof-fabric/siblings.js +77 -0
  101. package/src/posture/routing/policy-router.js +103 -0
  102. package/src/posture/rule-compiler/cli.js +101 -9
  103. package/src/posture/rule-compiler/corpus.js +13 -9
  104. package/src/posture/rule-compiler/ir.js +52 -9
  105. package/src/posture/rule-compiler/mutation.js +72 -1
  106. package/src/posture/rule-compiler/promotion.js +21 -5
  107. package/src/posture/rule-compiler/proposal.js +137 -0
  108. package/src/posture/rule-compiler/regression.js +95 -0
  109. package/src/posture/rule-compiler/store.js +45 -1
  110. package/src/posture/rule-pack-signing.js +4 -2
@@ -0,0 +1,95 @@
1
+ // Regression integration for promoted rules (PRD 3, requirement 8): a promoted rule joins a corpus every later rule and every later
2
+ // run is held to, instead of being checked once at promotion and forgotten.
3
+ //
4
+ // shared false-positive corpus the SAFE code of every promoted rule (its patched, nearby-clean and protected variants) plus any file
5
+ // the operator drops in `.agentic-security/rule-compiler/fp-corpus/`. A new candidate must not fire
6
+ // on any of it, so rule N+1 cannot reintroduce the false positive rule N was built to avoid.
7
+ // regression run `rules regress` re-scores every PROMOTED rule against its own corpus, then against the shared false
8
+ // positive corpus, and reports any rule that no longer clears its gate (the matcher, the loader or
9
+ // another rule changed underneath it).
10
+ // The rule-specific unit tests are the fixtures written at evaluation (the existing `rule test` convention); this module adds the cross-rule
11
+ // part, which a per-rule check cannot see.
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import { statePath } from '../state-dir.js';
15
+ import { buildCorpus, scoreRule } from './corpus.js';
16
+ import { evaluateRule } from './promotion.js';
17
+ import { runRule, normalizeCustomRule } from '../custom-rules.js';
18
+ import { compileToCustomRule } from './ir.js';
19
+ import { loadIR, loadCorpus, loadProvenance } from './store.js';
20
+ import { stateOf } from './lifecycle.js';
21
+
22
+ const FP_DIR = 'rule-compiler/fp-corpus';
23
+ const MAX_FP_FILE = 256 * 1024;
24
+ const MAX_FP_FILES = 500;
25
+ const EXT_OF = { '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.ts': 'typescript', '.py': 'python', '.go': 'go', '.rb': 'ruby', '.java': 'java', '.rs': 'rust', '.cs': 'csharp', '.php': 'php' };
26
+
27
+ /** Operator-supplied false positives (real code a rule should never flag). Plain files; symlinks and oversized files are ignored. */
28
+ function operatorFalsePositives(root) {
29
+ const dir = statePath(root, ...FP_DIR.split('/'));
30
+ const out = [];
31
+ let names = [];
32
+ try { names = fs.readdirSync(dir).sort(); } catch { return out; }
33
+ for (const n of names.slice(0, MAX_FP_FILES)) {
34
+ const f = path.join(dir, n);
35
+ try {
36
+ const st = fs.lstatSync(f);
37
+ if (!st.isFile() || st.isSymbolicLink() || st.size > MAX_FP_FILE) continue;
38
+ out.push({ label: `operator-fp:${n}`, file: n, language: EXT_OF[path.extname(n).toLowerCase()] ?? 'any', code: fs.readFileSync(f, 'utf8') });
39
+ } catch { /* unreadable: skipped */ }
40
+ }
41
+ return out;
42
+ }
43
+
44
+ function promotedRuleIds(ledger) {
45
+ return [...new Set(ledger.entries.map((e) => e.ruleId))].filter((id) => stateOf(ledger, id) === 'PROMOTED');
46
+ }
47
+
48
+ /** The shared false-positive corpus as seen by `exceptId`: the safe code of every OTHER promoted rule, plus the operator's files. */
49
+ export function sharedFalsePositiveCorpus(root, ledger, exceptId = null) {
50
+ const items = [];
51
+ for (const id of promotedRuleIds(ledger)) {
52
+ if (id === exceptId) continue;
53
+ const ir = loadIR(root, id); const spec = loadCorpus(root, id);
54
+ if (!ir || !spec || typeof spec.originalCode !== 'string') continue;
55
+ let corpus; try { corpus = buildCorpus(ir, spec); } catch { continue; }
56
+ for (const n of corpus.negatives) items.push({ label: `rule:${id}:${n.label}`, language: ir.languages[0] ?? 'any', code: n.code });
57
+ }
58
+ return [...items, ...operatorFalsePositives(root)];
59
+ }
60
+
61
+ const FILE_FOR_LANG = { javascript: 'x.js', typescript: 'x.ts', python: 'x.py', go: 'x.go', ruby: 'x.rb', java: 'x.java', rust: 'x.rs', csharp: 'x.cs', php: 'x.php', any: 'x.txt' };
62
+
63
+ /** Which shared false-positive items does this rule fire on? A rule is only run against items written in a language it applies to. */
64
+ export function falsePositiveHits(ir, items) {
65
+ const { ok, doc } = compileToCustomRule(ir);
66
+ if (!ok) return [{ label: '(rule does not compile)' }];
67
+ const rule = normalizeCustomRule(doc);
68
+ if (!rule) return [{ label: '(rule would be rejected by the loader)' }];
69
+ const hits = [];
70
+ for (const it of items) {
71
+ const lang = it.language && it.language !== 'any' ? it.language : (ir.languages[0] ?? 'any');
72
+ if (!ir.languages.includes('any') && !ir.languages.includes(lang)) continue;
73
+ if (runRule(rule, FILE_FOR_LANG[lang] ?? 'x.txt', it.code).length > 0) hits.push({ label: it.label });
74
+ }
75
+ return hits;
76
+ }
77
+
78
+ /** Re-score every promoted rule (or one) against its own corpus and the shared false-positive corpus. */
79
+ export function regressRules(root, ledger, { only = null, thresholds } = {}) {
80
+ const results = [];
81
+ for (const id of promotedRuleIds(ledger)) {
82
+ if (only && id !== only) continue;
83
+ const ir = loadIR(root, id); const spec = loadCorpus(root, id);
84
+ if (!ir || !spec || typeof spec.originalCode !== 'string') { results.push({ id, status: 'unverifiable', reasons: [!ir ? 'the rule IR is missing' : 'no evaluated corpus was kept for this rule (run rules evaluate --corpus again)'] }); continue; }
85
+ let ev; let corpus;
86
+ try { corpus = buildCorpus(ir, spec); ev = evaluateRule(ir, corpus, thresholds); } catch (e) { results.push({ id, status: 'broken', reasons: [`cannot be scored: ${String(e.message).slice(0, 120)}`] }); continue; }
87
+ const fp = falsePositiveHits(ir, sharedFalsePositiveCorpus(root, ledger, id));
88
+ const baseline = loadProvenance(root, id)?.benchmarkResults ?? null;
89
+ const reasons = [...ev.reasons];
90
+ if (fp.length) reasons.push(`fires on ${fp.length} item(s) of the shared false-positive corpus: ${fp.slice(0, 3).map((h) => h.label).join(', ')}${fp.length > 3 ? ', ...' : ''}`);
91
+ if (baseline && ev.score.recall != null && baseline.recall != null && ev.score.recall < baseline.recall) reasons.push(`recall fell from ${(baseline.recall * 100).toFixed(1)}% to ${(ev.score.recall * 100).toFixed(1)}% since promotion`);
92
+ results.push({ id, status: reasons.length ? 'regressed' : 'ok', reasons, precision: ev.score.precision, recall: ev.score.recall, protectedFailures: ev.score.protectedFailures, fpCorpusHits: fp.length });
93
+ }
94
+ return results;
95
+ }
@@ -8,6 +8,7 @@ import path from 'node:path';
8
8
  import { statePath, ensureStateDir, safeWriteState } from '../state-dir.js';
9
9
  import { newLedger, verifyLedger } from './lifecycle.js';
10
10
  import { compileToCustomRule } from './ir.js';
11
+ import { verifyRulePack, loadTrustedKeys, signRulePack } from '../rule-pack-signing.js';
11
12
 
12
13
  const DIR = 'rule-compiler';
13
14
  const EXT = { javascript: 'js', typescript: 'ts', python: 'py', go: 'go', ruby: 'rb', java: 'java', rust: 'rs', csharp: 'cs', php: 'php', any: 'txt' };
@@ -41,6 +42,22 @@ export function loadIR(scanRoot, id) {
41
42
  try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; }
42
43
  }
43
44
 
45
+ function saveJson(scanRoot, id, name, doc) {
46
+ if (!ensureStateDir(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
47
+ const f = path.join(ruleDir(scanRoot, id), name);
48
+ return { ok: safeWriteState(f, JSON.stringify(doc, null, 2)), file: f };
49
+ }
50
+ function loadJson(scanRoot, id, name) { try { return JSON.parse(fs.readFileSync(path.join(ruleDir(scanRoot, id), name), 'utf8')); } catch { return null; } }
51
+ /** The corpus a rule was synthesized on, kept so `rules evaluate <id>` needs no flag and the evidence is the same corpus. */
52
+ export const saveCorpus = (r, id, spec) => saveJson(r, id, 'corpus.json', spec);
53
+ export const loadCorpus = (r, id) => loadJson(r, id, 'corpus.json');
54
+ /** Every synthesis attempt: strategy, match, score and why it did not pass. */
55
+ export const saveSynthesis = (r, id, syn) => saveJson(r, id, 'synthesis.json', syn);
56
+ export const loadSynthesis = (r, id) => loadJson(r, id, 'synthesis.json');
57
+ /** The rule's provenance record (PRD 3 requirement 7). */
58
+ export const saveProvenance = (r, id, doc) => saveJson(r, id, 'provenance.json', doc);
59
+ export const loadProvenance = (r, id) => loadJson(r, id, 'provenance.json');
60
+
44
61
  export function saveEvaluation(scanRoot, id, evaluation) {
45
62
  if (!ensureStateDir(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
46
63
  const f = path.join(ruleDir(scanRoot, id), 'evaluation.json');
@@ -84,13 +101,40 @@ export function publishCustomRule(scanRoot, ir) {
84
101
  ...(doc.match.pattern ? [` pattern: ${JSON.stringify(doc.match.pattern)}`] : []),
85
102
  ...(doc.match.allOf ? [` allOf:`, ...doc.match.allOf.map((p) => ` - ${JSON.stringify(p)}`), ` window: ${doc.match.window}`] : []),
86
103
  ...(doc.match.notMatch ? [` notMatch: ${JSON.stringify(doc.match.notMatch)}`] : []),
104
+ ...(doc.match.fileRequires ? [` fileRequires:`, ...doc.match.fileRequires.map((p) => ` - ${JSON.stringify(p)}`)] : []),
105
+ ...(doc.match.guardedBy ? [` guardedBy: ${JSON.stringify(doc.match.guardedBy)}`, ` guardWindow: ${doc.match.guardWindow}`] : []),
106
+ ...(doc.match.excludeLine ? [` excludeLine:`, ...doc.match.excludeLine.map((p) => ` - ${JSON.stringify(p)}`)] : []),
87
107
  ` message: ${JSON.stringify(doc.message)}`,
88
108
  '',
89
109
  ].join('\n');
90
110
  return { ok: safeWriteState(f, yamlText), file: f };
91
111
  }
92
112
 
113
+ /** Sign a published rule with the operator's key (the same Ed25519 scheme `agentic-security-rule sign` uses). The key is never stored here. */
114
+ export function signPublishedRule(file, privateKeyB64) {
115
+ try { signRulePack(file, privateKeyB64); return { ok: true }; } catch (e) { return { ok: false, reason: String(e.message).slice(0, 120) }; }
116
+ }
117
+
118
+ /**
119
+ * Will the scanner actually LOAD this promoted rule? Publishing a file is not the same as the scanner trusting it: rule packs are
120
+ * signature-checked (posture/rule-pack-signing.js) so that a rule dropped into a repository cannot run in someone's CI. A promoted rule
121
+ * the scanner would refuse is not a capability yet, so this is reported next to the lifecycle state instead of being assumed.
122
+ */
123
+ export function packTrust(scanRoot, id) {
124
+ const file = statePath(scanRoot, 'rules', `${id}.yml`);
125
+ if (!fs.existsSync(file)) return { live: false, reason: 'not published', remediation: null };
126
+ let r;
127
+ const origErr = console.error; console.error = () => {};
128
+ try { r = verifyRulePack(file, loadTrustedKeys(scanRoot)); } finally { console.error = origErr; }
129
+ if (r.ok) return { live: true, via: r.passThrough ? 'signed, accepted in pass-through mode (no trusted keys are configured)' : `signed by ${r.keyId}`, reason: null, remediation: null };
130
+ if (r.reason === 'unsigned' && r.allowUnsigned) return { live: true, via: 'unsigned, accepted because AGENTIC_SECURITY_ALLOW_UNSIGNED_PACKS=1', reason: null, remediation: null };
131
+ const remediation = r.reason === 'unsigned'
132
+ ? `sign it (agentic-security-rule sign ${path.relative(process.cwd(), file) || file} with your key, or re-run promote with --sign-key <base64>), or opt in for this environment with AGENTIC_SECURITY_ALLOW_UNSIGNED_PACKS=1`
133
+ : (r.remediation ?? 'see the signing setup in bin/agentic-security-rule.js');
134
+ return { live: false, reason: r.reason, remediation };
135
+ }
136
+
93
137
  export function unpublishCustomRule(scanRoot, id) {
94
138
  const f = statePath(scanRoot, 'rules', `${id}.yml`);
95
- try { if (fs.existsSync(f)) fs.unlinkSync(f); return { ok: true }; } catch (e) { return { ok: false, reason: e.message }; }
139
+ try { for (const x of [f, `${f}.sig`]) if (fs.existsSync(x)) fs.unlinkSync(x); return { ok: true }; } catch (e) { return { ok: false, reason: e.message }; }
96
140
  }
@@ -198,9 +198,11 @@ export function signRulePack(rulePackPath, privateKeyB64) {
198
198
  const body = fs.readFileSync(rulePackPath);
199
199
  const privBytes = Buffer.from(privateKeyB64, 'base64');
200
200
  if (privBytes.length !== 32) throw new Error('private key must be 32 bytes (raw ed25519)');
201
+ // A raw 32-byte Ed25519 seed as a PKCS#8 key. (A JWK with only `d`, which this used to build, is rejected by current Node: it also
202
+ // requires the public `x`, so signing failed with "Invalid JWK OKP key" and no rule pack could be signed through this workflow.)
201
203
  const keyObj = crypto.createPrivateKey({
202
- key: { kty: 'OKP', crv: 'Ed25519', d: privBytes.toString('base64url') },
203
- format: 'jwk',
204
+ key: Buffer.concat([Buffer.from('302e020100300506032b657004220420', 'hex'), privBytes]),
205
+ format: 'der', type: 'pkcs8',
204
206
  });
205
207
  const sig = crypto.sign(null, body, keyObj);
206
208
  fs.writeFileSync(rulePackPath + '.sig', sig);