@clear-capabilities/agentic-security-scanner 0.166.0 → 0.168.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 +91 -0
- package/bin/agentic-security.js +40 -4
- package/dist/2199.index.js +348 -0
- package/dist/3736.index.js +2 -1
- package/dist/3740.index.js +722 -0
- package/dist/4113.index.js +1 -1
- package/dist/4142.index.js +929 -0
- package/dist/5738.index.js +240 -0
- package/dist/6932.index.js +1190 -0
- package/dist/7838.index.js +1 -1
- package/dist/808.index.js +1330 -0
- package/dist/818.index.js +93 -0
- package/dist/9800.index.js +245 -0
- package/dist/9999.index.js +2 -2
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +2 -2
- package/src/posture/artifact-registry.js +6 -0
- package/src/posture/fleet/cli.js +103 -0
- package/src/posture/fleet/fleet-graph.js +142 -0
- package/src/posture/fleet/fleet-manifest.js +77 -0
- package/src/posture/proof-fabric/cli.js +115 -0
- package/src/posture/proof-fabric/fix-proof.js +73 -0
- package/src/posture/proof-fabric/oracles.js +101 -0
- package/src/posture/proof-fabric/orchestrator.js +129 -0
- package/src/posture/proof-fabric/reproducer.js +108 -0
- package/src/posture/proof-fabric/runner.js +243 -0
- package/src/posture/proof-fabric/states.js +53 -0
- package/src/posture/proof-fabric/store.js +83 -0
- package/src/posture/rule-compiler/cli.js +141 -0
- package/src/posture/rule-compiler/corpus.js +61 -0
- package/src/posture/rule-compiler/eligibility.js +25 -0
- package/src/posture/rule-compiler/ir.js +87 -0
- package/src/posture/rule-compiler/lifecycle.js +57 -0
- package/src/posture/rule-compiler/mutation.js +60 -0
- package/src/posture/rule-compiler/promotion.js +47 -0
- package/src/posture/rule-compiler/store.js +96 -0
- package/src/sandbox/backend-userspace.js +7 -1
- package/src/sandbox/trust-boundary.js +2 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Persistence for the rule compiler: one directory per rule under `.agentic-security/rule-compiler/<id>/`
|
|
2
|
+
// (`ir.json`, `ledger.json`, `evaluation.json`), the promoted custom-rule document at
|
|
3
|
+
// `.agentic-security/rules/<id>.yml` (so the EXISTING `loadCustomRules`/`rule test` machinery picks it up with no
|
|
4
|
+
// change), and a regression fixture pair at `.agentic-security/rule-compiler/fixtures/<id>/{vulnerable,clean}/`
|
|
5
|
+
// using the SAME filename convention `runRuleTests` already reads (`vulnerable`/`clean` in the path).
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { statePath, ensureStateDir, safeWriteState } from '../state-dir.js';
|
|
9
|
+
import { newLedger, verifyLedger } from './lifecycle.js';
|
|
10
|
+
import { compileToCustomRule } from './ir.js';
|
|
11
|
+
|
|
12
|
+
const DIR = 'rule-compiler';
|
|
13
|
+
const EXT = { javascript: 'js', typescript: 'ts', python: 'py', go: 'go', ruby: 'rb', java: 'java', rust: 'rs', csharp: 'cs', php: 'php', any: 'txt' };
|
|
14
|
+
|
|
15
|
+
function ruleDir(scanRoot, id) { return statePath(scanRoot, DIR, id); }
|
|
16
|
+
|
|
17
|
+
export function loadLedger(scanRoot) {
|
|
18
|
+
const f = statePath(scanRoot, DIR, 'ledger.json');
|
|
19
|
+
if (!fs.existsSync(f)) return newLedger();
|
|
20
|
+
try {
|
|
21
|
+
const ledger = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
22
|
+
const v = verifyLedger(ledger);
|
|
23
|
+
return v.ok ? ledger : { ...newLedger(), tampered: true, tamperReason: v.reason };
|
|
24
|
+
} catch { return newLedger(); }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function saveLedger(scanRoot, ledger) {
|
|
28
|
+
if (!ensureStateDir(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
|
|
29
|
+
const f = statePath(scanRoot, DIR, 'ledger.json');
|
|
30
|
+
return { ok: safeWriteState(f, JSON.stringify(ledger, null, 2)) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function saveIR(scanRoot, ir) {
|
|
34
|
+
if (!ensureStateDir(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
|
|
35
|
+
const f = path.join(ruleDir(scanRoot, ir.id), 'ir.json');
|
|
36
|
+
return { ok: safeWriteState(f, JSON.stringify(ir, null, 2)), file: f };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function loadIR(scanRoot, id) {
|
|
40
|
+
const f = path.join(ruleDir(scanRoot, id), 'ir.json');
|
|
41
|
+
try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return null; }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function saveEvaluation(scanRoot, id, evaluation) {
|
|
45
|
+
if (!ensureStateDir(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
|
|
46
|
+
const f = path.join(ruleDir(scanRoot, id), 'evaluation.json');
|
|
47
|
+
return { ok: safeWriteState(f, JSON.stringify(evaluation, null, 2)) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Write the regression fixtures a promoted rule needs: the SAME vulnerable/clean convention `runRuleTests`
|
|
51
|
+
* already reads, scored against the exact corpus the rule was promoted on. */
|
|
52
|
+
export function writeFixtures(scanRoot, ir, corpus) {
|
|
53
|
+
const ext = EXT[ir.languages[0]] || 'txt';
|
|
54
|
+
const base = path.join(ruleDir(scanRoot, ir.id), 'fixtures');
|
|
55
|
+
if (!ensureStateDir(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
|
|
56
|
+
const written = [];
|
|
57
|
+
for (const p of corpus.positives) {
|
|
58
|
+
const f = path.join(base, 'vulnerable', `${p.label.replace(/[^a-z0-9_-]/gi, '_')}.${ext}`);
|
|
59
|
+
if (safeWriteState(f, p.code)) written.push(f);
|
|
60
|
+
}
|
|
61
|
+
for (const n of corpus.negatives) {
|
|
62
|
+
const f = path.join(base, 'clean', `${n.label.replace(/[^a-z0-9_-]/gi, '_')}.${ext}`);
|
|
63
|
+
if (safeWriteState(f, n.code)) written.push(f);
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, written, glob: path.join(base, '**', '*') };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Promote: write the compiled custom-rule document to the live rules directory. Does not touch the ledger —
|
|
69
|
+
* the caller records the PROMOTED transition itself, so a failed write here can never produce an inconsistent
|
|
70
|
+
* "promoted but not live" or "live but not recorded" state silently. */
|
|
71
|
+
export function publishCustomRule(scanRoot, ir) {
|
|
72
|
+
const { ok, doc, errs } = compileToCustomRule(ir);
|
|
73
|
+
if (!ok) return { ok: false, reason: `invalid rule IR: ${errs.join('; ')}` };
|
|
74
|
+
if (!ensureStateDir(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
|
|
75
|
+
const f = statePath(scanRoot, 'rules', `${ir.id}.yml`);
|
|
76
|
+
const yamlText = [
|
|
77
|
+
`# Compiled by the rule compiler from finding ${ir.provenance.originatingFindingId} (${ir.provenance.evidenceState}).`,
|
|
78
|
+
`- id: ${doc.id}`,
|
|
79
|
+
` title: ${JSON.stringify(doc.title)}`,
|
|
80
|
+
` severity: ${doc.severity}`,
|
|
81
|
+
` cwe: ${doc.cwe}`,
|
|
82
|
+
` languages: [${doc.languages.join(', ')}]`,
|
|
83
|
+
` match:`,
|
|
84
|
+
...(doc.match.pattern ? [` pattern: ${JSON.stringify(doc.match.pattern)}`] : []),
|
|
85
|
+
...(doc.match.allOf ? [` allOf:`, ...doc.match.allOf.map((p) => ` - ${JSON.stringify(p)}`), ` window: ${doc.match.window}`] : []),
|
|
86
|
+
...(doc.match.notMatch ? [` notMatch: ${JSON.stringify(doc.match.notMatch)}`] : []),
|
|
87
|
+
` message: ${JSON.stringify(doc.message)}`,
|
|
88
|
+
'',
|
|
89
|
+
].join('\n');
|
|
90
|
+
return { ok: safeWriteState(f, yamlText), file: f };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function unpublishCustomRule(scanRoot, id) {
|
|
94
|
+
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 }; }
|
|
96
|
+
}
|
|
@@ -50,11 +50,17 @@ function _profile({ allowNetwork, proxyPort = null, denyRead = [], confineReads
|
|
|
50
50
|
readRoots.forEach((_, i) => lines.push(`(allow file-read* (subpath (param "RD${i}")))`));
|
|
51
51
|
writeRoots.forEach((_, i) => lines.push(`(allow file-read* (subpath (param "WR${i}")))`));
|
|
52
52
|
metaLiterals.forEach((_, i) => lines.push(`(allow file-read-metadata (literal (param "META${i}")))`));
|
|
53
|
-
lines.push('(allow file-write* (literal "/dev/null"))');
|
|
54
53
|
}
|
|
55
54
|
// Read denial (CORE-003). SBPL is last-match-wins, so these deny rules MUST
|
|
56
55
|
// come after every read allow.
|
|
57
56
|
denyRead.forEach((_, i) => lines.push(`(deny file-read* (subpath (param "DENY${i}")))`));
|
|
57
|
+
// /dev/null is a kernel sink: writing to it (`stdio: 'ignore'`, `> /dev/null`, a piped-to-null redirect — all
|
|
58
|
+
// common and harmless) cannot leak data or escape confinement. Allowed unconditionally, in BOTH the default and
|
|
59
|
+
// capability-mode profiles (it used to be capability-mode only, which left the far more common default profile
|
|
60
|
+
// denying it: `posix_spawn` opens /dev/null to set up an ignored fd BEFORE exec, so the spawn itself failed with
|
|
61
|
+
// EPERM — any target that redirects a child's output to /dev/null never ran at all, silently, under the default
|
|
62
|
+
// profile only).
|
|
63
|
+
lines.push('(allow file-write* (literal "/dev/null"))');
|
|
58
64
|
lines.push('(allow file-write* (subpath (param "ROOT")))');
|
|
59
65
|
writeRoots.forEach((_, i) => lines.push(`(allow file-write* (subpath (param "WR${i}")))`));
|
|
60
66
|
if (allowNetwork) lines.push('(allow network*)');
|
|
@@ -58,7 +58,7 @@ export async function runInBoundary(argv, o = {}) {
|
|
|
58
58
|
const {
|
|
59
59
|
root, labelDirs = [], evidenceDirs = [], denyReadPaths = [], env = {},
|
|
60
60
|
allowNetwork = false, timeoutMs = 10000, graceMs = 1000, signal, force, limits,
|
|
61
|
-
maxOutputBytes, controlProbes,
|
|
61
|
+
maxOutputBytes, controlProbes, networkProxyPort = null,
|
|
62
62
|
} = o;
|
|
63
63
|
const required = [...(o.require || DEFAULT_REQUIRED_CONTROLS)];
|
|
64
64
|
if (!allowNetwork && !required.includes('network')) required.push('network');
|
|
@@ -94,6 +94,7 @@ export async function runInBoundary(argv, o = {}) {
|
|
|
94
94
|
const r = await runConfinedSupervised(argv, {
|
|
95
95
|
root, timeoutMs, graceMs, signal, force, allowNetwork, limits, maxOutputBytes,
|
|
96
96
|
env, denyReadPaths: protectedPaths,
|
|
97
|
+
...(networkProxyPort != null ? { networkProxyPort } : {}),
|
|
97
98
|
});
|
|
98
99
|
const executed = r.status !== 'disabled' && !(r.status === 'error' && !r.supervised);
|
|
99
100
|
const run = { ...r, executed };
|