@clear-capabilities/agentic-security-scanner 0.142.0 → 0.144.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 +399 -0
- package/bin/agentic-security.js +530 -54
- package/dist/1.index.js +223 -0
- package/dist/113.index.js +108 -17
- package/dist/144.index.js +163 -0
- package/dist/178.index.js +1 -1
- package/dist/238.index.js +3 -2
- package/dist/265.index.js +191 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +165 -52
- package/dist/526.index.js +108 -17
- package/dist/552.index.js +97 -0
- package/dist/637.index.js +1 -1
- package/dist/730.index.js +311 -0
- package/dist/736.index.js +301 -0
- package/dist/824.index.js +7 -0
- package/dist/905.index.js +88 -22
- package/dist/920.index.js +491 -0
- package/dist/970.index.js +109 -0
- package/dist/agentic-security.mjs +13 -13
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/calibration-seed.json +2 -0
- package/package.json +19 -11
- package/src/dataflow/index.js +18 -0
- package/src/dataflow/privacy-catalog.js +290 -0
- package/src/dataflow/privacy-deep-walker.js +515 -0
- package/src/dataflow/privacy-governance.js +126 -0
- package/src/dataflow/privacy-inventory.js +154 -0
- package/src/dataflow/privacy-sink-policy.js +125 -0
- package/src/dataflow/privacy-taint.js +115 -54
- package/src/dataflow/privacy-taxonomy.js +233 -0
- package/src/discovery/disprove.js +7 -3
- package/src/discovery/hunter.js +9 -5
- package/src/discovery/index.js +2 -2
- package/src/discovery/llm-invoke.js +69 -13
- package/src/egress/audit.js +147 -0
- package/src/egress/policy.js +313 -0
- package/src/egress/redact.js +180 -0
- package/src/engine.js +575 -288
- package/src/fix/apply-fix-service.js +403 -0
- package/src/fix/approver-registry.js +157 -0
- package/src/llm-validator/index.js +86 -9
- package/src/llm-validator/model-status.js +66 -0
- package/src/mcp/tools.js +157 -50
- package/src/pipeline/analyzer-supervisor.js +93 -0
- package/src/pipeline/analyzer-worker.js +26 -0
- package/src/pipeline/annotator-runner.js +33 -0
- package/src/pipeline/assurance-mode.js +91 -0
- package/src/pipeline/cascade-worker-pool.js +172 -0
- package/src/pipeline/cascade-worker.js +43 -0
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/detector-runner.js +51 -0
- package/src/pipeline/enrichment-completion.js +58 -0
- package/src/pipeline/evidence-provenance.js +91 -0
- package/src/pipeline/finding-schema.js +101 -0
- package/src/pipeline/legacy-compat.js +101 -0
- package/src/pipeline/producer-collector.js +48 -0
- package/src/pipeline/producer-registry.js +112 -0
- package/src/pipeline/scan-health.js +144 -0
- package/src/posture/CLAUDE.md +2 -0
- package/src/posture/accuracy-scorecard.js +96 -1
- package/src/posture/adversary-agent.js +15 -3
- package/src/posture/artifact-registry.js +217 -0
- package/src/posture/auditor-walkthrough.js +70 -8
- package/src/posture/calibration-feedback.js +201 -0
- package/src/posture/calibration-seed.json +2 -0
- package/src/posture/calibration.js +25 -0
- package/src/posture/compliance-evidence-signing.js +131 -0
- package/src/posture/compliance-policy.js +314 -17
- package/src/posture/custom-rules.js +36 -0
- package/src/posture/deterministic.js +8 -1
- package/src/posture/encryption-provider.js +205 -0
- package/src/posture/evidence-grade-wording.js +71 -0
- package/src/posture/fix-history.js +113 -19
- package/src/posture/fix-honesty-gate.js +47 -6
- package/src/posture/fix-verify.js +56 -7
- package/src/posture/fleet.js +0 -0
- package/src/posture/flow-narration.js +7 -2
- package/src/posture/legal-hold.js +140 -0
- package/src/posture/llm-redteam.js +10 -1
- package/src/posture/material-change.js +90 -0
- package/src/posture/policy-bundle.js +274 -0
- package/src/posture/privacy-framework.js +33 -6
- package/src/posture/production-feedback.js +179 -0
- package/src/posture/retention-policy.js +132 -0
- package/src/posture/risk-dollars.js +216 -26
- package/src/posture/scan-checkpoint.js +176 -31
- package/src/posture/state-dir.js +36 -1
- package/src/posture/state-lifecycle-report.js +77 -0
- package/src/posture/suppressions.js +59 -3
- package/src/privacy/ir-adapter.js +380 -0
- package/src/report/index.js +83 -18
- package/src/report/oscal.js +635 -0
- package/src/sast/cpp.js +3 -14
- package/src/sca/llm-function-extract.js +6 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
export const id = 736;
|
|
2
|
+
export const ids = [736];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 3736:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ buildPolicyBundle: () => (/* binding */ buildPolicyBundle),
|
|
10
|
+
/* harmony export */ ensurePolicyKeyPair: () => (/* binding */ ensurePolicyKeyPair),
|
|
11
|
+
/* harmony export */ loadPolicyBundles: () => (/* binding */ loadPolicyBundles),
|
|
12
|
+
/* harmony export */ loadPolicyPublicKey: () => (/* binding */ loadPolicyPublicKey),
|
|
13
|
+
/* harmony export */ resolveEffectivePolicy: () => (/* binding */ resolveEffectivePolicy),
|
|
14
|
+
/* harmony export */ signPolicyBundle: () => (/* binding */ signPolicyBundle)
|
|
15
|
+
/* harmony export */ });
|
|
16
|
+
/* unused harmony exports POLICY_BUNDLE_SCHEMA, SCOPES, canonicalPolicyBytes, verifyPolicyBundle, computePolicyDrift */
|
|
17
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
|
|
18
|
+
/* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8161);
|
|
19
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
20
|
+
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(7598);
|
|
21
|
+
/* harmony import */ var _evidence_bundle_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(8317);
|
|
22
|
+
/* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(1174);
|
|
23
|
+
// Signed, portable policy bundles (assurance-hardening PRD FR-1001).
|
|
24
|
+
//
|
|
25
|
+
// "Support signed portable policy bundles with organization, repository, and
|
|
26
|
+
// environment inheritance" | "Tampered or expired policy is rejected;
|
|
27
|
+
// effective policy is explainable."
|
|
28
|
+
//
|
|
29
|
+
// WHY REUSE evidence-bundle.js's Ed25519 SCHEME, NOT integrity.js's HMAC
|
|
30
|
+
// -------------------------------------------------------------------------
|
|
31
|
+
// A policy bundle is authored by one party (typically a central security
|
|
32
|
+
// team, "the organization") and distributed to many repositories that never
|
|
33
|
+
// had the signing key. integrity.js's per-install symmetric HMAC is
|
|
34
|
+
// tamper-evidence for the SAME install that wrote it — a different install
|
|
35
|
+
// verifying a bundle it didn't sign is exactly the case that scheme cannot
|
|
36
|
+
// serve. evidence-bundle.js already solved this for findings: Ed25519,
|
|
37
|
+
// verify with only the public key. This module reuses that primitive
|
|
38
|
+
// directly (canonicalisation shape, allowlist-signed-fields discipline,
|
|
39
|
+
// exclusive-create key generation) rather than inventing a third signing
|
|
40
|
+
// mechanism, per this session's own established "survey before building a
|
|
41
|
+
// parallel primitive" practice (see D-0023/D-0025 for the same discipline
|
|
42
|
+
// applied to identity and exception mechanisms respectively).
|
|
43
|
+
//
|
|
44
|
+
// EA-03 LESSON, APPLIED HERE FROM THE START
|
|
45
|
+
// -------------------------------------------------------------------------
|
|
46
|
+
// evidence-bundle.js's own history (see its EA-03 comment) is the reason
|
|
47
|
+
// `canonicalPolicyBytes` signs an explicit ALLOWLIST and `verifyPolicyBundle`
|
|
48
|
+
// REJECTS any top-level key outside it: a bundle with a field stapled on
|
|
49
|
+
// after signing must fail verification, not silently pass with the addition
|
|
50
|
+
// unverified. Getting this right on the first pass — rather than discovering
|
|
51
|
+
// the gap the way evidence-bundle.js did — is the entire point of writing it
|
|
52
|
+
// down here.
|
|
53
|
+
//
|
|
54
|
+
// SCOPE — WHAT THIS MODULE DOES NOT DO
|
|
55
|
+
// -------------------------------------------------------------------------
|
|
56
|
+
// It does not invent a policy SCHEMA — `policy` is an opaque object; whatever
|
|
57
|
+
// keys a bundle carries (egress rules, severity floors, approved providers,
|
|
58
|
+
// anything else this repo already treats as policy) are merged the same way.
|
|
59
|
+
// It does not implement deep/recursive merging — inheritance is shallow,
|
|
60
|
+
// top-level-key override (organization -> repository -> environment, most
|
|
61
|
+
// specific wins per key), matching how every other flat policy config file
|
|
62
|
+
// in this repo (egress-policy.yml, compliance-severity-policy.json, …) is
|
|
63
|
+
// already shaped. A future consumer needing nested-key inheritance is new,
|
|
64
|
+
// separate scope, not assumed here.
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
const POLICY_BUNDLE_SCHEMA = 'agentic-security/policy-bundle@1';
|
|
74
|
+
|
|
75
|
+
const SCOPES = ['organization', 'repository', 'environment'];
|
|
76
|
+
// Inheritance order — later entries override earlier ones, per top-level key.
|
|
77
|
+
const INHERITANCE_ORDER = ['organization', 'repository', 'environment'];
|
|
78
|
+
|
|
79
|
+
function keyDir() {
|
|
80
|
+
const xdg = process.env.XDG_CONFIG_HOME || node_path__WEBPACK_IMPORTED_MODULE_2__.join(node_os__WEBPACK_IMPORTED_MODULE_1__.homedir(), '.config');
|
|
81
|
+
return node_path__WEBPACK_IMPORTED_MODULE_2__.join(xdg, 'agentic-security', 'policy-bundles');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** A SEPARATE Ed25519 keypair from evidence-bundle.js's finding-attestation
|
|
85
|
+
* key — signing "this policy is authentic" and "this finding really came
|
|
86
|
+
* from a scan run by me" are different trust domains and must not share a
|
|
87
|
+
* key. Reuses evidence-bundle.js's hardened, race-safe generation logic
|
|
88
|
+
* directly (its exclusive-create collision handling is the kind of code
|
|
89
|
+
* worth NOT re-implementing) rather than duplicating it — the separation
|
|
90
|
+
* that matters is the DIRECTORY (`policy-bundles/`, isolated from
|
|
91
|
+
* evidence-bundle.js's own key dir), not the leaf filenames, which are
|
|
92
|
+
* `attest-key.pem`/`attest-key.pub.pem` regardless of caller (a
|
|
93
|
+
* evidence-bundle.js internal, not overridable here) — a readability wart,
|
|
94
|
+
* not a security one, since the directory is what prevents key reuse. */
|
|
95
|
+
function ensurePolicyKeyPair(dir = keyDir()) {
|
|
96
|
+
return (0,_evidence_bundle_js__WEBPACK_IMPORTED_MODULE_4__.ensureKeyPair)(dir);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Deterministic JSON, keys sorted at every level — identical algorithm to
|
|
101
|
+
* evidence-bundle.js's canonicalJson (duplicated rather than imported: it is
|
|
102
|
+
* a pure, three-line function, and importing it would couple this module's
|
|
103
|
+
* signature format to evidence-bundle.js's internals for no real benefit).
|
|
104
|
+
*/
|
|
105
|
+
function canonicalJson(value) {
|
|
106
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value ?? null);
|
|
107
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
108
|
+
const keys = Object.keys(value).sort();
|
|
109
|
+
return `{${keys.map(k => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// The complete set of fields a legitimately-built bundle carries, BEFORE
|
|
113
|
+
// signing. This is the signed allowlist AND (in verifyPolicyBundle) the
|
|
114
|
+
// complete set of keys a signed bundle is permitted to have alongside
|
|
115
|
+
// `signature` — see the EA-03 note above for why both matter.
|
|
116
|
+
const BUNDLE_FIELDS = ['schema', 'scope', 'policy', 'issuedAt', 'expiresAt'];
|
|
117
|
+
const BUNDLE_TOP_LEVEL_KEYS = new Set([...BUNDLE_FIELDS, 'signature']);
|
|
118
|
+
|
|
119
|
+
function canonicalPolicyBytes(bundle) {
|
|
120
|
+
const signed = {};
|
|
121
|
+
for (const k of BUNDLE_FIELDS) signed[k] = bundle[k] ?? null;
|
|
122
|
+
return Buffer.from(canonicalJson(signed), 'utf8');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build an unsigned bundle. `policy` is caller-supplied and opaque — this
|
|
127
|
+
* module does not validate its shape, only its provenance and freshness.
|
|
128
|
+
*/
|
|
129
|
+
function buildPolicyBundle(scope, policy, { issuedAt, expiresAt } = {}) {
|
|
130
|
+
if (!SCOPES.includes(scope)) return null;
|
|
131
|
+
if (!policy || typeof policy !== 'object' || Array.isArray(policy)) return null;
|
|
132
|
+
return {
|
|
133
|
+
schema: POLICY_BUNDLE_SCHEMA,
|
|
134
|
+
scope,
|
|
135
|
+
policy,
|
|
136
|
+
issuedAt: issuedAt ?? new Date().toISOString(),
|
|
137
|
+
expiresAt: expiresAt ?? null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Sign a bundle. Returns a new object; the input is not mutated. */
|
|
142
|
+
function signPolicyBundle(bundle, privateKeyPem) {
|
|
143
|
+
const sig = node_crypto__WEBPACK_IMPORTED_MODULE_3__.sign(null, canonicalPolicyBytes(bundle), privateKeyPem);
|
|
144
|
+
return {
|
|
145
|
+
...bundle,
|
|
146
|
+
signature: { algorithm: 'ed25519', canonicalisation: POLICY_BUNDLE_SCHEMA, value: sig.toString('base64') },
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Verify a bundle: signature authenticity AND freshness. Never throws — a
|
|
152
|
+
* malformed or tampered bundle from an untrusted source (a repo checking out
|
|
153
|
+
* whatever an org distributed) is an expected input, not exceptional.
|
|
154
|
+
*
|
|
155
|
+
* @returns {{ok: boolean, reason: string|null}}
|
|
156
|
+
*/
|
|
157
|
+
function verifyPolicyBundle(bundle, publicKeyPem, { now = Date.now() } = {}) {
|
|
158
|
+
if (!bundle || typeof bundle !== 'object') return { ok: false, reason: 'bundle is not an object' };
|
|
159
|
+
if (bundle.schema !== POLICY_BUNDLE_SCHEMA) return { ok: false, reason: `unrecognised schema: ${bundle.schema}` };
|
|
160
|
+
if (!SCOPES.includes(bundle.scope)) return { ok: false, reason: `unrecognised scope: ${bundle.scope}` };
|
|
161
|
+
const unknownKeys = Object.keys(bundle).filter(k => !BUNDLE_TOP_LEVEL_KEYS.has(k));
|
|
162
|
+
if (unknownKeys.length) {
|
|
163
|
+
return { ok: false, reason: `unrecognised top-level key(s) not covered by the signature: ${unknownKeys.join(', ')}` };
|
|
164
|
+
}
|
|
165
|
+
const sig = bundle.signature;
|
|
166
|
+
if (!sig?.value) return { ok: false, reason: 'bundle is unsigned' };
|
|
167
|
+
if (sig.algorithm !== 'ed25519') return { ok: false, reason: `unsupported algorithm: ${sig.algorithm}` };
|
|
168
|
+
if (!publicKeyPem) return { ok: false, reason: 'no public key supplied' };
|
|
169
|
+
let sigOk = false;
|
|
170
|
+
try {
|
|
171
|
+
sigOk = node_crypto__WEBPACK_IMPORTED_MODULE_3__.verify(null, canonicalPolicyBytes(bundle), publicKeyPem, Buffer.from(sig.value, 'base64'));
|
|
172
|
+
} catch (e) {
|
|
173
|
+
return { ok: false, reason: `verification error: ${e.message}` };
|
|
174
|
+
}
|
|
175
|
+
if (!sigOk) return { ok: false, reason: 'signature does not match the bundle contents — it was modified after signing' };
|
|
176
|
+
if (bundle.expiresAt) {
|
|
177
|
+
const exp = Date.parse(bundle.expiresAt);
|
|
178
|
+
if (!Number.isFinite(exp)) return { ok: false, reason: `expiresAt is not a valid date: ${bundle.expiresAt}` };
|
|
179
|
+
if (exp < now) return { ok: false, reason: `policy expired on ${bundle.expiresAt}` };
|
|
180
|
+
}
|
|
181
|
+
return { ok: true, reason: null };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Merge verified bundles into one effective policy, in inheritance order
|
|
186
|
+
* (organization -> repository -> environment; most specific wins per key).
|
|
187
|
+
* A tampered or expired bundle is EXCLUDED from the merge (rejected, not
|
|
188
|
+
* silently dropped — its scope and reason are reported so a rejection is
|
|
189
|
+
* visible, not just absent). `provenance[key]` names which scope's bundle
|
|
190
|
+
* last set that key — the "effective policy is explainable" half of the
|
|
191
|
+
* acceptance criterion.
|
|
192
|
+
*
|
|
193
|
+
* @param {Array<{scope: string, bundle: object}>} entries
|
|
194
|
+
* @param {string} publicKeyPem
|
|
195
|
+
* @returns {{effective: object, provenance: Record<string,string>,
|
|
196
|
+
* accepted: string[], rejected: Array<{scope:string, reason:string}>}}
|
|
197
|
+
*/
|
|
198
|
+
function resolveEffectivePolicy(entries, publicKeyPem, { now = Date.now() } = {}) {
|
|
199
|
+
const byScope = new Map();
|
|
200
|
+
for (const e of entries || []) {
|
|
201
|
+
if (e && SCOPES.includes(e.scope)) byScope.set(e.scope, e.bundle);
|
|
202
|
+
}
|
|
203
|
+
const effective = {};
|
|
204
|
+
const provenance = {};
|
|
205
|
+
const accepted = [];
|
|
206
|
+
const rejected = [];
|
|
207
|
+
for (const scope of INHERITANCE_ORDER) {
|
|
208
|
+
const bundle = byScope.get(scope);
|
|
209
|
+
if (!bundle) continue;
|
|
210
|
+
const v = verifyPolicyBundle(bundle, publicKeyPem, { now });
|
|
211
|
+
if (!v.ok) { rejected.push({ scope, reason: v.reason }); continue; }
|
|
212
|
+
accepted.push(scope);
|
|
213
|
+
for (const [k, val] of Object.entries(bundle.policy)) {
|
|
214
|
+
effective[k] = val;
|
|
215
|
+
provenance[k] = scope;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { effective, provenance, accepted, rejected };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Load whichever policy bundle files exist under
|
|
223
|
+
* `.agentic-security/policy-bundles/{organization,repository,environment}.json`.
|
|
224
|
+
* Degrades gracefully — a missing directory or missing/malformed individual
|
|
225
|
+
* file is simply absent from the result, never thrown. Read-first-in-try/
|
|
226
|
+
* catch throughout (no existsSync-then-readFileSync — D-0012/D-0022).
|
|
227
|
+
*/
|
|
228
|
+
function loadPolicyBundles(scanRoot) {
|
|
229
|
+
if (!scanRoot) return [];
|
|
230
|
+
const entries = [];
|
|
231
|
+
for (const scope of SCOPES) {
|
|
232
|
+
let fp;
|
|
233
|
+
try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_5__/* .statePath */ .BQ)(scanRoot, 'policy-bundles', `${scope}.json`); } catch { continue; }
|
|
234
|
+
let raw;
|
|
235
|
+
try { raw = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { continue; }
|
|
236
|
+
try {
|
|
237
|
+
const bundle = JSON.parse(raw);
|
|
238
|
+
if (bundle && typeof bundle === 'object') entries.push({ scope, bundle });
|
|
239
|
+
} catch { /* malformed — skip, do not throw */ }
|
|
240
|
+
}
|
|
241
|
+
return entries;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Read the operator-supplied public key an org distributes alongside its
|
|
246
|
+
* bundles, from `.agentic-security/policy-bundle-public-key.pem`. Returns
|
|
247
|
+
* null (not a throw) if absent or unreadable — resolution then rejects
|
|
248
|
+
* every bundle for lack of a key, same as any other "no public key
|
|
249
|
+
* supplied" case in verifyPolicyBundle.
|
|
250
|
+
*/
|
|
251
|
+
function loadPolicyPublicKey(scanRoot) {
|
|
252
|
+
if (!scanRoot) return null;
|
|
253
|
+
let fp;
|
|
254
|
+
try { fp = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_5__/* .statePath */ .BQ)(scanRoot, 'policy-bundle-public-key.pem'); } catch { return null; }
|
|
255
|
+
try { return node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(fp, 'utf8'); } catch { return null; }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* FR-1006 ("policy drift"): does this repository's LOCALLY resolved effective
|
|
260
|
+
* policy diverge from what the organization-scope bundle alone specifies —
|
|
261
|
+
* and is anything the repo is relying on actually being rejected (tampered
|
|
262
|
+
* or expired) rather than enforced? Both are GOVERNANCE gaps, distinct from
|
|
263
|
+
* risk findings, which is the exact distinction FR-1006's acceptance
|
|
264
|
+
* criterion asks fleet output to make.
|
|
265
|
+
*
|
|
266
|
+
* No bundles configured at all is a no-op (`null`), matching this session's
|
|
267
|
+
* established convention: drift can only be reported relative to a
|
|
268
|
+
* baseline, and a repo with no organization bundle has no baseline to drift
|
|
269
|
+
* from.
|
|
270
|
+
*
|
|
271
|
+
* @returns {{accepted: string[], rejected: Array<{scope,reason}>,
|
|
272
|
+
* overrides: Array<{key, organizationValue, effectiveValue, overriddenBy}>}
|
|
273
|
+
* | null}
|
|
274
|
+
*/
|
|
275
|
+
function computePolicyDrift(scanRoot) {
|
|
276
|
+
const entries = loadPolicyBundles(scanRoot);
|
|
277
|
+
if (!entries.length) return null;
|
|
278
|
+
const publicKeyPem = loadPolicyPublicKey(scanRoot);
|
|
279
|
+
const { effective, provenance, accepted, rejected } = resolveEffectivePolicy(entries, publicKeyPem);
|
|
280
|
+
|
|
281
|
+
const overrides = [];
|
|
282
|
+
const orgEntry = entries.find(e => e.scope === 'organization');
|
|
283
|
+
if (orgEntry && accepted.includes('organization')) {
|
|
284
|
+
for (const [key, organizationValue] of Object.entries(orgEntry.bundle.policy || {})) {
|
|
285
|
+
const overriddenBy = provenance[key];
|
|
286
|
+
if (overriddenBy && overriddenBy !== 'organization' && !_deepEqual(effective[key], organizationValue)) {
|
|
287
|
+
overrides.push({ key, organizationValue, effectiveValue: effective[key], overriddenBy });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { accepted, rejected, overrides };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function _deepEqual(a, b) {
|
|
295
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
/***/ })
|
|
300
|
+
|
|
301
|
+
};
|
package/dist/824.index.js
CHANGED
|
@@ -12,6 +12,7 @@ export const modules = {
|
|
|
12
12
|
/* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(7598);
|
|
13
13
|
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
|
|
14
14
|
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
|
|
15
|
+
/* harmony import */ var _egress_policy_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5712);
|
|
15
16
|
// LLM-assisted vulnerable function extraction for SCA findings.
|
|
16
17
|
//
|
|
17
18
|
// For CVEs without OSV ecosystem_specific data or GHSA fix commits,
|
|
@@ -24,6 +25,7 @@ export const modules = {
|
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
|
|
28
|
+
|
|
27
29
|
const CACHE_DIR = process.env.XDG_CONFIG_HOME
|
|
28
30
|
? node_path__WEBPACK_IMPORTED_MODULE_2__.join(process.env.XDG_CONFIG_HOME, 'agentic-security', 'llm-sca-cache')
|
|
29
31
|
: node_path__WEBPACK_IMPORTED_MODULE_2__.join(process.env.HOME || '/tmp', '.config', 'agentic-security', 'llm-sca-cache');
|
|
@@ -77,6 +79,11 @@ async function extractVulnFunctionsViaLLM(supplyChain, opts = {}) {
|
|
|
77
79
|
if (!isLlmScaEnabled()) return [];
|
|
78
80
|
const config = _endpointConfig();
|
|
79
81
|
if (!config) return [];
|
|
82
|
+
// FR-601: one decision for the whole batch (same endpoint for every CVE in
|
|
83
|
+
// this run) — evaluated before any of the per-candidate prompts below are
|
|
84
|
+
// built.
|
|
85
|
+
const egressDecision = (0,_egress_policy_js__WEBPACK_IMPORTED_MODULE_3__/* .evaluateEgress */ .nn)({ scanRoot: opts.scanRoot, purpose: 'sca-llm-function-extract', endpoint: config.endpoint });
|
|
86
|
+
if (!egressDecision.allowed) return [];
|
|
80
87
|
|
|
81
88
|
const enriched = [];
|
|
82
89
|
const candidates = (supplyChain || []).filter(sc =>
|
package/dist/905.index.js
CHANGED
|
@@ -111,6 +111,8 @@ function partitionCallGraph(callGraph, opts = {}) {
|
|
|
111
111
|
|
|
112
112
|
// EXTERNAL MODULE: ./src/discovery/lenses.js
|
|
113
113
|
var discovery_lenses = __webpack_require__(3499);
|
|
114
|
+
// EXTERNAL MODULE: ./src/egress/policy.js
|
|
115
|
+
var policy = __webpack_require__(5712);
|
|
114
116
|
;// CONCATENATED MODULE: ./src/discovery/llm-invoke.js
|
|
115
117
|
//
|
|
116
118
|
// Shared LLM endpoint caller. Both the hunter and the refutation panel need
|
|
@@ -119,6 +121,8 @@ var discovery_lenses = __webpack_require__(3499);
|
|
|
119
121
|
// other does not, the bug stays buried in one direction.
|
|
120
122
|
//
|
|
121
123
|
|
|
124
|
+
|
|
125
|
+
|
|
122
126
|
const DEFAULT_TIMEOUT_MS = 60000;
|
|
123
127
|
|
|
124
128
|
async function defaultLlmInvoke(prompt, opts = {}) {
|
|
@@ -202,12 +206,27 @@ function consensusOf(responses) {
|
|
|
202
206
|
* An llmInvoke that queries several endpoints and returns the consensus answer.
|
|
203
207
|
* Returns null when no endpoint answered — the callers already treat a null or
|
|
204
208
|
* a throw as degradation, so an all-providers-down run degrades honestly.
|
|
209
|
+
*
|
|
210
|
+
* FR-605 (assurance-hardening PRD): each endpoint gets its OWN egress
|
|
211
|
+
* decision before being included — `mode: local-only` / `deniedProviders`
|
|
212
|
+
* must not be smuggled past just because the SINGLE-endpoint path already
|
|
213
|
+
* checks it. A policy-denied endpoint is EXCLUDED from the vote, the exact
|
|
214
|
+
* same treatment an unreachable endpoint already gets a few lines below
|
|
215
|
+
* (never counted as dissent) — the module's own established pattern
|
|
216
|
+
* extended to a second exclusion reason. Returns `{invoke, decisions}`:
|
|
217
|
+
* `invoke` is null only when EVERY endpoint was denied (the all-down
|
|
218
|
+
* equivalent); `decisions` is the full per-endpoint array for a caller
|
|
219
|
+
* that wants it, kept alongside the single aggregate `decision` the
|
|
220
|
+
* pre-existing callers already read.
|
|
205
221
|
*/
|
|
206
|
-
function makeConsensusInvoke(endpoints, { timeoutMs } = {}) {
|
|
222
|
+
function makeConsensusInvoke(endpoints, { timeoutMs, scanRoot, purpose } = {}) {
|
|
207
223
|
const list = parseEndpoints(endpoints);
|
|
208
|
-
if (list.length === 0) return null;
|
|
209
|
-
|
|
210
|
-
|
|
224
|
+
if (list.length === 0) return { invoke: null, decisions: [] };
|
|
225
|
+
const decisions = list.map(url => (0,policy/* evaluateEgress */.nn)({ scanRoot, purpose: purpose || 'discovery-consensus', endpoint: url }));
|
|
226
|
+
const allowedList = list.filter((_, i) => decisions[i].allowed);
|
|
227
|
+
if (allowedList.length === 0) return { invoke: null, decisions };
|
|
228
|
+
const invoke = async (prompt) => {
|
|
229
|
+
const answers = await Promise.all(allowedList.map(async (url) => {
|
|
211
230
|
try { return await defaultLlmInvoke(prompt, { timeoutMs, endpoint: url }); }
|
|
212
231
|
catch { return null; } // excluded from the vote, never counted as dissent
|
|
213
232
|
}));
|
|
@@ -215,22 +234,61 @@ function makeConsensusInvoke(endpoints, { timeoutMs } = {}) {
|
|
|
215
234
|
if (value === null) throw new Error('no LLM endpoint answered');
|
|
216
235
|
return value;
|
|
217
236
|
};
|
|
237
|
+
return { invoke, decisions };
|
|
218
238
|
}
|
|
219
239
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
// FR-601: egress policy is evaluated here, BEFORE returning a callable and
|
|
241
|
+
// therefore before either caller (hunter.js, disprove.js) builds a prompt —
|
|
242
|
+
// a denied decision makes this resolve to `invoke: null`, which both callers
|
|
243
|
+
// already treat as "nothing to call" via their existing degrade path, so a
|
|
244
|
+
// denial produces no network request through the exact same code path a
|
|
245
|
+
// missing endpoint always has. `decision` carries the machine-readable
|
|
246
|
+
// reason so callers can distinguish "not configured" from "configured but
|
|
247
|
+
// policy-denied" in their own degrade message, rather than reporting a
|
|
248
|
+
// generic "not set" that would be actively wrong once policy is what
|
|
249
|
+
// blocked the call.
|
|
250
|
+
//
|
|
251
|
+
// FR-605 (assurance-hardening PRD): consensus mode (multiple endpoints) is
|
|
252
|
+
// now egress-filtered per endpoint, same as the single-endpoint path below —
|
|
253
|
+
// this is what closes the actual "a remote URL cannot be smuggled into
|
|
254
|
+
// local-only configuration" gap the paragraph below used to describe as
|
|
255
|
+
// open. Per-endpoint CONSTRAINT dimensions beyond allow/deny/local-only
|
|
256
|
+
// (role/region/repository/path/data-class — one provider allowed, another
|
|
257
|
+
// denied for a REASON beyond the deny-list) remain FR-602's separate scope;
|
|
258
|
+
// what changed here is that the single allow/deny/local-only gate FR-601
|
|
259
|
+
// already built is no longer bypassable just by using multiple endpoints
|
|
260
|
+
// instead of one.
|
|
261
|
+
function resolveLlmInvokeWithDecision(opts = {}) {
|
|
262
|
+
// An injected callback is a test/consumer-controlled escape hatch — it
|
|
263
|
+
// bypasses egress the same way it already bypasses endpoint resolution,
|
|
264
|
+
// because there is no real endpoint here for a policy to evaluate.
|
|
265
|
+
if (opts.llmInvoke) return { invoke: opts.llmInvoke, decision: null };
|
|
225
266
|
|
|
226
267
|
const multi = opts.endpoints || process.env[DEFAULT_CONSENSUS_ENV];
|
|
227
268
|
if (multi) {
|
|
228
|
-
const
|
|
229
|
-
|
|
269
|
+
const { invoke, decisions } = makeConsensusInvoke(multi, { timeoutMs: opts.timeoutMs, scanRoot: opts.scanRoot, purpose: opts.purpose });
|
|
270
|
+
// A single aggregate `decision` for the pre-existing callers (hunter.js,
|
|
271
|
+
// disprove.js), which only ever read `.reason` when `invoke` is null —
|
|
272
|
+
// the all-denied case. The full per-endpoint detail is on `decisions`
|
|
273
|
+
// for a caller that wants it.
|
|
274
|
+
const decision = decisions.find(d => !d.allowed) || decisions[0] || null;
|
|
275
|
+
return { invoke, decision, decisions };
|
|
230
276
|
}
|
|
231
277
|
|
|
232
|
-
|
|
233
|
-
|
|
278
|
+
const endpoint = process.env.AGENTIC_SECURITY_LLM_ENDPOINT;
|
|
279
|
+
if (!endpoint) return { invoke: null, decision: null };
|
|
280
|
+
|
|
281
|
+
const decision = (0,policy/* evaluateEgress */.nn)({ scanRoot: opts.scanRoot, purpose: opts.purpose || 'discovery', endpoint });
|
|
282
|
+
if (!decision.allowed) return { invoke: null, decision };
|
|
283
|
+
|
|
284
|
+
return { invoke: (prompt) => defaultLlmInvoke(prompt, { timeoutMs: opts.timeoutMs }), decision };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function resolveLlmInvoke(opts = {}) {
|
|
288
|
+
// Precedence, most explicit first: an injected callback beats configuration,
|
|
289
|
+
// and a multi-endpoint list beats a single endpoint. A caller who supplied
|
|
290
|
+
// their own function must always get exactly that function.
|
|
291
|
+
return resolveLlmInvokeWithDecision(opts).invoke;
|
|
234
292
|
}
|
|
235
293
|
|
|
236
294
|
;// CONCATENATED MODULE: ./src/discovery/hunter.js
|
|
@@ -311,12 +369,16 @@ async function runHunter(focusArea, lens, ctx = {}, opts = {}) {
|
|
|
311
369
|
const transcript = [];
|
|
312
370
|
const lensKey = lens?.key || 'unknown';
|
|
313
371
|
const base = { focusAreaId: focusArea.id, lens: lensKey, transcript };
|
|
314
|
-
const llmInvoke =
|
|
372
|
+
const { invoke: llmInvoke, decision: egressDecision } = resolveLlmInvokeWithDecision({ ...opts, purpose: 'discovery-hunter' });
|
|
315
373
|
|
|
316
374
|
if (typeof llmInvoke !== 'function') {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
375
|
+
// FR-601: distinguish "policy denied a configured endpoint" from "nothing
|
|
376
|
+
// was configured at all" — the reason must reflect which actually happened.
|
|
377
|
+
const reason = egressDecision
|
|
378
|
+
? `egress policy denied this call: ${egressDecision.reason}`
|
|
379
|
+
: 'no llmInvoke supplied and AGENTIC_SECURITY_LLM_ENDPOINT not set';
|
|
380
|
+
appendEntry(transcript, { phase: 'init', reason, egressDecision: egressDecision || undefined });
|
|
381
|
+
return { ...base, candidates: [], degraded: true, reason, egressDecision };
|
|
320
382
|
}
|
|
321
383
|
|
|
322
384
|
let prompt;
|
|
@@ -454,7 +516,11 @@ function parseVote(raw) {
|
|
|
454
516
|
|
|
455
517
|
async function disproveCandidate(candidate, opts = {}) {
|
|
456
518
|
const angles = Array.isArray(opts.angles) && opts.angles.length ? opts.angles : DEFAULT_ANGLES;
|
|
457
|
-
|
|
519
|
+
// FR-601: an egress-policy denial resolves llmInvoke to null the same way a
|
|
520
|
+
// missing endpoint always has, so it falls straight into this module's own
|
|
521
|
+
// pre-existing rule — "silence never refutes" — with zero votes cast and no
|
|
522
|
+
// prompt ever built for a denied endpoint.
|
|
523
|
+
const { invoke: llmInvoke, decision: egressDecision } = resolveLlmInvokeWithDecision({ ...opts, purpose: 'discovery-disprove' });
|
|
458
524
|
|
|
459
525
|
const votes = [];
|
|
460
526
|
if (typeof llmInvoke === 'function') {
|
|
@@ -469,7 +535,7 @@ async function disproveCandidate(candidate, opts = {}) {
|
|
|
469
535
|
const refuteCount = votes.filter(v => v.refuted).length;
|
|
470
536
|
const undecided = voterCount === 0;
|
|
471
537
|
const refuted = !undecided && refuteCount * 2 > voterCount;
|
|
472
|
-
return { ...candidate, refutation: { votes, voterCount, refuteCount, refuted, undecided } };
|
|
538
|
+
return { ...candidate, refutation: { votes, voterCount, refuteCount, refuted, undecided, egressDecision: egressDecision || undefined } };
|
|
473
539
|
}
|
|
474
540
|
|
|
475
541
|
async function disprovePanel(candidates, opts = {}) {
|
|
@@ -800,7 +866,7 @@ function makeTaintProbe(perFileIR, callGraph) {
|
|
|
800
866
|
|
|
801
867
|
async function runDeepAnalysisSafe(perFileIR, callGraph) {
|
|
802
868
|
try {
|
|
803
|
-
const { runDeepAnalysis } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__,
|
|
869
|
+
const { runDeepAnalysis } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 7165));
|
|
804
870
|
return runDeepAnalysis(perFileIR, callGraph, {});
|
|
805
871
|
} catch {
|
|
806
872
|
return null;
|
|
@@ -925,7 +991,7 @@ async function runDiscovery(ctx = {}, opts = {}) {
|
|
|
925
991
|
for (const area of areas) {
|
|
926
992
|
let areaDegradedCount = 0;
|
|
927
993
|
for (const lens of lenses) {
|
|
928
|
-
const run = await runHunter(area, lens, { fileContents: ctx.fileContents || {} }, { llmInvoke });
|
|
994
|
+
const run = await runHunter(area, lens, { fileContents: ctx.fileContents || {} }, { llmInvoke, scanRoot: opts.scanRoot });
|
|
929
995
|
runs.push({ focusAreaId: run.focusAreaId, lens: run.lens, degraded: run.degraded, reason: run.reason, candidateCount: run.candidates.length });
|
|
930
996
|
if (run.degraded && run.reason) reasons.push(`${area.label} × ${lens.key}: ${run.reason}`);
|
|
931
997
|
if (run.degraded) areaDegradedCount += 1;
|
|
@@ -987,7 +1053,7 @@ async function runDiscovery(ctx = {}, opts = {}) {
|
|
|
987
1053
|
'every candidate is reported unconfirmed and severity is not evidence-derived');
|
|
988
1054
|
}
|
|
989
1055
|
const confirmed = await confirmAll(candidates, { taintProbe });
|
|
990
|
-
const { survivors, refuted } = await disprovePanel(confirmed, { llmInvoke });
|
|
1056
|
+
const { survivors, refuted } = await disprovePanel(confirmed, { llmInvoke, scanRoot: opts.scanRoot });
|
|
991
1057
|
const { fresh, duplicates, suppressed } = judgeCandidates(survivors, ctx.priorScan, ctx.triageFeedback);
|
|
992
1058
|
|
|
993
1059
|
// A spent budget is a coverage gap, stated once at the top level rather than
|