@clear-capabilities/agentic-security-scanner 0.154.0 → 0.156.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 +29 -0
- package/bin/agentic-security.js +8 -0
- package/dist/1919.index.js +445 -0
- package/dist/2862.index.js +2 -2
- package/dist/8551.index.js +47 -19
- package/dist/8798.index.js +4 -2
- package/dist/agentic-security.mjs +4 -4
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -4
- package/src/engine.js +2 -2
- package/src/language/assurance.js +2 -2
- package/src/language/haskell-guards.js +12 -5
- package/src/language/haskell-ir.js +8 -1
- package/src/language/haskell-models.js +8 -0
- package/src/language/haskell-security-rules.js +23 -2
- package/src/language/haskell-supply.js +6 -6
- package/src/language/haskell-web.js +3 -0
- package/src/language/nix-secrets.js +2 -0
- package/src/language/nixos-hardening.js +32 -1
- package/src/language/resolved-pass.js +45 -17
- package/src/language/support-registry.js +70 -1
- package/src/language/trusted-inputs.js +38 -0
- package/src/language/witness.js +4 -2
package/dist/8551.index.js
CHANGED
|
@@ -19,14 +19,14 @@ __webpack_require__.d(__webpack_exports__, {
|
|
|
19
19
|
|
|
20
20
|
// EXTERNAL MODULE: external "node:fs"
|
|
21
21
|
var external_node_fs_ = __webpack_require__(73024);
|
|
22
|
-
// EXTERNAL MODULE:
|
|
23
|
-
var
|
|
22
|
+
// EXTERNAL MODULE: external "node:crypto"
|
|
23
|
+
var external_node_crypto_ = __webpack_require__(77598);
|
|
24
|
+
// EXTERNAL MODULE: ./src/language/trusted-inputs.js
|
|
25
|
+
var trusted_inputs = __webpack_require__(6342);
|
|
24
26
|
// EXTERNAL MODULE: ./src/language/haskell-manifests.js
|
|
25
27
|
var haskell_manifests = __webpack_require__(16522);
|
|
26
28
|
// EXTERNAL MODULE: ./src/language/haskell-resolved-graph.js
|
|
27
29
|
var haskell_resolved_graph = __webpack_require__(86359);
|
|
28
|
-
// EXTERNAL MODULE: external "node:crypto"
|
|
29
|
-
var external_node_crypto_ = __webpack_require__(77598);
|
|
30
30
|
;// CONCATENATED MODULE: ./src/language/nix-closure.js
|
|
31
31
|
// Resolved derivation and closure inventory (NIX-008).
|
|
32
32
|
//
|
|
@@ -1032,6 +1032,7 @@ function mergeEvaluationHealth(scanHealth, result) {
|
|
|
1032
1032
|
|
|
1033
1033
|
|
|
1034
1034
|
|
|
1035
|
+
|
|
1035
1036
|
const RESOLVED_PASS_VERSION = 'resolved-pass/1';
|
|
1036
1037
|
|
|
1037
1038
|
const PLAN = /(?:^|\/)dist-newstyle\/cache\/plan\.json$/;
|
|
@@ -1071,6 +1072,12 @@ function resolvedHackageComponents(files) {
|
|
|
1071
1072
|
const gaps = (g.gaps || []).map((x) => ({ kind: `resolved-${x.kind}`, detail: x.detail || x.message || x.kind, file: r.file }));
|
|
1072
1073
|
const summary = { file: r.file, source: g.source, graphAvailable: g.graphAvailable, freshness: g.freshness, closure: g.closure, units: g.units ? g.units.length : null, edges: g.edges ? g.edges.length : null };
|
|
1073
1074
|
if (!g.graphAvailable || g.freshness.status === 'stale') return { components: [], gaps, summary };
|
|
1075
|
+
// The plan is a file in the scanned project. Where the project's own freeze file also pins a package, the two must agree: a plan that
|
|
1076
|
+
// contradicts the freeze is stale or forged, and contributes no versions.
|
|
1077
|
+
const frozen = new Map(((r.manifests && r.manifests.lockedPackages) || []).filter((l) => l.name && l.version).map((l) => [l.name, l.version]));
|
|
1078
|
+
const clashes = (g.units || []).filter((u) => u.name && u.version && frozen.has(u.name) && frozen.get(u.name) !== u.version).map((u) => `${u.name} ${u.version} (plan) vs ${frozen.get(u.name)} (freeze)`);
|
|
1079
|
+
if (clashes.length) { gaps.push({ kind: 'resolved-stale-plan-contradicts-freeze', detail: `the plan contradicts the freeze file (${clashes.slice(0, 3).join('; ')}): it is stale or forged, so no version was taken from it`, file: r.file }); return { components: [], gaps, summary }; }
|
|
1080
|
+
gaps.push({ kind: 'resolved-project-supplied-plan', detail: 'the plan is a file in the scanned project: it is checked against the declared bounds and the freeze file, but whoever controls the project controls its contents', file: r.file });
|
|
1074
1081
|
const SCOPE = { runtime: 'required', test: 'optional', benchmark: 'optional', setup: 'optional', 'build-tool': 'optional' };
|
|
1075
1082
|
const direct = new Set((g.declared || []).map((d) => d.name));
|
|
1076
1083
|
const components = [];
|
|
@@ -1084,24 +1091,39 @@ function resolvedHackageComponents(files) {
|
|
|
1084
1091
|
}
|
|
1085
1092
|
|
|
1086
1093
|
// ── Nix closure ──────────────────────────────────────────────────────────────────
|
|
1087
|
-
function loadNixExports(files) {
|
|
1088
|
-
const exports = []; const sources = []; const problems = [];
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
if (!parsed.ok) { problems.push({ kind: 'malformed-export', file:
|
|
1094
|
+
function loadNixExports(files, env = process.env) {
|
|
1095
|
+
const exports = []; const sources = []; const problems = []; const trust = new Set();
|
|
1096
|
+
const take = (label, text, origin) => {
|
|
1097
|
+
sources.push(label);
|
|
1098
|
+
const parsed = parseJson(text);
|
|
1099
|
+
if (!parsed.ok) { problems.push({ kind: 'malformed-export', file: label, detail: parsed.reason }); return; }
|
|
1093
1100
|
const j = parsed.value;
|
|
1094
|
-
|
|
1101
|
+
trust.add(origin);
|
|
1102
|
+
// NOTE: an `expected` block inside the file is IGNORED. The file is the thing under test, so it cannot also say what it should be
|
|
1103
|
+
// checked against; what an export must match is derived by the scanner (below) or comes from the operator.
|
|
1104
|
+
if (j && Array.isArray(j.exports)) exports.push(...j.exports.filter((e) => e && typeof e === 'object'));
|
|
1095
1105
|
else if (j && typeof j === 'object' && j.schema && (j.data !== undefined || j.text !== undefined)) exports.push(j);
|
|
1096
|
-
else exports.push({ text
|
|
1106
|
+
else exports.push({ text });
|
|
1107
|
+
};
|
|
1108
|
+
if (env.AGENTIC_SECURITY_NIX_EXPORT) {
|
|
1109
|
+
try { take(env.AGENTIC_SECURITY_NIX_EXPORT, (0,external_node_fs_.readFileSync)(env.AGENTIC_SECURITY_NIX_EXPORT, 'utf8'), 'operator'); }
|
|
1110
|
+
catch (e) { sources.push(env.AGENTIC_SECURITY_NIX_EXPORT); problems.push({ kind: 'malformed-export', file: env.AGENTIC_SECURITY_NIX_EXPORT, detail: `unreadable: ${e.code || e.message}` }); }
|
|
1097
1111
|
}
|
|
1098
|
-
|
|
1112
|
+
for (const p of Object.keys(files).filter((x) => NIX_EXPORT.test(x)).sort()) take(p, files[p], 'project-supplied');
|
|
1113
|
+
// What the export must have been made against: the real flake.lock of this project (a forged export cannot also forge that file's hash
|
|
1114
|
+
// without the lock it claims to describe being the lock that is here).
|
|
1115
|
+
const expected = {};
|
|
1116
|
+
const lockPath = Object.keys(files).filter((p) => /(^|\/)flake\.lock$/.test(p)).sort((a, b) => a.length - b.length)[0];
|
|
1117
|
+
if (lockPath) expected.flakeLockSha256 = (0,external_node_crypto_.createHash)('sha256').update(files[lockPath]).digest('hex');
|
|
1118
|
+
else if (sources.length) problems.push({ kind: 'export-unverified', detail: 'there is no flake.lock to bind the export to, so it cannot be shown to describe this project' });
|
|
1119
|
+
if (env.AGENTIC_SECURITY_NIX_EXPORT_PUBKEY) { try { expected.publicKeyPem = (0,external_node_fs_.readFileSync)(env.AGENTIC_SECURITY_NIX_EXPORT_PUBKEY, 'utf8'); expected.requireSigned = true; } catch { problems.push({ kind: 'malformed-export', detail: 'the configured export public key is unreadable' }); } }
|
|
1120
|
+
return { exports, sources, problems, expected, trust: [...trust] };
|
|
1099
1121
|
}
|
|
1100
1122
|
|
|
1101
1123
|
/** The imported closure (and each derivation's env block) for a file set, or null when there is no export. Cached per file set. */
|
|
1102
1124
|
function nixClosureOf(files, { now = Date.now() } = {}) {
|
|
1103
1125
|
return memoized(files, 'nix-closure', () => {
|
|
1104
|
-
const { exports, sources, problems, expected } = loadNixExports(files);
|
|
1126
|
+
const { exports, sources, problems, expected, trust } = loadNixExports(files);
|
|
1105
1127
|
if (!sources.length) return null;
|
|
1106
1128
|
const closure = importNixClosure({ exports, expected, now });
|
|
1107
1129
|
const drvEnv = {};
|
|
@@ -1110,15 +1132,19 @@ function nixClosureOf(files, { now = Date.now() } = {}) {
|
|
|
1110
1132
|
let data = ex.data; if (data === undefined && typeof ex.text === 'string') { const p = parseJson(ex.text); data = p.ok ? p.value : null; }
|
|
1111
1133
|
for (const [k, v] of Object.entries(data || {})) if (v && v.env) drvEnv[k] = v.env;
|
|
1112
1134
|
}
|
|
1113
|
-
|
|
1135
|
+
// A disclosure that says the export does not describe THIS project (another lock, another target, a bad or missing required
|
|
1136
|
+
// signature) means its contents are not evidence about this build: it is refused, not merely labelled.
|
|
1137
|
+
const REFUSE = new Set(['stale-export', 'foreign-target', 'foreign-revision', 'invalid-signature', 'unsigned']);
|
|
1138
|
+
const refused = (closure.disclosures || []).filter((d) => REFUSE.has(d.kind));
|
|
1139
|
+
return { closure, drvEnv, sources, problems, trust, refused };
|
|
1114
1140
|
});
|
|
1115
1141
|
}
|
|
1116
1142
|
|
|
1117
1143
|
/** Advisory records for Nix: a snapshot named by AGENTIC_SECURITY_NIX_ADVISORIES or kept in the project state directory. */
|
|
1118
1144
|
function configuredNixAdvisories(root, env = process.env, hackageDb = null) {
|
|
1119
|
-
const
|
|
1120
|
-
if (!path) return { data: null, reason:
|
|
1121
|
-
let snap; try { snap = JSON.parse((0,external_node_fs_.readFileSync)(path, 'utf8')); } catch (e) { return { data: null, reason: `the Nix advisory snapshot is unreadable: ${e.code || e.message}` }; }
|
|
1145
|
+
const sel = (0,trusted_inputs/* resolveOperatorSnapshot */.Rn)({ envVar: 'AGENTIC_SECURITY_NIX_ADVISORIES', fileName: 'nix-advisories.json', root, env });
|
|
1146
|
+
if (!sel.path) return { data: null, reason: `no Nix advisory snapshot is configured (set AGENTIC_SECURITY_NIX_ADVISORIES, or place nix-advisories.json in the operator configuration directory, agentic-security under XDG_CONFIG_HOME)${sel.projectLocalIgnored ? (0,trusted_inputs/* IGNORED_NOTE */.cY)('nix-advisories.json') : ''}` };
|
|
1147
|
+
let snap; try { snap = JSON.parse((0,external_node_fs_.readFileSync)(sel.path, 'utf8')); } catch (e) { return { data: null, reason: `the Nix advisory snapshot is unreadable: ${e.code || e.message}` }; }
|
|
1122
1148
|
const records = Array.isArray(snap) ? snap : (Array.isArray(snap.records) ? snap.records : []);
|
|
1123
1149
|
return { data: new NixAdvisoryData({ records, hackage: hackageDb, source: 'pinned-snapshot', generatedAt: (snap && snap.generatedAt) || null }), reason: null };
|
|
1124
1150
|
}
|
|
@@ -1134,9 +1160,11 @@ function _analyzeNixClosure(files, { scanRoot = null, env = process.env, now = D
|
|
|
1134
1160
|
const adv = configuredNixAdvisories(scanRoot, env, hackage);
|
|
1135
1161
|
let usage = null; try { usage = (0,haskell_supply/* collectUsage */.c$)(files); } catch { usage = null; }
|
|
1136
1162
|
let matched = null;
|
|
1137
|
-
try { matched = matchNixVulnerabilities({ closure: c.closure, drvEnv: c.drvEnv, data: adv.data, overlays: overlayEvidence(files), haskellUsage: usage }); } catch (e) { matched = null; c.problems.push({ kind: 'match-failed', detail: String((e && e.message) || e).slice(0, 160) }); }
|
|
1163
|
+
if (!c.refused.length) try { matched = matchNixVulnerabilities({ closure: c.closure, drvEnv: c.drvEnv, data: adv.data, overlays: overlayEvidence(files), haskellUsage: usage }); } catch (e) { matched = null; c.problems.push({ kind: 'match-failed', detail: String((e && e.message) || e).slice(0, 160) }); }
|
|
1138
1164
|
const gaps = [];
|
|
1165
|
+
if (c.trust.includes('project-supplied') && !c.trust.includes('operator')) gaps.push({ kind: 'closure-project-supplied', detail: 'the closure export is a file in the scanned project, bound to its flake.lock but not signed: whoever controls the project controls its contents, so the ABSENCE of a finding for a component it lists is not evidence the real build is free of it (provide the export through AGENTIC_SECURITY_NIX_EXPORT, signed with AGENTIC_SECURITY_NIX_EXPORT_PUBKEY, to have it treated as operator evidence)' });
|
|
1139
1166
|
for (const d of c.closure.disclosures || []) gaps.push({ kind: `closure-${d.kind}`, detail: d.detail });
|
|
1167
|
+
for (const d of c.refused) gaps.push({ kind: `closure-refused-${d.kind}`, detail: `${d.detail}; the export was REFUSED and contributes nothing` });
|
|
1140
1168
|
for (const p of c.problems) gaps.push({ kind: `closure-${p.kind}`, detail: p.detail, file: p.file });
|
|
1141
1169
|
if (!adv.data) gaps.push({ kind: 'closure-advisory-feed-unavailable', detail: `${adv.reason}. The closure's components were NOT checked against any advisory: the absence of findings is not a clean result.` });
|
|
1142
1170
|
else if (adv.data.stale) gaps.push({ kind: 'closure-advisory-feed-stale', detail: `the Nix advisory snapshot is stale or undated (${adv.data.ageDays == null ? 'age unknown' : `${Math.floor(adv.data.ageDays)} day(s) old`})` });
|
package/dist/8798.index.js
CHANGED
|
@@ -174,8 +174,10 @@ function judgeFixWitness(before, after) {
|
|
|
174
174
|
function haskellRunner() {
|
|
175
175
|
const p = which('runghc') || which('runhaskell');
|
|
176
176
|
if (!p) return { available: false, reason: 'no Haskell toolchain (runghc) on PATH; witnesses for Haskell findings are unavailable here' };
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
// GHC's first start on a cold, loaded machine (a CI runner) has taken more than 10 s; a probe that gives up there reports a toolchain
|
|
178
|
+
// that is installed as absent, and the answer then differs between two calls in one run.
|
|
179
|
+
const v = (0,node_child_process__WEBPACK_IMPORTED_MODULE_3__.spawnSync)(p, ['--version'], { encoding: 'utf8', timeout: 60_000 });
|
|
180
|
+
return v.status === 0 ? { available: true, path: p, version: String(v.stdout || v.stderr).trim() } : { available: false, reason: v.error && v.error.code === 'ETIMEDOUT' ? 'the Haskell runner did not start within 60 s' : 'the Haskell runner did not start' };
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
/**
|