@clear-capabilities/agentic-security-scanner 0.153.2 → 0.155.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 +47 -0
- package/bin/agentic-security.js +131 -19
- package/dist/1444.index.js +64 -19
- package/dist/1905.index.js +46 -3
- package/dist/1919.index.js +445 -0
- package/dist/2862.index.js +325 -0
- package/dist/3499.index.js +5 -1
- package/dist/3531.index.js +261 -0
- package/dist/3596.index.js +103 -0
- package/dist/4113.index.js +15 -3
- package/dist/4171.index.js +319 -0
- package/dist/5637.index.js +10 -4
- package/dist/6359.index.js +433 -0
- package/dist/7838.index.js +15 -3
- package/dist/8551.index.js +1192 -0
- package/dist/8587.index.js +410 -0
- package/dist/8714.index.js +98 -0
- package/dist/8798.index.js +218 -0
- package/dist/9220.index.js +35 -3
- package/dist/9560.index.js +26 -3
- package/dist/agentic-security.mjs +51 -51
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +20 -3
- package/src/dataflow/CLAUDE.md +9 -0
- package/src/dataflow/catalog-haskell.js +37 -0
- package/src/dataflow/catalog.js +4 -0
- package/src/dataflow/engine.js +33 -3
- package/src/dataflow/privacy-catalog-haskell.js +50 -0
- package/src/dataflow/privacy-catalog.js +5 -1
- package/src/discovery/CLAUDE.md +6 -0
- package/src/discovery/index.js +12 -3
- package/src/discovery/lenses.js +4 -1
- package/src/discovery/partition.js +28 -0
- package/src/egress/redact.js +8 -0
- package/src/engine.js +125 -14
- package/src/ir/index.js +39 -2
- package/src/language/CLAUDE.md +74 -0
- package/src/language/accuracy.js +90 -0
- package/src/language/aibom.js +307 -0
- package/src/language/assurance.js +193 -0
- package/src/language/bom-validate.js +227 -0
- package/src/language/bom.js +236 -0
- package/src/language/bridges.js +392 -0
- package/src/language/compliance-map.js +61 -0
- package/src/language/context.js +199 -0
- package/src/language/contracts.js +339 -0
- package/src/language/discovery.js +359 -0
- package/src/language/engine-pass.js +120 -0
- package/src/language/fix-lifecycle.js +120 -0
- package/src/language/haskell-adapter.js +51 -0
- package/src/language/haskell-disclosure.js +170 -0
- package/src/language/haskell-findings.js +44 -0
- package/src/language/haskell-fix.js +409 -0
- package/src/language/haskell-grammar.js +78 -0
- package/src/language/haskell-guards.js +382 -0
- package/src/language/haskell-ir.js +1377 -0
- package/src/language/haskell-llm.js +128 -0
- package/src/language/haskell-manifests.js +1290 -0
- package/src/language/haskell-models.js +278 -0
- package/src/language/haskell-parser.js +1073 -0
- package/src/language/haskell-resolved-graph.js +414 -0
- package/src/language/haskell-sca.js +387 -0
- package/src/language/haskell-security-rules.js +332 -0
- package/src/language/haskell-supply.js +140 -0
- package/src/language/haskell-sweep.js +85 -0
- package/src/language/haskell-syntax.js +895 -0
- package/src/language/haskell-web.js +749 -0
- package/src/language/nix-adapter.js +42 -0
- package/src/language/nix-agents.js +155 -0
- package/src/language/nix-build-trust.js +790 -0
- package/src/language/nix-closure.js +331 -0
- package/src/language/nix-eval-isolation.js +274 -0
- package/src/language/nix-fix.js +268 -0
- package/src/language/nix-grammar.js +97 -0
- package/src/language/nix-inventory.js +379 -0
- package/src/language/nix-ir.js +642 -0
- package/src/language/nix-parser.js +908 -0
- package/src/language/nix-privacy.js +114 -0
- package/src/language/nix-sca.js +358 -0
- package/src/language/nix-script-taint.js +885 -0
- package/src/language/nix-secrets.js +396 -0
- package/src/language/nixos-hardening.js +564 -0
- package/src/language/nixos-module-resolver.js +595 -0
- package/src/language/nixos-option-catalog.js +96 -0
- package/src/language/pragma.js +150 -0
- package/src/language/resolved-pass.js +174 -0
- package/src/language/secrets.js +323 -0
- package/src/language/state-artifacts.js +78 -0
- package/src/language/support-registry.js +289 -0
- package/src/language/trusted-inputs.js +38 -0
- package/src/language/witness.js +185 -0
- package/src/lineage/CLAUDE.md +8 -0
- package/src/lineage/coverage.js +2 -0
- package/src/lineage/cross-repo-link.js +22 -0
- package/src/lineage/graph-builder.js +5 -1
- package/src/lineage/haskell-view.js +319 -0
- package/src/lineage/index.js +8 -1
- package/src/lineage/nix-view.js +198 -0
- package/src/lineage/sink-registry.js +10 -1
- package/src/lineage/source-registry.js +3 -0
- package/src/lineage/transform-catalog.js +12 -0
- package/src/llm-validator/index.js +16 -2
- package/src/lsp/server.js +69 -9
- package/src/mcp/CLAUDE.md +7 -0
- package/src/mcp/tools.js +26 -3
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/accuracy-scorecard.js +51 -0
- package/src/posture/aibom.js +61 -3
- package/src/posture/artifact-registry.js +12 -0
- package/src/posture/auditor-walkthrough.js +41 -0
- package/src/posture/clustering.js +2 -2
- package/src/posture/evidence-bundle.js +7 -1
- package/src/posture/execution-proof.js +15 -0
- package/src/posture/falsification.js +7 -0
- package/src/posture/family-resolve.js +7 -0
- package/src/posture/fix-history.js +40 -0
- package/src/posture/fix-verify.js +15 -3
- package/src/posture/provenance/git-evidence.js +24 -0
- package/src/posture/provenance/origin-resolver.js +14 -2
- package/src/posture/provenance/predicate-replay.js +11 -2
- package/src/posture/provenance/sca-origin.js +20 -3
- package/src/posture/sbom-diff.js +7 -6
- package/src/posture/sbom.js +85 -32
- package/src/posture/sca-policy.js +10 -1
- package/src/posture/sca-verdict.js +8 -1
- package/src/posture/secret-history.js +62 -18
- package/src/posture/stable-id.js +21 -2
- package/src/posture/state-dir.js +21 -0
- package/src/posture/threat-model-auto.js +4 -4
- package/src/posture/triage.js +18 -1
- package/src/posture/watch-mode.js +34 -3
- package/src/pr-delta.js +9 -4
- package/src/report/index.js +35 -9
- package/src/runScan.js +56 -5
- package/src/sast/pipeline.js +34 -1
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
export const id = 2862;
|
|
2
|
+
export const ids = [2862];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 72862:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// EXPORTS
|
|
10
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
11
|
+
assessLanguageAssurance: () => (/* binding */ assessLanguageAssurance)
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// UNUSED EXPORTS: ASSURANCE_VERSION, OPTIONAL_MODES, REQUIRED_CAPABILITIES
|
|
15
|
+
|
|
16
|
+
// EXTERNAL MODULE: ./src/language/contracts.js
|
|
17
|
+
var contracts = __webpack_require__(43790);
|
|
18
|
+
// EXTERNAL MODULE: ./src/language/haskell-parser.js
|
|
19
|
+
var haskell_parser = __webpack_require__(41745);
|
|
20
|
+
// EXTERNAL MODULE: ./src/language/haskell-grammar.js
|
|
21
|
+
var haskell_grammar = __webpack_require__(50029);
|
|
22
|
+
;// CONCATENATED MODULE: ./src/language/haskell-adapter.js
|
|
23
|
+
// Adapter that plugs the Haskell parser into the language pipeline contracts
|
|
24
|
+
// (CORE-003), so parse outcomes reach scan health instead of staying inside the
|
|
25
|
+
// parser. A missing or corrupt grammar becomes `missing_grammar`, a budget bound
|
|
26
|
+
// becomes `timed_out` (deadline) or `unresolved` (size/depth), and syntax errors
|
|
27
|
+
// and opaque boundaries become `unresolved-branch` outcomes. Findings from other
|
|
28
|
+
// languages are never touched.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const HASKELL_PARSE_PRODUCER = 'language:haskell-parse';
|
|
35
|
+
|
|
36
|
+
// Boundaries whose content the parser could not see. cpp and ffi are disclosed on
|
|
37
|
+
// the parse result but do not make the file unresolved: both CPP branches are
|
|
38
|
+
// parsed and a foreign declaration's own Haskell signature is.
|
|
39
|
+
const OPAQUE_BOUNDARIES = new Set(['th-splice', 'th-quote', 'th-name-quote', 'th-top-level-splice', 'quasiquote', 'hsc', 'generated']);
|
|
40
|
+
|
|
41
|
+
function ensureHaskellParseProducer() {
|
|
42
|
+
if (!(0,contracts/* isRegisteredLanguageProducer */.dO)(HASKELL_PARSE_PRODUCER)) {
|
|
43
|
+
(0,contracts/* registerLanguageProducer */.OQ)({ id: HASKELL_PARSE_PRODUCER, language: 'haskell', capability: 'parse', evidenceKinds: ['source'], version: '1' });
|
|
44
|
+
}
|
|
45
|
+
return HASKELL_PARSE_PRODUCER;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {{grammarSource?:Function, budgets?:object, mode?:string, onParse?:(file:string, parse:object)=>void}} [opts]
|
|
50
|
+
*/
|
|
51
|
+
function createHaskellAdapter(opts = {}) {
|
|
52
|
+
ensureHaskellParseProducer();
|
|
53
|
+
return {
|
|
54
|
+
id: HASKELL_PARSE_PRODUCER,
|
|
55
|
+
language: 'haskell',
|
|
56
|
+
hasGrammar: () => (0,haskell_grammar/* loadHaskellGrammar */.UP)({ grammarSource: opts.grammarSource }).available,
|
|
57
|
+
analyze(file, content) {
|
|
58
|
+
const parse = (0,haskell_parser/* parseHaskell */.cX)(content, { file, grammarSource: opts.grammarSource, budgets: opts.budgets, mode: opts.mode });
|
|
59
|
+
if (typeof opts.onParse === 'function') opts.onParse(file, parse);
|
|
60
|
+
if (parse.status === 'budget_exceeded' && parse.budget && parse.budget.name === 'deadlineMs') {
|
|
61
|
+
throw Object.assign(new Error(`Haskell parser deadline exceeded for ${file}`), { code: 'LANG_TIMEOUT' });
|
|
62
|
+
}
|
|
63
|
+
const unresolved = [];
|
|
64
|
+
if (parse.status === 'budget_exceeded') unresolved.push({ line: 1, reason: `parser budget exceeded: ${parse.budget.name}` });
|
|
65
|
+
else if (parse.status === 'failed') throw new Error(parse.errors[0] ? parse.errors[0].detail : 'Haskell parse failed');
|
|
66
|
+
else {
|
|
67
|
+
for (const e of parse.errors) unresolved.push({ line: e.span ? e.span.startLine : 1, reason: `syntax-error: ${e.kind}` });
|
|
68
|
+
for (const b of parse.boundaries) if (OPAQUE_BOUNDARIES.has(b.kind)) unresolved.push({ line: b.span.startLine, reason: `opaque-boundary: ${b.kind}` });
|
|
69
|
+
}
|
|
70
|
+
return { findings: [], unresolved, parse };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// EXTERNAL MODULE: ./src/language/nix-ir.js
|
|
76
|
+
var nix_ir = __webpack_require__(67772);
|
|
77
|
+
// EXTERNAL MODULE: ./src/language/nix-grammar.js
|
|
78
|
+
var nix_grammar = __webpack_require__(46910);
|
|
79
|
+
;// CONCATENATED MODULE: ./src/language/nix-adapter.js
|
|
80
|
+
// Adapter that plugs the Nix parser and config IR into the language pipeline
|
|
81
|
+
// contracts (CORE-003), so parse outcomes reach scan health instead of staying
|
|
82
|
+
// inside the parser. A missing or corrupt grammar becomes `missing_grammar`, a
|
|
83
|
+
// deadline hit becomes `timed_out`, and syntax errors, dynamic attributes,
|
|
84
|
+
// dynamic imports, lazy recursion and IR/parser budget hits become `unresolved`
|
|
85
|
+
// outcomes. Findings from other languages are never touched.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
const NIX_PARSE_PRODUCER = 'language:nix-parse';
|
|
92
|
+
|
|
93
|
+
function ensureNixParseProducer() {
|
|
94
|
+
if (!(0,contracts/* isRegisteredLanguageProducer */.dO)(NIX_PARSE_PRODUCER)) {
|
|
95
|
+
(0,contracts/* registerLanguageProducer */.OQ)({ id: NIX_PARSE_PRODUCER, language: 'nix', capability: 'parse', evidenceKinds: ['source'], version: '1' });
|
|
96
|
+
}
|
|
97
|
+
return NIX_PARSE_PRODUCER;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {{grammarSource?:Function, budgets?:object, irBudgets?:object, onParse?:(file:string, result:object)=>void}} [opts]
|
|
102
|
+
*/
|
|
103
|
+
function createNixAdapter(opts = {}) {
|
|
104
|
+
ensureNixParseProducer();
|
|
105
|
+
return {
|
|
106
|
+
id: NIX_PARSE_PRODUCER,
|
|
107
|
+
language: 'nix',
|
|
108
|
+
hasGrammar: () => (0,nix_grammar/* loadNixGrammar */.T$)({ grammarSource: opts.grammarSource }).available,
|
|
109
|
+
analyze(file, content) {
|
|
110
|
+
const result = (0,nix_ir/* analyzeNix */.KL)(content, { file, grammarSource: opts.grammarSource, budgets: opts.budgets, irBudgets: opts.irBudgets });
|
|
111
|
+
if (typeof opts.onParse === 'function') opts.onParse(file, result);
|
|
112
|
+
const { parse, ir } = result;
|
|
113
|
+
if (parse.status === 'budget_exceeded' && parse.budget && parse.budget.name === 'deadlineMs') {
|
|
114
|
+
throw Object.assign(new Error(`Nix parser deadline exceeded for ${file}`), { code: 'LANG_TIMEOUT' });
|
|
115
|
+
}
|
|
116
|
+
if (parse.status === 'failed' && !parse.errors.length) throw new Error('Nix parse failed');
|
|
117
|
+
const unresolved = ir.unresolved.map((u) => ({ line: u.span ? u.span.startLine : 1, reason: `${u.kind}: ${u.detail}` }));
|
|
118
|
+
return { findings: [], unresolved, parse, ir };
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// EXTERNAL MODULE: ./src/language/haskell-manifests.js
|
|
124
|
+
var haskell_manifests = __webpack_require__(16522);
|
|
125
|
+
// EXTERNAL MODULE: ./src/language/haskell-models.js
|
|
126
|
+
var haskell_models = __webpack_require__(54084);
|
|
127
|
+
;// CONCATENATED MODULE: ./src/language/assurance.js
|
|
128
|
+
// Scan health and assurance for Haskell and Nix analysis (X-007).
|
|
129
|
+
//
|
|
130
|
+
// Collects every way language analysis can be incomplete into ONE `languageCoverage` input for computeScanHealth,
|
|
131
|
+
// so the existing advisory / standard / strict policy and the "Safe to deploy" verdict apply unchanged:
|
|
132
|
+
//
|
|
133
|
+
// required capabilities parser, SAST, taint, SCA, secrets (Haskell); parser, config, taint, secrets, supply (Nix).
|
|
134
|
+
// A required capability that is disabled, unavailable or failed is a CONDITION.
|
|
135
|
+
// expected static limits an opaque boundary (Template Haskell, quasi-quotes, FFI, CPP, hsc) is a documented limit
|
|
136
|
+
// of a static analysis, not a failure. It is listed under `limitations`, never as a
|
|
137
|
+
// condition, and never hides the files it touches (they stay `unresolved` in the ledger).
|
|
138
|
+
// optional modes nix-eval, cabal-plan, hackage-live. Their state is separate: `not_selected` (no
|
|
139
|
+
// condition), `not_run` (selected but absent: a condition, never reported as run), `failed`
|
|
140
|
+
// (a condition; static findings are retained untouched), `ok`.
|
|
141
|
+
// data currency a stale or missing advisory snapshot, a malformed manifest, a partial effective NixOS
|
|
142
|
+
// configuration.
|
|
143
|
+
//
|
|
144
|
+
// Independent findings are never dropped here: this module only produces health inputs and returns the findings it
|
|
145
|
+
// was given by reference, with a count, so a caller can prove retention.
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
const ASSURANCE_VERSION = 'language-assurance/1';
|
|
154
|
+
|
|
155
|
+
const REQUIRED_CAPABILITIES = Object.freeze({
|
|
156
|
+
haskell: Object.freeze(['parser', 'sast', 'taint', 'sca', 'secrets']),
|
|
157
|
+
nix: Object.freeze(['parser', 'config', 'taint', 'secrets', 'supply']),
|
|
158
|
+
});
|
|
159
|
+
const OPTIONAL_MODES = Object.freeze(['nix-eval', 'cabal-plan', 'hackage-live']);
|
|
160
|
+
|
|
161
|
+
const MALFORMED_MANIFEST = new Set(['malformed_manifest', 'malformed_yaml', 'parser_failure', 'read_failed', 'malformed_dependency', 'malformed_lock_entry', 'malformed_constraint', 'malformed_extra_dep', 'invalid_version_range', 'empty_manifest']);
|
|
162
|
+
const LANGS = Object.keys(REQUIRED_CAPABILITIES);
|
|
163
|
+
const langOf = (f) => (/\.l?hs$|\.hs-boot$|\.hsc$/i.test(f) ? 'haskell' : /\.nix$/i.test(f) ? 'nix' : null);
|
|
164
|
+
const isManifest = (f) => /(?:^|\/)(?:[^/]+\.cabal|cabal\.project(?:\.freeze|\.local)?|package\.yaml|stack\.yaml(?:\.lock)?)$/i.test(f);
|
|
165
|
+
|
|
166
|
+
function optionalState(name, cfg) {
|
|
167
|
+
const c = cfg && cfg[name];
|
|
168
|
+
if (!c || !c.selected) return { name, selected: false, status: 'not_selected', ran: false, reason: 'the mode is opt-in and was not selected', condition: null };
|
|
169
|
+
const r = c.result;
|
|
170
|
+
if (!r) return { name, selected: true, status: 'not_run', ran: false, reason: c.reason || 'selected, but no result was produced', condition: `optional mode "${name}" was selected but did not run` };
|
|
171
|
+
if (r.status === 'ok' && r.ran !== false) return { name, selected: true, status: 'ok', ran: true, reason: null, condition: null };
|
|
172
|
+
return { name, selected: true, status: r.status || 'failed', ran: false, reason: r.reason || null, condition: `optional mode "${name}" ${r.status === 'timed_out' ? 'timed out' : 'failed'}${r.reason ? ` (${r.reason})` : ''}; static results are retained` };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {object} input
|
|
177
|
+
* @param {Record<string,string>} input.files rel path -> content: sources and manifests
|
|
178
|
+
* @param {object[]} [input.adapters] override the default parse adapters (tests inject hung or corrupt ones)
|
|
179
|
+
* @param {number} [input.timeoutMs] per-file adapter deadline
|
|
180
|
+
* @param {string[]} [input.disabled] capability ids switched off, "<language>:<capability>"
|
|
181
|
+
* @param {Record<string,string[]>} [input.required] override of REQUIRED_CAPABILITIES
|
|
182
|
+
* @param {object|null} [input.advisoryDb] a Hackage AdvisoryDb (haskell-sca.js), or null when none loaded
|
|
183
|
+
* @param {object[]} [input.effectiveConfigs] NixOS effective-config reports (nixos-module-resolver.js)
|
|
184
|
+
* @param {Record<string,{selected:boolean, result?:object, reason?:string}>} [input.optional]
|
|
185
|
+
* @param {object[]} [input.findings] findings already produced; returned untouched
|
|
186
|
+
*/
|
|
187
|
+
async function assessLanguageAssurance(input = {}) {
|
|
188
|
+
const files = input.files || {};
|
|
189
|
+
const present = new Set(Object.keys(files).map(langOf).filter(Boolean));
|
|
190
|
+
const manifests = Object.entries(files).filter(([f]) => isManifest(f));
|
|
191
|
+
if (manifests.length) present.add('haskell');
|
|
192
|
+
const required = input.required || REQUIRED_CAPABILITIES;
|
|
193
|
+
const disabled = new Set(input.disabled || []);
|
|
194
|
+
const findings = Array.isArray(input.findings) ? input.findings : [];
|
|
195
|
+
const conditions = [];
|
|
196
|
+
const limitations = [];
|
|
197
|
+
|
|
198
|
+
// 1. parse adapters over every language source
|
|
199
|
+
// Boundaries that do not make a file unresolved but that a reader must be told about: CPP (every branch was parsed, the
|
|
200
|
+
// chosen one is not known) and FFI (the foreign code is not analysed).
|
|
201
|
+
const disclosed = new Map(); // boundary kind -> {count, files:Set}
|
|
202
|
+
const projectModules = new Set(); const importedModules = new Map(); // module -> files
|
|
203
|
+
const onParse = (file, parse) => {
|
|
204
|
+
if (parse && parse.module && parse.module.name) projectModules.add(parse.module.name);
|
|
205
|
+
for (const i of (parse && parse.imports) || []) if (i && i.module) { const e = importedModules.get(i.module) || new Set(); e.add(file); importedModules.set(i.module, e); }
|
|
206
|
+
for (const b of (parse && parse.boundaries) || []) if (b.kind === 'cpp' || b.kind === 'ffi') { const e = disclosed.get(b.kind) || { count: 0, files: new Set() }; e.count += b.count || 1; e.files.add(file); disclosed.set(b.kind, e); }
|
|
207
|
+
};
|
|
208
|
+
const adapters = input.adapters || [createHaskellAdapter({ onParse }), createNixAdapter()];
|
|
209
|
+
const run = await (0,contracts/* runLanguageAnalysis */.i5)({ files, adapters, timeoutMs: input.timeoutMs || 0 });
|
|
210
|
+
const reconcile = (0,contracts/* reconcileLanguageLedger */.SA)(run.ledger, files);
|
|
211
|
+
const base = (0,contracts/* languageHealth */.bX)({ ledger: run.ledger, outcomes: run.outcomes });
|
|
212
|
+
const byKind = { ...base.byKind };
|
|
213
|
+
|
|
214
|
+
// outcome classification: expected static limits are not conditions
|
|
215
|
+
const opaque = run.outcomes.filter((o) => o.kind === 'unresolved-branch' && /^opaque-boundary/.test(String(o.detail || '')));
|
|
216
|
+
const otherUnresolved = run.outcomes.filter((o) => o.kind === 'unresolved-branch' && !/^opaque-boundary/.test(String(o.detail || '')));
|
|
217
|
+
if (byKind['missing-grammar']) conditions.push(`${byKind['missing-grammar']} language file(s) skipped: grammar unavailable or corrupt`);
|
|
218
|
+
if (byKind['adapter-exception']) conditions.push(`${byKind['adapter-exception']} language adapter exception(s)`);
|
|
219
|
+
if (byKind['timeout']) conditions.push(`${byKind['timeout']} language file(s) timed out`);
|
|
220
|
+
if (otherUnresolved.length) conditions.push(`${otherUnresolved.length} unresolved language construct(s) (syntax errors, dynamic attributes or imports)`);
|
|
221
|
+
if (byKind['unregistered-producer']) conditions.push(`${byKind['unregistered-producer']} result(s) from an unregistered language producer were discarded`);
|
|
222
|
+
if (opaque.length) {
|
|
223
|
+
const kinds = {};
|
|
224
|
+
for (const o of opaque) { const k = String(o.detail).replace(/^opaque-boundary:\s*/, ''); kinds[k] = (kinds[k] || 0) + 1; }
|
|
225
|
+
for (const [k, n] of Object.entries(kinds).sort()) limitations.push({ kind: 'opaque-boundary', boundary: k, count: n, note: 'a documented limit of static analysis: this code is not analysed, and is not reported clean' });
|
|
226
|
+
}
|
|
227
|
+
const DISCLOSED_NOTE = { cpp: 'preprocessor conditionals: every branch was parsed, but which branch is compiled is not known', ffi: 'foreign declarations: the foreign code is not analysed and its effects are not modelled' };
|
|
228
|
+
for (const [k, e] of [...disclosed.entries()].sort()) limitations.push({ kind: 'opaque-boundary', boundary: k, count: e.count, files: [...e.files].sort().slice(0, 20), note: `a documented limit of static analysis (${DISCLOSED_NOTE[k]})` });
|
|
229
|
+
// Imported modules this scan has no model for: their functions are ordinary, opaque calls (never a source, sink or
|
|
230
|
+
// sanitizer), so a taint flow through them is widened and disclosed on the finding. Listed so a reader knows how much of the
|
|
231
|
+
// import surface the models cover. A limitation, not a condition: most real projects import unmodelled modules.
|
|
232
|
+
const unmodeled = [...importedModules.keys()].filter((m) => !projectModules.has(m) && !(0,haskell_models/* packageOfModule */.KF)(m)).sort();
|
|
233
|
+
if (unmodeled.length) limitations.push({ kind: 'unmodeled-imports', count: unmodeled.length, modules: unmodeled.slice(0, 30), note: 'no security model exists for these imported modules: their functions are analysed as ordinary calls, so a flow through them is widened and disclosed on the finding' });
|
|
234
|
+
// Sources the default ignore list kept out of the scan (tests, specs, mocks, bin, build, dist, vendor): the same
|
|
235
|
+
// policy every language has, but stated here so an unscanned Haskell/Nix file is never invisible.
|
|
236
|
+
// Sources and manifests over the read cap were NOT analysed: unlike a default-ignored directory, that is a gap in the analysis,
|
|
237
|
+
// so it is a condition (the verdict cannot be complete), listed with the path, its size and the cap.
|
|
238
|
+
const sizeSkipped = Array.isArray(input.sizeSkipped) ? input.sizeSkipped : [];
|
|
239
|
+
if (sizeSkipped.length) conditions.push(`${sizeSkipped.length} Haskell/Nix source or manifest file(s) exceed the size cap and were not analysed: ${sizeSkipped.slice(0, 3).map((x) => `${x.file} (${x.bytes} bytes > ${x.cap})`).join(', ')}${sizeSkipped.length > 3 ? ', ...' : ''}`);
|
|
240
|
+
const policyExcluded = Array.isArray(input.policyExcluded) ? input.policyExcluded : [];
|
|
241
|
+
if (policyExcluded.length) limitations.push({ kind: 'default-ignore', count: policyExcluded.length, files: policyExcluded.slice(0, 20), note: 'Haskell/Nix files under a default-ignored directory (tests, specs, mocks, bin, build, dist, vendor) are not scanned; this is the project-wide default for every language' });
|
|
242
|
+
// Nix-built images and declared OCI services are not image-layer or base-image scanned: the container analyzers read
|
|
243
|
+
// Dockerfile and compose SOURCE. Stated whenever such a declaration is present, so its absence from the findings is
|
|
244
|
+
// never read as a clean image.
|
|
245
|
+
const ociFiles = Object.entries(files).filter(([f, t]) => /\.nix$/i.test(f) && typeof t === 'string' && /oci-containers|dockerTools\.(?:buildImage|buildLayeredImage|streamLayeredImage)|virtualisation\.(?:docker|podman)/.test(t)).map(([f]) => f);
|
|
246
|
+
if (ociFiles.length) limitations.push({ kind: 'container-image-scan', files: ociFiles.sort(), note: 'Nix-built images and declared OCI services are analysed as configuration only: image layers and base-image vulnerabilities are NOT scanned' });
|
|
247
|
+
if (!reconcile.ok) conditions.push(`language ledger does not reconcile with the inputs: ${reconcile.errors[0]}`);
|
|
248
|
+
|
|
249
|
+
// 2. manifests
|
|
250
|
+
if (manifests.length) {
|
|
251
|
+
let m = null;
|
|
252
|
+
try { m = (0,haskell_manifests/* analyzeHaskellManifests */.JS)(manifests.map(([path, text]) => ({ path, text })), {}); } catch (e) { conditions.push(`Haskell manifest analysis failed: ${String((e && e.message) || e)}`); }
|
|
253
|
+
if (m) {
|
|
254
|
+
const bad = m.diagnostics.filter((d) => MALFORMED_MANIFEST.has(d.kind));
|
|
255
|
+
if (bad.length) conditions.push(`${bad.length} malformed Haskell manifest entr${bad.length === 1 ? 'y' : 'ies'}: ${[...new Set(bad.map((d) => d.kind))].join(', ')}`);
|
|
256
|
+
if (m.coverage && m.coverage.failed) conditions.push(`${m.coverage.failed} Haskell manifest(s) could not be parsed`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 3. advisory feed currency (only meaningful when Haskell dependencies are in play and SCA is required)
|
|
261
|
+
const scaRequired = (required.haskell || []).includes('sca') && !disabled.has('haskell:sca');
|
|
262
|
+
if (present.has('haskell') && scaRequired && manifests.length) {
|
|
263
|
+
const db = input.advisoryDb;
|
|
264
|
+
if (!db) conditions.push(`no Hackage advisory snapshot is loaded: Haskell dependency vulnerabilities were not assessed${input.advisoryReason ? `. ${input.advisoryReason}` : ''}`);
|
|
265
|
+
else if (db.stale) conditions.push(`the Hackage advisory snapshot is stale (${db.ageDays == null ? 'age unknown' : `${Math.floor(db.ageDays)} day(s) old`}, limit ${db.maxAgeDays})`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// 3b. resolved dependency data and the Nix closure: a stale, malformed or unchecked input is stated, never silently dropped
|
|
269
|
+
for (const g of input.supplyGaps || []) {
|
|
270
|
+
if (!g || !g.kind) continue;
|
|
271
|
+
if (/unavailable|stale|malformed|failed|unverified|refused|contradicts/.test(g.kind)) conditions.push(`${g.kind.replace(/^(?:resolved|closure)-/, '')}: ${String(g.detail || '').slice(0, 220)}`);
|
|
272
|
+
else limitations.push({ kind: 'supply-gap', gap: g.kind, note: String(g.detail || '').slice(0, 220) });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (input.licenseUnavailable) limitations.push({ kind: 'license-data-unavailable', count: input.licenseUnavailable, note: 'Haskell and Nix dependency records carry no license data, so no license policy was applied to them; this is not a statement that they are acceptable' });
|
|
276
|
+
|
|
277
|
+
// 4. effective NixOS configuration
|
|
278
|
+
for (const r of input.effectiveConfigs || []) {
|
|
279
|
+
if (r && r.completeness === 'partial') conditions.push(`the effective NixOS configuration for ${(r.target && r.target.name) || 'a target'} is partial: ${(r.unresolved || []).length} unresolved, ${(r.truncated || []).length} truncated`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// 5. required capabilities
|
|
283
|
+
const capabilities = {};
|
|
284
|
+
for (const lang of LANGS) {
|
|
285
|
+
if (!present.has(lang)) continue;
|
|
286
|
+
capabilities[lang] = {};
|
|
287
|
+
for (const cap of required[lang] || []) {
|
|
288
|
+
const id = `${lang}:${cap}`;
|
|
289
|
+
let status = 'ran'; let detail = null;
|
|
290
|
+
if (disabled.has(id)) { status = 'disabled'; detail = 'switched off by configuration'; }
|
|
291
|
+
else if (cap === 'parser') {
|
|
292
|
+
const counts = Object.values(run.ledger.byAnalyzer).filter((a) => a.language === lang);
|
|
293
|
+
const failed = counts.reduce((s, a) => s + a.failed + a.timedOut + a.missingGrammar, 0);
|
|
294
|
+
if (failed) { status = 'failed'; detail = `${failed} file(s) not parsed`; }
|
|
295
|
+
}
|
|
296
|
+
capabilities[lang][cap] = { required: true, status, detail };
|
|
297
|
+
if (status === 'disabled') conditions.push(`required analyzer "${id}" is disabled`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 6. optional modes
|
|
302
|
+
const optionalModes = {};
|
|
303
|
+
for (const name of OPTIONAL_MODES) {
|
|
304
|
+
const st = optionalState(name, input.optional);
|
|
305
|
+
optionalModes[name] = { selected: st.selected, status: st.status, ran: st.ran, reason: st.reason };
|
|
306
|
+
if (st.condition) conditions.push(st.condition);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
version: ASSURANCE_VERSION,
|
|
311
|
+
languageCoverage: { totals: base.totals, byKind, conditions, limitations, capabilities, optionalModes },
|
|
312
|
+
ledger: { reconciled: reconcile.ok, errors: reconcile.errors, byAnalyzer: run.ledger.byAnalyzer },
|
|
313
|
+
outcomes: run.outcomes,
|
|
314
|
+
retained: { findings: findings.length, findingsRef: findings },
|
|
315
|
+
conditions,
|
|
316
|
+
limitations,
|
|
317
|
+
optionalModes,
|
|
318
|
+
capabilities,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
/***/ })
|
|
324
|
+
|
|
325
|
+
};
|
package/dist/3499.index.js
CHANGED
|
@@ -10,6 +10,7 @@ export const modules = {
|
|
|
10
10
|
/* harmony export */ LENSES: () => (/* binding */ LENSES),
|
|
11
11
|
/* harmony export */ j: () => (/* binding */ buildHunterPrompt)
|
|
12
12
|
/* harmony export */ });
|
|
13
|
+
/* harmony import */ var _egress_redact_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(74831);
|
|
13
14
|
//
|
|
14
15
|
// The seven hunting lenses. Each hunter run is one (focus area × lens) pair.
|
|
15
16
|
//
|
|
@@ -19,6 +20,7 @@ export const modules = {
|
|
|
19
20
|
// same code than one told to look at crypto, so the union covers failure modes
|
|
20
21
|
// no single prompt reaches. `wildcard` exists because a fixed taxonomy is a
|
|
21
22
|
// ceiling, and the classes worth finding are the ones not on the list.
|
|
23
|
+
|
|
22
24
|
const LENSES = Object.freeze([
|
|
23
25
|
{ key: 'injection', title: 'Injection', family: 'injection', cwe: 'CWE-74',
|
|
24
26
|
brief: 'Untrusted input reaching an interpreter: SQL, shell, template, XPath, LDAP, or deserialization. Follow the value, not the function name.' },
|
|
@@ -51,7 +53,9 @@ function buildHunterPrompt(focusArea, lens, ctx = {}) {
|
|
|
51
53
|
let budget = maxChars;
|
|
52
54
|
const blocks = [];
|
|
53
55
|
for (const f of files) {
|
|
54
|
-
|
|
56
|
+
// Everything that leaves the machine is redacted first (secrets, PII/PHI/PCI-shaped fields, operator-declared
|
|
57
|
+
// customer data, and whole proprietary paths), by the same choke point the validator and the proposers use.
|
|
58
|
+
const src = (0,_egress_redact_js__WEBPACK_IMPORTED_MODULE_0__/* .redactPayload */ .cy)({ text: contents[f], filePath: f, scanRoot: ctx.scanRoot || null }).text;
|
|
55
59
|
const slice = src.length > budget ? src.slice(0, Math.max(0, budget)) : src;
|
|
56
60
|
const truncated = slice.length < src.length;
|
|
57
61
|
blocks.push(`--- SOURCE FILE (untrusted data): ${f}${truncated ? ' (truncated)' : ''} ---\n${slice}\n--- END SOURCE FILE: ${f} ---`);
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
export const id = 3531;
|
|
2
|
+
export const ids = [3531];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 33531:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
__webpack_require__.r(__webpack_exports__);
|
|
9
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
10
|
+
/* harmony export */ LANGUAGE_BOM_VERSION: () => (/* binding */ LANGUAGE_BOM_VERSION),
|
|
11
|
+
/* harmony export */ genericPurl: () => (/* binding */ genericPurl),
|
|
12
|
+
/* harmony export */ languageBom: () => (/* binding */ languageBom),
|
|
13
|
+
/* harmony export */ languagePbom: () => (/* binding */ languagePbom),
|
|
14
|
+
/* harmony export */ purlFromGitHost: () => (/* binding */ purlFromGitHost),
|
|
15
|
+
/* harmony export */ purlFromUpstreamUrl: () => (/* binding */ purlFromUpstreamUrl)
|
|
16
|
+
/* harmony export */ });
|
|
17
|
+
/* harmony import */ var _haskell_supply_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(86349);
|
|
18
|
+
/* harmony import */ var _haskell_sca_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2437);
|
|
19
|
+
/* harmony import */ var _haskell_resolved_graph_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(86359);
|
|
20
|
+
/* harmony import */ var _nix_inventory_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(81303);
|
|
21
|
+
// Hackage and Nix components, dependency edges and target provenance for the SBOM emitters (X-010).
|
|
22
|
+
//
|
|
23
|
+
// Turns what the language analyses already know into BOM-shaped data WITHOUT inventing any of it:
|
|
24
|
+
//
|
|
25
|
+
// * A version appears only where a resolver or a lock states one. A declared range (`aeson ==2.1.*`) is a property,
|
|
26
|
+
// never a version. A Nix package selector (`pkgs.git`) is not a component at all.
|
|
27
|
+
// * A dependency graph appears only from a fresh resolved plan/export. A manifest or a freeze file gives declared
|
|
28
|
+
// edges at most, and the document says so through `compositions` (`incomplete`), never `complete`.
|
|
29
|
+
// * Resolved builds are never merged away: two Hackage units that differ in flags, or two Nix outputs that share a
|
|
30
|
+
// name and version but not a store hash, stay two components with two bom-refs.
|
|
31
|
+
// * A license appears only when a source states one. Nothing here guesses a license from a name.
|
|
32
|
+
// * Purls follow the registered type rules: `hackage`, `github` for validated upstream hosts, and `generic` (with
|
|
33
|
+
// vcs_url / download_url qualifiers) as the labelled fallback. Nix build provenance travels beside the identity
|
|
34
|
+
// as properties (store path, derivation, system, installable, lock digest), not inside the purl.
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
const LANGUAGE_BOM_VERSION = 'language-bom/1';
|
|
42
|
+
|
|
43
|
+
const enc = (s) => encodeURIComponent(String(s)).replace(/%2F/gi, '/').replace(/%40/g, '%40');
|
|
44
|
+
const qEnc = (s) => encodeURIComponent(String(s));
|
|
45
|
+
const prop = (name, value) => (value === null || value === undefined || value === '' ? null : { name: `agentic-security:${name}`, value: typeof value === 'string' ? value : JSON.stringify(value) });
|
|
46
|
+
const props = (...xs) => xs.filter(Boolean);
|
|
47
|
+
|
|
48
|
+
/** purl for a generic component, qualifiers sorted by key as the purl spec requires. */
|
|
49
|
+
function genericPurl(name, version, qualifiers = {}) {
|
|
50
|
+
const q = Object.entries(qualifiers).filter(([, v]) => v !== null && v !== undefined && v !== '').sort(([a], [b]) => (a < b ? -1 : 1)).map(([k, v]) => `${k.toLowerCase()}=${qEnc(v)}`);
|
|
51
|
+
return `pkg:generic/${enc(name)}${version ? `@${qEnc(version)}` : ''}${q.length ? `?${q.join('&')}` : ''}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A purl for a recognised source host; null otherwise (never guessed). */
|
|
55
|
+
function purlFromGitHost(type, owner, repo, rev) {
|
|
56
|
+
if (!['github', 'gitlab', 'bitbucket'].includes(type) || !owner || !repo) return null;
|
|
57
|
+
return `pkg:${type}/${enc(String(owner).toLowerCase())}/${enc(String(repo).toLowerCase())}${rev ? `@${qEnc(rev)}` : ''}`;
|
|
58
|
+
}
|
|
59
|
+
const GH_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?\/(?:releases\/download|archive)\//i;
|
|
60
|
+
/** Validated upstream identity from a source URL on a known host. */
|
|
61
|
+
function purlFromUpstreamUrl(url, version) {
|
|
62
|
+
const m = GH_URL.exec(String(url || ''));
|
|
63
|
+
return m ? purlFromGitHost('github', m[1], m[2], version) : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const cdxScope = (scopes) => ((scopes || []).includes('runtime') ? 'required' : 'excluded');
|
|
67
|
+
|
|
68
|
+
// ── Haskell ─────────────────────────────────────────────────────────────────
|
|
69
|
+
function haskellBom(files, opts) {
|
|
70
|
+
const out = { components: [], dependencies: [], compositions: [], properties: [], gaps: [], root: null };
|
|
71
|
+
const { components: declared, manifests } = (0,_haskell_supply_js__WEBPACK_IMPORTED_MODULE_0__/* .hackageComponents */ .vG)(files);
|
|
72
|
+
if (!manifests) return out;
|
|
73
|
+
const pkg = (manifests.packages || [])[0] || null;
|
|
74
|
+
const rootRef = pkg ? `haskell-root:${pkg.name}@${pkg.version || 'unversioned'}` : 'haskell-root';
|
|
75
|
+
if (pkg) out.root = { type: 'application', 'bom-ref': rootRef, name: pkg.name, ...(pkg.version ? { version: pkg.version } : {}) };
|
|
76
|
+
|
|
77
|
+
const graph = opts.resolved || (0,_haskell_resolved_graph_js__WEBPACK_IMPORTED_MODULE_2__/* .buildResolvedGraph */ .xU)({ manifests, plan: opts.plan, stackExport: opts.stackExport, expected: opts.expected, rejectStale: opts.rejectStale });
|
|
78
|
+
out.properties.push(prop('haskell:resolution', graph.source), prop('haskell:freshness', graph.freshness && graph.freshness.status), prop('haskell:closure', graph.closure));
|
|
79
|
+
if (graph.compiler) out.properties.push(prop('haskell:compiler', graph.compiler.id));
|
|
80
|
+
out.gaps.push(...(graph.gaps || []).map((g) => ({ ...g, source: 'haskell' })));
|
|
81
|
+
|
|
82
|
+
const refOf = new Map(); // unit id -> bom-ref
|
|
83
|
+
if (graph.graphAvailable && graph.units) {
|
|
84
|
+
for (const u of graph.units) {
|
|
85
|
+
if (u.origin === 'local') { refOf.set(u.id, rootRef); continue; }
|
|
86
|
+
const ref = `hackage:${u.id}`;
|
|
87
|
+
refOf.set(u.id, ref);
|
|
88
|
+
let purl = null;
|
|
89
|
+
if (u.origin === 'hackage' || u.origin === 'compiler') purl = (0,_haskell_sca_js__WEBPACK_IMPORTED_MODULE_1__/* .hackagePurl */ .Gf)(u.name, u.version || null);
|
|
90
|
+
else if (u.origin === 'vcs') purl = genericPurl(u.name, u.version, { vcs_url: u.repo && u.repo.uri });
|
|
91
|
+
else if (u.origin === 'url') purl = genericPurl(u.name, u.version, { download_url: u.repo && u.repo.uri });
|
|
92
|
+
out.components.push({
|
|
93
|
+
ecosystem: 'hackage', name: u.name, version: u.version || null, purl, bomRef: ref, filePath: (files && Object.keys(files).find((f) => /\.cabal$|cabal\.project$/.test(f))) || undefined, cdxScope: cdxScope(u.scopes), scopes: u.scopes || [],
|
|
94
|
+
// identity across versions: the package, plus its flag assignment when it has one (two builds stay two components)
|
|
95
|
+
identityKey: `hackage:${u.name}${u.flags && Object.keys(u.flags).length ? `?${JSON.stringify(u.flags)}` : ''}${u.sublibrary ? `:${u.sublibrary}` : ''}`,
|
|
96
|
+
...(u.sha256 ? { hashes: [{ alg: 'SHA-256', content: u.sha256 }] } : {}),
|
|
97
|
+
properties: props(prop('haskell:unitId', u.id), prop('haskell:origin', u.origin), prop('haskell:scopes', u.scopes), prop('haskell:flags', u.flags && Object.keys(u.flags).length ? u.flags : null), prop('haskell:boot', u.boot ? true : null), prop('haskell:component', u.component && u.component.kind), prop('haskell:originEvidence', u.originEvidence)),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const dep = new Map();
|
|
101
|
+
for (const e of graph.edges || []) {
|
|
102
|
+
const from = refOf.get(e.from); const to = refOf.get(e.to);
|
|
103
|
+
if (!from || !to || from === to) continue;
|
|
104
|
+
if (!dep.has(from)) dep.set(from, new Map());
|
|
105
|
+
const toUnit = (graph.units || []).find((x) => x.id === e.to);
|
|
106
|
+
dep.get(from).set(to, (toUnit && toUnit.scopes && toUnit.scopes.includes('runtime')) ? 'runtime' : ((toUnit && toUnit.scopes && toUnit.scopes.includes('test')) ? 'test' : 'build'));
|
|
107
|
+
}
|
|
108
|
+
for (const [ref, m] of [...dep.entries()].sort((a, b) => (a[0] < b[0] ? -1 : 1))) out.dependencies.push({ ref, dependsOn: [...m.keys()].sort(), kinds: Object.fromEntries(m) });
|
|
109
|
+
out.compositions.push({ aggregate: graph.closure && graph.closure.complete ? 'complete' : 'incomplete', dependencies: [...new Set([...refOf.values()])].sort() });
|
|
110
|
+
} else {
|
|
111
|
+
// Declared inventory only: names and (where a freeze or exact pin states one) versions. Not a closure.
|
|
112
|
+
const seen = new Map();
|
|
113
|
+
for (const d of declared) {
|
|
114
|
+
const key = `${d.name}@${d.version || ''}`;
|
|
115
|
+
const ref = d.version ? `hackage:${d.name}@${d.version}` : `hackage:${d.name}`;
|
|
116
|
+
if (seen.has(key)) { const c = seen.get(key); if (!c.scopes.includes(d.scope)) c.scopes.push(d.scope); c.cdxScope = cdxScope(c.scopes); continue; }
|
|
117
|
+
const c = {
|
|
118
|
+
ecosystem: 'hackage', name: d.name, version: d.version || null, purl: (0,_haskell_sca_js__WEBPACK_IMPORTED_MODULE_1__/* .hackagePurl */ .Gf)(d.name, d.version || null), bomRef: ref, filePath: d.manifest || undefined, scopes: [d.scope], cdxScope: cdxScope([d.scope]), identityKey: `hackage:${d.name}`,
|
|
119
|
+
properties: props(prop('haskell:declaredRange', d.declaredRange), prop('haskell:resolution', d.resolution), prop('haskell:boot', d.ghcComponent ? true : null), prop('haskell:manifest', d.manifest)),
|
|
120
|
+
};
|
|
121
|
+
seen.set(key, c); out.components.push(c);
|
|
122
|
+
}
|
|
123
|
+
if (declared.length) {
|
|
124
|
+
out.dependencies.push({ ref: rootRef, dependsOn: out.components.map((c) => c.bomRef).sort(), kinds: Object.fromEntries(out.components.map((c) => [c.bomRef, (c.scopes || []).includes('runtime') ? 'runtime' : 'build'])) });
|
|
125
|
+
out.compositions.push({ aggregate: 'incomplete', dependencies: [rootRef] });
|
|
126
|
+
}
|
|
127
|
+
out.gaps.push({ kind: 'declared-inventory-only', source: 'haskell', detail: 'no fresh plan or Stack export: components are the declared dependencies, versions only where a freeze or an exact pin states one; transitive packages are not listed' });
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── Nix ─────────────────────────────────────────────────────────────────────
|
|
133
|
+
function nixInputBom(files) {
|
|
134
|
+
const out = { components: [], dependencies: [], compositions: [], properties: [], gaps: [], root: null };
|
|
135
|
+
const inv = (0,_nix_inventory_js__WEBPACK_IMPORTED_MODULE_3__/* .analyzeNixInputs */ .Tw)({ files });
|
|
136
|
+
out.gaps.push(...inv.gaps.map((g) => ({ ...g, source: 'nix' })));
|
|
137
|
+
const refOf = new Map();
|
|
138
|
+
for (const f of inv.flakes) {
|
|
139
|
+
const nodes = (f.graph && f.graph.nodes) || [];
|
|
140
|
+
for (const n of nodes) {
|
|
141
|
+
if (n.root || !n.reachable) continue;
|
|
142
|
+
const key = `${f.file}:${n.key}`;
|
|
143
|
+
const ref = `nix-input:${key}`;
|
|
144
|
+
refOf.set(`${f.file}|${n.key}`, ref);
|
|
145
|
+
const lk = n.locked || null;
|
|
146
|
+
const rev = lk && (lk.rev || null);
|
|
147
|
+
let purl = null; let url = null;
|
|
148
|
+
if (lk && lk.type) {
|
|
149
|
+
purl = purlFromGitHost(lk.type, lk.owner, lk.repo, rev);
|
|
150
|
+
if (!purl && lk.url) purl = genericPurl(n.name, rev, lk.type === 'tarball' ? { download_url: lk.url } : { vcs_url: lk.url });
|
|
151
|
+
if (lk.type === 'github') url = `https://github.com/${lk.owner}/${lk.repo}`;
|
|
152
|
+
else if (lk.url) url = lk.url;
|
|
153
|
+
}
|
|
154
|
+
if (!purl) purl = genericPurl(n.name, null);
|
|
155
|
+
out.components.push({
|
|
156
|
+
ecosystem: 'nix', name: n.name, version: rev, purl, bomRef: ref, filePath: f.lock && f.lock.file ? f.lock.file : f.file, cdxScope: 'required', scopes: ['source'], identityKey: `nix-input:${f.file}:${n.key}`,
|
|
157
|
+
...(url ? { externalReferences: [{ type: 'vcs', url }] } : {}),
|
|
158
|
+
properties: props(prop('nix:class', 'flake-input'), prop('nix:lockNode', n.key), prop('nix:flakeFile', f.file), prop('nix:fetchType', lk && lk.type), prop('nix:narHash', lk && lk.narHash), prop('nix:locked', lk ? true : false), prop('nix:purlBasis', purl.startsWith('pkg:generic') ? 'generic-fallback' : 'validated-upstream-host')),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const edges = (f.graph && f.graph.edges) || [];
|
|
162
|
+
const by = new Map();
|
|
163
|
+
for (const e of edges) { const a = refOf.get(`${f.file}|${e.from}`); const b = e.to ? refOf.get(`${f.file}|${e.to}`) : null; if (a && b && a !== b) { if (!by.has(a)) by.set(a, new Set()); by.get(a).add(b); } }
|
|
164
|
+
for (const [ref, set] of [...by.entries()].sort((x, y) => (x[0] < y[0] ? -1 : 1))) out.dependencies.push({ ref, dependsOn: [...set].sort(), kinds: Object.fromEntries([...set].map((s) => [s, 'runtime'])) });
|
|
165
|
+
}
|
|
166
|
+
if (out.components.length) out.compositions.push({ aggregate: 'incomplete', dependencies: out.components.map((c) => c.bomRef).sort() });
|
|
167
|
+
out.gaps.push({ kind: 'inputs-are-not-a-closure', source: 'nix', detail: 'flake.lock lists source inputs; it is not the build or runtime closure, so the Nix inputs are reported as incomplete' });
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function nixClosureBom(closure) {
|
|
172
|
+
const out = { components: [], dependencies: [], compositions: [], properties: [], gaps: [], root: null };
|
|
173
|
+
if (!closure || closure.status !== 'ok') { if (closure) out.gaps.push({ kind: 'closure-not-usable', source: 'nix', detail: `closure import status ${closure.status}` }); return out; }
|
|
174
|
+
const prov = (closure.provenance || [])[0] || {};
|
|
175
|
+
const target = prov.target || {};
|
|
176
|
+
out.properties.push(prop('nix:target.system', target.system), prop('nix:target.installable', target.installable), prop('nix:toolVersion', prov.toolVersion), prop('nix:exactRuntimeClosure', closure.claims && closure.claims.exactRuntimeClosure), prop('nix:exactBuildGraph', closure.claims && closure.claims.exactBuildGraph));
|
|
177
|
+
const refOf = new Map();
|
|
178
|
+
for (const n of closure.nodes) {
|
|
179
|
+
if (n.kind !== 'output' && n.kind !== 'source') continue;
|
|
180
|
+
if (!n.pname && !n.storeName) continue;
|
|
181
|
+
const name = n.pname || n.storeName;
|
|
182
|
+
const ref = `nix-store:${n.hash}-${n.storeName}`;
|
|
183
|
+
refOf.set(n.id, ref);
|
|
184
|
+
const urls = (n.upstream && n.upstream.urls) || [];
|
|
185
|
+
let purl = null; let basis = 'generic-fallback';
|
|
186
|
+
for (const u of urls) { const p = purlFromUpstreamUrl(u, n.version); if (p) { purl = p; basis = 'validated-upstream-host'; break; } }
|
|
187
|
+
if (!purl) purl = genericPurl(name, n.version || null);
|
|
188
|
+
const scopes = n.scopes || [];
|
|
189
|
+
out.components.push({
|
|
190
|
+
ecosystem: 'nix', name, version: n.version || null, purl, bomRef: ref, cdxScope: cdxScope(scopes), scopes,
|
|
191
|
+
identityKey: `nix:${name}:${n.system || ''}:${n.outputName || 'out'}${n.sameNameDifferentBuild ? `:${n.hash}` : ''}`,
|
|
192
|
+
properties: props(prop('nix:class', n.kind === 'source' ? 'source' : 'store-output'), prop('nix:storePath', n.id), prop('nix:system', n.system), prop('nix:output', n.outputName), prop('nix:scopes', scopes), prop('nix:versionAuthority', n.versionAuthority), prop('nix:identity', n.identity), prop('nix:purlBasis', basis), prop('nix:sameNameDifferentBuild', n.sameNameDifferentBuild ? true : null), prop('nix:patches', (n.patches || []).map((p) => p.name)), prop('nix:installable', target.installable)),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const by = new Map();
|
|
196
|
+
for (const e of closure.edges) {
|
|
197
|
+
if (e.kind !== 'reference') continue;
|
|
198
|
+
const a = refOf.get(e.from); const b = refOf.get(e.to);
|
|
199
|
+
if (!a || !b || a === b) continue;
|
|
200
|
+
if (!by.has(a)) by.set(a, new Set()); by.get(a).add(b);
|
|
201
|
+
}
|
|
202
|
+
for (const [ref, set] of [...by.entries()].sort((x, y) => (x[0] < y[0] ? -1 : 1))) out.dependencies.push({ ref, dependsOn: [...set].sort(), kinds: Object.fromEntries([...set].map((s) => [s, 'runtime'])) });
|
|
203
|
+
const runtimeRefs = closure.nodes.filter((n) => n.kind === 'output' && (n.scopes || []).includes('runtime')).map((n) => refOf.get(n.id)).filter(Boolean).sort();
|
|
204
|
+
if (runtimeRefs.length) out.compositions.push({ aggregate: closure.claims && closure.claims.exactRuntimeClosure ? 'complete' : 'incomplete', dependencies: runtimeRefs });
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param {Record<string,string>} files rel path -> content (sources and manifests)
|
|
210
|
+
* @param {{closure?: object, resolved?: object, plan?: object, stackExport?: object, expected?: object, rejectStale?: boolean}} [opts]
|
|
211
|
+
* `closure` is an importNixClosure() result (an explicit, provenance-carrying export); nothing is run or fetched.
|
|
212
|
+
*/
|
|
213
|
+
function languageBom(files, opts = {}) {
|
|
214
|
+
const parts = [haskellBom(files || {}, opts), nixInputBom(files || {}), nixClosureBom(opts.closure)];
|
|
215
|
+
const merged = { version: LANGUAGE_BOM_VERSION, components: [], dependencies: [], compositions: [], properties: [], gaps: [], root: null };
|
|
216
|
+
const byRef = new Map();
|
|
217
|
+
for (const p of parts) {
|
|
218
|
+
for (const c of p.components) { if (!byRef.has(c.bomRef)) { byRef.set(c.bomRef, c); merged.components.push(c); } }
|
|
219
|
+
for (const d of p.dependencies) merged.dependencies.push(d);
|
|
220
|
+
merged.compositions.push(...p.compositions);
|
|
221
|
+
merged.properties.push(...p.properties);
|
|
222
|
+
merged.gaps.push(...p.gaps);
|
|
223
|
+
if (!merged.root && p.root) merged.root = p.root;
|
|
224
|
+
}
|
|
225
|
+
// a dependency edge may only name refs that exist (the root ref is allowed)
|
|
226
|
+
const known = new Set([...byRef.keys(), ...(merged.root ? [merged.root['bom-ref']] : [])]);
|
|
227
|
+
merged.dependencies = merged.dependencies.map((d) => ({ ...d, dependsOn: d.dependsOn.filter((r) => known.has(r)) })).filter((d) => known.has(d.ref));
|
|
228
|
+
merged.components.sort((a, b) => (a.bomRef < b.bomRef ? -1 : 1));
|
|
229
|
+
return merged;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The language half of a Pipeline Bill of Materials: what is fetched and how it is pinned before anything is built.
|
|
234
|
+
* Static facts from the manifests and lock files only; nothing is fetched or evaluated.
|
|
235
|
+
*/
|
|
236
|
+
function languagePbom(files) {
|
|
237
|
+
const out = { version: LANGUAGE_BOM_VERSION, haskell: null, nix: null };
|
|
238
|
+
const { manifests } = (0,_haskell_supply_js__WEBPACK_IMPORTED_MODULE_0__/* .hackageComponents */ .vG)(files || {});
|
|
239
|
+
if (manifests) {
|
|
240
|
+
out.haskell = {
|
|
241
|
+
packages: (manifests.packages || []).map((p) => ({ name: p.name, version: p.version || null, buildType: p.buildType || null, file: p.file })),
|
|
242
|
+
customSetup: (manifests.packages || []).some((p) => String(p.buildType || '').toLowerCase() === 'custom'),
|
|
243
|
+
sourceRepositories: (manifests.sourceRepositories || []).map((r) => ({ location: r.location, tag: r.tag || null, pinned: /^[0-9a-f]{40}$/i.test(r.tag || ''), file: r.file, line: r.line })),
|
|
244
|
+
compiler: (manifests.projects || []).map((p) => p.withCompiler).find(Boolean) || null,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const inv = (0,_nix_inventory_js__WEBPACK_IMPORTED_MODULE_3__/* .analyzeNixInputs */ .Tw)({ files: files || {} });
|
|
248
|
+
if (inv.flakes.length || inv.legacy.length) {
|
|
249
|
+
out.nix = {
|
|
250
|
+
inputs: inv.flakes.flatMap((f) => (f.declared || []).map((d) => ({ flake: f.file, name: d.name, url: d.url || null, status: d.status, fetchType: d.lockedFetch ? d.lockedFetch.type : null, rev: d.lockedFetch ? d.lockedFetch.rev || null : null, narHash: d.lockedFetch ? d.lockedFetch.narHash || null : null, locked: !!d.locked }))),
|
|
251
|
+
legacy: inv.legacy.map((l) => ({ kind: l.kind, file: l.file, line: l.line || null, resolution: l.resolution || null })),
|
|
252
|
+
closure: { complete: false, reason: 'inputs and fetchers only; a build or runtime closure needs an explicit export' },
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
/***/ })
|
|
260
|
+
|
|
261
|
+
};
|