@clear-capabilities/agentic-security-scanner 0.172.1 → 0.173.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/bin/agentic-security.js +37 -0
  3. package/dist/119.index.js +197 -0
  4. package/dist/128.index.js +129 -0
  5. package/dist/1293.index.js +5689 -0
  6. package/dist/1351.index.js +133 -0
  7. package/dist/1387.index.js +271 -0
  8. package/dist/1437.index.js +23 -0
  9. package/dist/1445.index.js +110 -0
  10. package/dist/1505.index.js +151 -0
  11. package/dist/1557.index.js +293 -0
  12. package/dist/1621.index.js +311 -0
  13. package/dist/1751.index.js +160 -0
  14. package/dist/1765.index.js +727 -0
  15. package/dist/1861.index.js +690 -0
  16. package/dist/1898.index.js +116 -0
  17. package/dist/1906.index.js +433 -0
  18. package/dist/1920.index.js +1 -291
  19. package/dist/208.index.js +1631 -0
  20. package/dist/2199.index.js +0 -239
  21. package/dist/2223.index.js +159 -0
  22. package/dist/2530.index.js +152 -0
  23. package/dist/2565.index.js +116 -0
  24. package/dist/2574.index.js +127 -0
  25. package/dist/2620.index.js +345 -0
  26. package/dist/2773.index.js +98 -0
  27. package/dist/2775.index.js +32 -0
  28. package/dist/2966.index.js +171 -0
  29. package/dist/2972.index.js +87 -0
  30. package/dist/2991.index.js +131 -0
  31. package/dist/3063.index.js +833 -0
  32. package/dist/3075.index.js +227 -0
  33. package/dist/3121.index.js +297 -0
  34. package/dist/3314.index.js +263 -0
  35. package/dist/3480.index.js +1822 -0
  36. package/dist/3481.index.js +24 -0
  37. package/dist/3506.index.js +268 -0
  38. package/dist/3533.index.js +591 -0
  39. package/dist/3653.index.js +450 -0
  40. package/dist/3822.index.js +131 -0
  41. package/dist/3868.index.js +292 -0
  42. package/dist/3917.index.js +152 -0
  43. package/dist/3918.index.js +192 -0
  44. package/dist/4090.index.js +288 -0
  45. package/dist/4121.index.js +1190 -0
  46. package/dist/4142.index.js +0 -239
  47. package/dist/4336.index.js +436 -0
  48. package/dist/4387.index.js +532 -0
  49. package/dist/4413.index.js +302 -0
  50. package/dist/4476.index.js +4510 -0
  51. package/dist/4491.index.js +133 -0
  52. package/dist/4538.index.js +185 -0
  53. package/dist/4684.index.js +319 -0
  54. package/dist/4740.index.js +165 -0
  55. package/dist/4780.index.js +209 -0
  56. package/dist/4802.index.js +225 -0
  57. package/dist/4812.index.js +379 -0
  58. package/dist/4835.index.js +126 -0
  59. package/dist/494.index.js +226 -0
  60. package/dist/4991.index.js +549 -0
  61. package/dist/5199.index.js +305 -0
  62. package/dist/5216.index.js +109 -0
  63. package/dist/5439.index.js +301 -0
  64. package/dist/5443.index.js +97 -0
  65. package/dist/5496.index.js +377 -0
  66. package/dist/550.index.js +2 -1
  67. package/dist/5801.index.js +588 -0
  68. package/dist/5858.index.js +223 -0
  69. package/dist/5933.index.js +143 -0
  70. package/dist/601.index.js +23 -0
  71. package/dist/6173.index.js +273 -0
  72. package/dist/6291.index.js +1871 -0
  73. package/dist/6312.index.js +666 -0
  74. package/dist/634.index.js +16182 -0
  75. package/dist/644.index.js +87 -0
  76. package/dist/6508.index.js +283 -0
  77. package/dist/6560.index.js +645 -0
  78. package/dist/6576.index.js +422 -0
  79. package/dist/6579.index.js +271 -0
  80. package/dist/6745.index.js +305 -0
  81. package/dist/6818.index.js +21642 -0
  82. package/dist/6972.index.js +167 -0
  83. package/dist/6995.index.js +163 -0
  84. package/dist/7249.index.js +191 -0
  85. package/dist/7372.index.js +1197 -0
  86. package/dist/7546.index.js +541 -0
  87. package/dist/76.index.js +448 -0
  88. package/dist/7879.index.js +163 -0
  89. package/dist/8025.index.js +130 -0
  90. package/dist/8054.index.js +248 -0
  91. package/dist/8097.index.js +201 -0
  92. package/dist/8126.index.js +191 -0
  93. package/dist/8178.index.js +78 -0
  94. package/dist/818.index.js +212 -0
  95. package/dist/8382.index.js +93 -0
  96. package/dist/8408.index.js +261 -0
  97. package/dist/8540.index.js +174 -0
  98. package/dist/8635.index.js +802 -0
  99. package/dist/8688.index.js +410 -0
  100. package/dist/8699.index.js +779 -0
  101. package/dist/8891.index.js +457 -0
  102. package/dist/8916.index.js +885 -0
  103. package/dist/9146.index.js +170 -0
  104. package/dist/9168.index.js +841 -0
  105. package/dist/9292.index.js +369 -0
  106. package/dist/9382.index.js +135 -11
  107. package/dist/9422.index.js +866 -0
  108. package/dist/9440.index.js +453 -0
  109. package/dist/9479.index.js +152 -0
  110. package/dist/9481.index.js +220 -0
  111. package/dist/9539.index.js +100 -0
  112. package/dist/9547.index.js +103 -0
  113. package/dist/9560.index.js +41 -61
  114. package/dist/9692.index.js +90 -0
  115. package/dist/9800.index.js +213 -1
  116. package/dist/9899.index.js +138 -0
  117. package/dist/9981.index.js +206 -0
  118. package/dist/999.index.js +117 -0
  119. package/dist/agentic-security.mjs +27 -27
  120. package/dist/agentic-security.mjs.sha256 +1 -1
  121. package/package.json +28 -3
  122. package/src/context-intelligence/candidates.js +2 -1
  123. package/src/context-intelligence/capabilities.js +2 -1
  124. package/src/context-intelligence/cli.js +2 -0
  125. package/src/context-intelligence/config.js +3 -1
  126. package/src/context-intelligence/controls.js +49 -1
  127. package/src/context-intelligence/counterexample.js +200 -0
  128. package/src/context-intelligence/cross-route.js +111 -0
  129. package/src/context-intelligence/findings.js +166 -0
  130. package/src/context-intelligence/invariants.js +197 -0
  131. package/src/context-intelligence/queue.js +6 -1
  132. package/src/context-intelligence/reachability.js +2 -2
  133. package/src/context-intelligence/review.js +68 -4
  134. package/src/dataflow/access-paths.js +11 -0
  135. package/src/dataflow/analysis-limits.js +66 -0
  136. package/src/dataflow/engine.js +272 -20
  137. package/src/dataflow/service-edges.js +174 -0
  138. package/src/dataflow/summaries.js +7 -1
  139. package/src/discovery/judge.js +2 -1
  140. package/src/engine.js +82 -14
  141. package/src/fix/apply-fix-service.js +55 -8
  142. package/src/fix/batch-journal.js +84 -0
  143. package/src/fix/exploit-replay.js +51 -0
  144. package/src/fix/footprint.js +65 -0
  145. package/src/fix/patch-policy.js +49 -0
  146. package/src/fix/secret-persistence.js +39 -0
  147. package/src/fix/test-commands.js +84 -0
  148. package/src/fix/verification-adapters.js +97 -0
  149. package/src/fix/verification-record.js +243 -0
  150. package/src/fix/verify-patch.js +89 -0
  151. package/src/ir/CLAUDE.md +5 -0
  152. package/src/ir/callgraph.js +23 -1
  153. package/src/ir/module-resolver.js +315 -0
  154. package/src/ir/parser-js.js +21 -2
  155. package/src/ir/semantic-graph.js +245 -0
  156. package/src/lineage/build-attested-correlation.js +63 -0
  157. package/src/lineage/deployment-identity.js +76 -0
  158. package/src/mcp/tools.js +13 -34
  159. package/src/pipeline/canonical-output.js +64 -0
  160. package/src/pipeline/detector-runner.js +7 -0
  161. package/src/pipeline/scan-health.js +14 -0
  162. package/src/pipeline/supervised-analyzers.js +125 -0
  163. package/src/pipeline/supervised-child.js +17 -0
  164. package/src/pipeline/telemetry.js +79 -0
  165. package/src/posture/accuracy-scorecard.js +98 -9
  166. package/src/posture/artifact-registry.js +5 -0
  167. package/src/posture/assurance/evidence-registry.js +215 -0
  168. package/src/posture/assurance/release-package-verify.js +135 -0
  169. package/src/posture/assurance/release-package.js +118 -0
  170. package/src/posture/assurance/release-verdict.js +94 -0
  171. package/src/posture/assurance/waiver.js +200 -0
  172. package/src/posture/compliance-truth.js +100 -0
  173. package/src/posture/evaluation/scan-child.js +3 -1
  174. package/src/posture/evaluation/why-missed.js +3 -1
  175. package/src/posture/exploitability-evidence.js +141 -0
  176. package/src/posture/fix-verify.js +4 -2
  177. package/src/posture/fleet/exposure-ranking.js +115 -0
  178. package/src/posture/fleet/service-chain-graph.js +150 -0
  179. package/src/posture/graph-reconciliation.js +154 -0
  180. package/src/posture/provenance/coverage.js +103 -0
  181. package/src/posture/provenance/git-evidence.js +84 -0
  182. package/src/posture/provenance/origin-resolver.js +7 -1
  183. package/src/posture/provenance/predicate-replay.js +14 -4
  184. package/src/posture/runtime-correlation.js +61 -15
  185. package/src/posture/runtime-explanation.js +116 -0
  186. package/src/posture/runtime-trace-ingest.js +194 -0
  187. package/src/posture/scan-checkpoint.js +38 -10
  188. package/src/posture/waf-ingest.js +7 -2
  189. package/src/report/index.js +4 -0
  190. package/src/report/oscal.js +38 -0
  191. package/src/report/perspectives.js +150 -0
  192. package/src/sast/_comment-strip.js +8 -1
  193. package/src/sast/ruby.js +26 -0
  194. package/src/util/string-memo.js +38 -0
@@ -0,0 +1,133 @@
1
+ export const id = 4491;
2
+ export const ids = [4491];
3
+ export const modules = {
4
+
5
+ /***/ 54491:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ extractVulnFunctionsViaLLM: () => (/* binding */ extractVulnFunctionsViaLLM)
10
+ /* harmony export */ });
11
+ /* unused harmony export isLlmScaEnabled */
12
+ /* harmony import */ var node_crypto__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(77598);
13
+ /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(73024);
14
+ /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
15
+ /* harmony import */ var _egress_policy_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(15369);
16
+ // LLM-assisted vulnerable function extraction for SCA findings.
17
+ //
18
+ // For CVEs without OSV ecosystem_specific data or GHSA fix commits,
19
+ // uses an LLM to extract vulnerable function names from the CVE description.
20
+ //
21
+ // Gated behind AGENTIC_SECURITY_LLM_SCA=1 (opt-in).
22
+ // Uses the same LLM endpoint config as the SAST validator.
23
+
24
+
25
+
26
+
27
+
28
+
29
+ const CACHE_DIR = process.env.XDG_CONFIG_HOME
30
+ ? node_path__WEBPACK_IMPORTED_MODULE_2__.join(process.env.XDG_CONFIG_HOME, 'agentic-security', 'llm-sca-cache')
31
+ : node_path__WEBPACK_IMPORTED_MODULE_2__.join(process.env.HOME || '/tmp', '.config', 'agentic-security', 'llm-sca-cache');
32
+
33
+ function _cacheKey(osvId) {
34
+ return node_crypto__WEBPACK_IMPORTED_MODULE_0__.createHash('sha256').update(`sca-fn:${osvId}`).digest('hex').slice(0, 16);
35
+ }
36
+
37
+ function _readCache(osvId) {
38
+ try {
39
+ const fp = node_path__WEBPACK_IMPORTED_MODULE_2__.join(CACHE_DIR, _cacheKey(osvId) + '.json');
40
+ return JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_1__.readFileSync(fp, 'utf8'));
41
+ } catch { return null; }
42
+ }
43
+
44
+ function _writeCache(osvId, data) {
45
+ try {
46
+ node_fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(CACHE_DIR, { recursive: true });
47
+ node_fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(node_path__WEBPACK_IMPORTED_MODULE_2__.join(CACHE_DIR, _cacheKey(osvId) + '.json'), JSON.stringify(data));
48
+ } catch { /* cache write failure is non-fatal */ }
49
+ }
50
+
51
+ function isLlmScaEnabled() {
52
+ return process.env.AGENTIC_SECURITY_LLM_SCA === '1';
53
+ }
54
+
55
+ function _endpointConfig() {
56
+ const endpoint = process.env.AGENTIC_SECURITY_LLM_ENDPOINT;
57
+ const apiKey = process.env.AGENTIC_SECURITY_LLM_API_KEY;
58
+ const model = process.env.AGENTIC_SECURITY_LLM_MODEL || 'unknown';
59
+ return endpoint ? { endpoint, apiKey, model } : null;
60
+ }
61
+
62
+ async function _askLlm(prompt, config) {
63
+ const headers = { 'Content-Type': 'application/json' };
64
+ if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`;
65
+ const resp = await fetch(config.endpoint, {
66
+ method: 'POST',
67
+ headers,
68
+ body: JSON.stringify({ prompt, model: config.model }),
69
+ signal: AbortSignal.timeout(15000),
70
+ });
71
+ if (!resp.ok) return null;
72
+ const text = await resp.text();
73
+ const jsonMatch = text.match(/\{[^{}]*"functions"\s*:\s*\[[^\]]*\][^{}]*\}/);
74
+ if (!jsonMatch) return null;
75
+ try { return JSON.parse(jsonMatch[0]); } catch { return null; }
76
+ }
77
+
78
+ async function extractVulnFunctionsViaLLM(supplyChain, opts = {}) {
79
+ if (!isLlmScaEnabled()) return [];
80
+ const config = _endpointConfig();
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 [];
87
+
88
+ const enriched = [];
89
+ const candidates = (supplyChain || []).filter(sc =>
90
+ sc.type === 'vulnerable_dep' &&
91
+ (!sc.osvVulnFunctions || !sc.osvVulnFunctions.length) &&
92
+ sc.noKnownCallSite &&
93
+ sc.description
94
+ );
95
+
96
+ const BATCH_LIMIT = 20;
97
+ for (const sc of candidates.slice(0, BATCH_LIMIT)) {
98
+ const cached = _readCache(sc.osvId);
99
+ if (cached) {
100
+ if (cached.functions && cached.functions.length) {
101
+ sc.osvVulnFunctions = cached.functions;
102
+ sc._llmFunctionExtracted = true;
103
+ enriched.push(sc);
104
+ }
105
+ continue;
106
+ }
107
+
108
+ const prompt = `Given security advisory ${sc.osvId || ''} (${sc.cveAliases?.[0] || ''}) affecting npm package "${sc.name}" version ${sc.version}:\n\nDescription: ${sc.description.slice(0, 500)}\n\nWhat specific exported function(s) in this package are vulnerable? Return ONLY a JSON object: { "functions": ["functionName1", "functionName2"] }\n\nIf you cannot determine the specific functions, return: { "functions": [] }`;
109
+
110
+ try {
111
+ const result = await _askLlm(prompt, config);
112
+ if (result && Array.isArray(result.functions)) {
113
+ const fns = result.functions.filter(f => typeof f === 'string' && f.length > 0 && f.length < 100);
114
+ _writeCache(sc.osvId, { functions: fns, model: config.model, extractedAt: new Date().toISOString() });
115
+ if (fns.length) {
116
+ sc.osvVulnFunctions = fns;
117
+ sc._llmFunctionExtracted = true;
118
+ enriched.push(sc);
119
+ }
120
+ } else {
121
+ _writeCache(sc.osvId, { functions: [], model: config.model, extractedAt: new Date().toISOString() });
122
+ }
123
+ } catch {
124
+ // LLM call failure — skip, don't cache (may be transient)
125
+ }
126
+ }
127
+ return enriched;
128
+ }
129
+
130
+
131
+ /***/ })
132
+
133
+ };
@@ -0,0 +1,185 @@
1
+ export const id = 4538;
2
+ export const ids = [4538];
3
+ export const modules = {
4
+
5
+ /***/ 74538:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ proposeGovernanceEdit: () => (/* binding */ proposeGovernanceEdit)
10
+ /* harmony export */ });
11
+ /* harmony import */ var _recipient_registry_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(16288);
12
+ // governance-edit.js — M5 deliverable #5 (PRD line 1324's 5-part write
13
+ // contract: preview, validation, backup/version guard, confirmation,
14
+ // audit event). A CLI-only workflow for proposing a validated,
15
+ // reviewable edit to recipient-profiles.json — the one governance
16
+ // config file this codebase already has real, tested per-entry
17
+ // validation for (isValidRecipientConfigEntry, recipient-registry.js).
18
+ //
19
+ // Deliberately narrower than the PRD's own richer "interactive review/
20
+ // approve UI" vision — the HTTP-server-side interactive write surface
21
+ // (new routes, CSRF protection, a write-authorization mechanism beyond
22
+ // the existing read-only session token) is real, separately-scoped
23
+ // future work, not attempted here. See this sub-project's own scoping
24
+ // doc for the full reasoning: no PRD acceptance criterion gates this
25
+ // deliverable at all, and every M4/M5 deliverable this session has
26
+ // shipped has been CLI-first with zero UI/HTTP-write work.
27
+ //
28
+ // This module is pure — no file I/O, no fs access. The CLI layer
29
+ // (bin/agentic-security.js's cmdGovernancePropose) owns reading the
30
+ // current file, writing the backup, writing the new content, and
31
+ // calling auditCall.
32
+
33
+
34
+
35
+ // Validates the top-level `{recipients: {...}}` container shape — used
36
+ // for BOTH the current on-disk config and the --patch file. A patch is
37
+ // user input the command's whole job is to validate, so it gets no
38
+ // tolerant degradation (fixes I2/I3: previously a patch missing
39
+ // `recipients` entirely, or with `recipients` as an array, degraded
40
+ // silently to an empty no-op write). The CURRENT config gets the same
41
+ // check for a different reason: if its top-level shape is unrecognized
42
+ // (e.g. a typo'd `Recipients` key), the tool must refuse rather than
43
+ // silently treating "no recognizable data" as "start from empty" and
44
+ // overwriting whatever WAS there (this was B1's second live repro —
45
+ // the whole file being replaced under a misspelled key).
46
+ function _validateContainerShape(recipients, label) {
47
+ if (recipients === undefined) {
48
+ return [{ key: '(top-level)', message: `${label} is missing a "recipients" object` }];
49
+ }
50
+ if (recipients === null || typeof recipients !== 'object' || Array.isArray(recipients)) {
51
+ return [{ key: '(top-level)', message: `${label}'s "recipients" must be a plain object, not ${Array.isArray(recipients) ? 'an array' : recipients === null ? 'null' : typeof recipients}` }];
52
+ }
53
+ const errors = [];
54
+ for (const key of Object.keys(recipients)) {
55
+ if (key.length === 0) errors.push({ key, message: `${label} contains an empty-string recipient key, which is never valid` });
56
+ if (key === '__proto__') errors.push({ key, message: `${label} contains a "__proto__" recipient key, which is never valid` });
57
+ }
58
+ return errors;
59
+ }
60
+
61
+ function _validateEntries(patchRecipients) {
62
+ const errors = [];
63
+ for (const [key, entry] of Object.entries(patchRecipients)) {
64
+ if (entry === null) continue; // explicit deletion marker, not a config entry
65
+ if (!(0,_recipient_registry_js__WEBPACK_IMPORTED_MODULE_0__/* .isValidRecipientConfigEntry */ .cw)(entry)) {
66
+ errors.push({ key, message: `recipient "${key}" is not a valid recipient-profile-shaped config entry` });
67
+ }
68
+ }
69
+ return errors;
70
+ }
71
+
72
+ // RFC-7396-style merge patch at the recipient-key level: a non-null
73
+ // patch value REPLACES that key's entire entry; a null value DELETES
74
+ // it; a key the patch never mentions is left untouched. This is the
75
+ // fix for a real data-loss bug (found by this task's own review,
76
+ // live-reproduced): the prior design treated `patch.recipients` as
77
+ // the ENTIRE new recipients object, so omitting a key silently
78
+ // deleted it — a reasonable operator adding one vendor had no reason
79
+ // to expect every other vendor's compliance facts to vanish.
80
+ function _mergeRecipients(currentRecipients, patchRecipients) {
81
+ const merged = { ...currentRecipients };
82
+ for (const [key, value] of Object.entries(patchRecipients)) {
83
+ if (value === null) delete merged[key];
84
+ else merged[key] = value;
85
+ }
86
+ return merged;
87
+ }
88
+
89
+ // Canonical (key-order-independent) JSON serialization for change
90
+ // detection — mirrors ids.js's own _canon precedent. A hand-authored
91
+ // patch file has no reason to preserve the stored config's own key
92
+ // order, so a raw JSON.stringify comparison would spuriously flag a
93
+ // semantically-unchanged recipient as "changed" (found by the task
94
+ // review, reproduced live). Arrays keep their own order (order is
95
+ // semantically meaningful there — e.g. subprocessorChain); only OBJECT
96
+ // key order is normalized.
97
+ function _canonicalize(value) {
98
+ if (Array.isArray(value)) return value.map(_canonicalize);
99
+ if (value && typeof value === 'object') {
100
+ const sorted = {};
101
+ for (const key of Object.keys(value).sort()) sorted[key] = _canonicalize(value[key]);
102
+ return sorted;
103
+ }
104
+ return value;
105
+ }
106
+
107
+ function _diffRecipients(currentRecipients, patchRecipients) {
108
+ const added = [];
109
+ const removed = [];
110
+ const changed = [];
111
+ const currentKeys = new Set(Object.keys(currentRecipients));
112
+ for (const [key, value] of Object.entries(patchRecipients)) {
113
+ if (value === null) {
114
+ if (currentKeys.has(key)) removed.push(key);
115
+ // deleting a key that never existed is a no-op, not reported
116
+ continue;
117
+ }
118
+ if (!currentKeys.has(key)) { added.push(key); continue; }
119
+ if (JSON.stringify(_canonicalize(currentRecipients[key])) !== JSON.stringify(_canonicalize(value))) {
120
+ changed.push({ key, before: currentRecipients[key], after: value });
121
+ }
122
+ }
123
+ added.sort();
124
+ removed.sort();
125
+ changed.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
126
+ return { added, removed, changed };
127
+ }
128
+
129
+ /**
130
+ * Propose a patch to a recipient-profiles.json-shaped config. Pure,
131
+ * never throws, never touches the filesystem. `currentConfig`/`patch`
132
+ * are both expected to be `{recipients: {...}}`-shaped — a malformed
133
+ * top-level shape on EITHER side (missing `recipients`, `recipients`
134
+ * not a plain object, or an empty-string/`__proto__` recipient key) is
135
+ * a validation failure (`valid: false`, `merged: null`), never a
136
+ * silent empty-object fallback. The stored config gets this treatment
137
+ * so an unrecognized shape (e.g. a typo'd top-level key) refuses to
138
+ * write rather than being silently treated as "nothing here yet" and
139
+ * overwritten; the patch gets it because it is user input the command's
140
+ * whole job is to validate.
141
+ *
142
+ * `patch.recipients` is an RFC-7396-style JSON MERGE PATCH against
143
+ * `currentConfig.recipients`, keyed at the recipient level — never a
144
+ * full replacement of the whole object: a key present with a non-null
145
+ * value REPLACES that key's entire entry (never deep-merged within
146
+ * itself); a key present with value `null` DELETES it (the only way to
147
+ * remove a recipient); a key the patch never mentions is left
148
+ * untouched in the merged result. Every OTHER top-level key on the
149
+ * current config (e.g. `$schema`, `version`) is preserved verbatim into
150
+ * `merged` — only `recipients` itself is merged.
151
+ *
152
+ * Returns `{valid, errors, diff, merged}` — `diff`/`merged` are always
153
+ * computed when both container shapes are valid, even when `valid` is
154
+ * false due to a per-entry error, so an operator can see what they
155
+ * attempted before fixing a validation error. `merged` is `null` only
156
+ * when there is no safe merge base to compute (a container-shape
157
+ * failure on either side). `merged`, when non-null, is the full
158
+ * config-shaped result the caller should write — never the raw patch.
159
+ */
160
+ function proposeGovernanceEdit(currentConfig, patch) {
161
+ const currentContainerErrors = _validateContainerShape(currentConfig?.recipients, 'the current config file');
162
+ const patchContainerErrors = _validateContainerShape(patch?.recipients, 'the --patch file');
163
+ if (currentContainerErrors.length || patchContainerErrors.length) {
164
+ return {
165
+ valid: false,
166
+ errors: [...currentContainerErrors, ...patchContainerErrors],
167
+ diff: { added: [], removed: [], changed: [] },
168
+ merged: null,
169
+ };
170
+ }
171
+ const currentRecipients = currentConfig.recipients;
172
+ const patchRecipients = patch.recipients;
173
+ const entryErrors = _validateEntries(patchRecipients);
174
+ const diff = _diffRecipients(currentRecipients, patchRecipients);
175
+ const mergedRecipients = _mergeRecipients(currentRecipients, patchRecipients);
176
+ // Preserve every OTHER top-level key from the current config verbatim
177
+ // (e.g. $schema, version) — only `recipients` itself is merged.
178
+ const merged = { ...(currentConfig && typeof currentConfig === 'object' ? currentConfig : {}), recipients: mergedRecipients };
179
+ return { valid: entryErrors.length === 0, errors: entryErrors, diff, merged };
180
+ }
181
+
182
+
183
+ /***/ })
184
+
185
+ };
@@ -0,0 +1,319 @@
1
+ export const id = 4684;
2
+ export const ids = [4684];
3
+ export const modules = {
4
+
5
+ /***/ 34684:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+
9
+ // EXPORTS
10
+ __webpack_require__.d(__webpack_exports__, {
11
+ analyzeLanguageFindings: () => (/* binding */ analyzeLanguageFindings),
12
+ analyzeLanguageSupplyChain: () => (/* binding */ analyzeLanguageSupplyChain)
13
+ });
14
+
15
+ // UNUSED EXPORTS: nixosEntries
16
+
17
+ // EXTERNAL MODULE: ./src/language/resolved-pass.js + 4 modules
18
+ var resolved_pass = __webpack_require__(30208);
19
+ // EXTERNAL MODULE: ./src/language/nix-build-trust.js
20
+ var nix_build_trust = __webpack_require__(13787);
21
+ // EXTERNAL MODULE: ./src/language/discovery.js
22
+ var discovery = __webpack_require__(41928);
23
+ // EXTERNAL MODULE: ./src/language/haskell-supply.js
24
+ var haskell_supply = __webpack_require__(67052);
25
+ // EXTERNAL MODULE: ./src/language/nix-script-taint.js
26
+ var nix_script_taint = __webpack_require__(89928);
27
+ // EXTERNAL MODULE: ./src/language/nix-secrets.js
28
+ var nix_secrets = __webpack_require__(2245);
29
+ // EXTERNAL MODULE: ./src/language/nix-parser.js
30
+ var nix_parser = __webpack_require__(81985);
31
+ // EXTERNAL MODULE: ./src/language/secrets.js
32
+ var secrets = __webpack_require__(67855);
33
+ // EXTERNAL MODULE: ./src/language/haskell-manifests.js
34
+ var haskell_manifests = __webpack_require__(60377);
35
+ ;// CONCATENATED MODULE: ./src/language/nix-agents.js
36
+ // Nix-launched agent and MCP service configuration (X-003).
37
+ //
38
+ // What this reads is DECLARATION: a systemd service or container whose command line starts an MCP server or
39
+ // an agent CLI, with the filesystem roots, flags, bind address, user and environment the configuration gives
40
+ // it. Every finding here is configuration evidence of a CAPABILITY, never proof that an agent exercised it
41
+ // (`declaredCapability: true`, `exercised: 'not-established'`). Bridges to the application that reads the
42
+ // configuration (an environment variable the Nix service sets and the Haskell program reads) carry explicit
43
+ // provenance on both sides, and only become `executable-linked` when the service launches an executable the
44
+ // project's own manifest builds; a matching variable name alone is a `candidate`.
45
+ //
46
+ // Static only: nothing is evaluated or launched.
47
+
48
+
49
+
50
+
51
+
52
+ const NIX_AGENTS_VERSION = 'nix-agents/1';
53
+ const PH = '';
54
+
55
+ const MCP_RE = /(?:@modelcontextprotocol\/|\bmcp[-_]server|\bserver[-_]mcp|\bmcp[-_]proxy|fastmcp|\buvx\s+mcp|\bnpx\b[^\n]*\bmcp\b|\s--mcp\b)/i;
56
+ const SHELL_MCP_RE = /(?:mcp[-_]server[-_](?:shell|commands|exec)|server-shell|shell-server|mcp-shell|mcp[-_]commands)/i;
57
+ const AGENT_RE = /(?:^|[/\s])(?:claude|aider|goose|codex|gemini|gemini-cli|opencode|crush|openhands|open-interpreter|interpreter|autogpt|cline)(?:\s|$|--)/i;
58
+ const AUTO_APPROVE_RE = /(?:--dangerously-skip-permissions|--dangerously-bypass[\w-]*|--yolo\b|--auto-?approve\b|--approval-mode[= ]yolo|--no-confirm\b|--yes-always\b|--permission-mode[= ]bypass\w*|-y\s+--(?:auto|run))/i;
59
+ const BIND_ALL_RE = /(?:--(?:host|listen|address|bind)[= ]\s*(?:0\.0\.0\.0|::|\[::\]|\*)|-H\s+0\.0\.0\.0|\b(?:HOST|LISTEN_ADDR|BIND(?:_ADDRESS)?)=0\.0\.0\.0)/i;
60
+ const BROAD_ROOTS = [[/(?:^|\s)\/(?:\s|$)/, '/', 'high'], [/(?:^|\s)\/(?:etc|root|nix|boot|proc|sys|usr)(?:\/\S*)?(?:\s|$)/, '/etc, /root, /nix or another system tree', 'high'], [/(?:^|\s)(?:\/home|\/var|\/mnt|\/srv\/?)(?:\s|$)/, '/home, /var or /mnt', 'medium'], [/(?:^|\s)(?:~\/?|\$HOME\/?|%h\/?)(?:\s|$)/, 'the user home directory', 'medium']];
61
+
62
+ const segName = (s) => (s && s.kind === 'static' ? s.name : null);
63
+ const unparen = (n) => { let x = n; while (x && x.type === 'paren') x = x.expr; return x; };
64
+
65
+ /** A string's text with every interpolation replaced by a placeholder (never evaluated). */
66
+ function textOf(n) {
67
+ n = unparen(n);
68
+ if (!n) return null;
69
+ if (n.type === 'string') return n.parts.map((p) => (p.kind === 'text' ? p.value : PH)).join('');
70
+ if (n.type === 'list') return n.items.map((i) => textOf(i)).filter((x) => x !== null).join(' ');
71
+ if (n.type === 'path') return n.literal || null;
72
+ if (n.type === 'binop' && n.op === '+') { const a = textOf(n.left); const b = textOf(n.right); return a !== null && b !== null ? a + b : null; }
73
+ return null;
74
+ }
75
+
76
+ function collectServices(files) {
77
+ const services = new Map();
78
+ const get = (file, name, kind, line) => { const k = `${kind}:${name}`; if (!services.has(k)) services.set(k, { file, name, kind, line, commands: [], env: {}, user: null, dynamicUser: null, openFirewall: null, extra: {} }); return services.get(k); };
79
+ for (const [file, text] of Object.entries(files)) {
80
+ const parse = (0,nix_parser/* parseNix */.Z$)(text, { file });
81
+ if (!parse.ast) continue;
82
+ const stack = [{ n: parse.ast, path: [] }];
83
+ while (stack.length) {
84
+ const { n, path } = stack.pop();
85
+ if (!n || typeof n !== 'object') continue;
86
+ if (n.type === 'attrset') {
87
+ for (const b of n.bindings || []) {
88
+ if (b.kind !== 'attr' || !b.value) continue;
89
+ const key = (b.path || []).map(segName);
90
+ if (key.some((x) => x === null)) { stack.push({ n: b.value, path: [...path, ...key.filter(Boolean)] }); continue; }
91
+ const full = [...path, ...key];
92
+ const line = b.span ? b.span.startLine : null;
93
+ let m = /^systemd\.(?:user\.)?services\.([^.]+)\.(.+)$/.exec(full.join('.'));
94
+ let kind = 'systemd';
95
+ const oci = /^virtualisation\.oci-containers\.containers\.([^.]+)\.(.+)$/.exec(full.join('.'));
96
+ if (oci) { m = oci; kind = 'oci'; }
97
+ if (m) {
98
+ const svc = get(file, m[1], kind, line); const rest = m[2];
99
+ const v = unparen(b.value); const t = textOf(v);
100
+ if (kind === 'systemd') {
101
+ if ((/^(?:script|preStart|postStart)$/.test(rest) || /^serviceConfig\.ExecStart(?:Pre)?$/.test(rest)) && t !== null) svc.commands.push({ text: t, line, file, option: rest });
102
+ if (rest === 'serviceConfig.User' && t !== null) svc.user = { value: t, line };
103
+ if (rest === 'serviceConfig.DynamicUser' && v && v.type === 'ident') svc.dynamicUser = { value: v.name === 'true', line };
104
+ const env = /^environment\.(.+)$/.exec(rest);
105
+ if (env && t !== null) svc.env[env[1]] = { text: t, line, file };
106
+ if (rest === 'environment' && v && v.type === 'attrset') for (const eb of v.bindings) { if (eb.kind !== 'attr') continue; const ek = (eb.path || []).map(segName).filter(Boolean).join('.'); const et = textOf(eb.value); if (et !== null) svc.env[ek] = { text: et, line: eb.span ? eb.span.startLine : line, file }; }
107
+ } else {
108
+ if (/^(?:cmd|entrypoint)$/.test(rest) && t !== null) svc.commands.push({ text: t, line, file, option: rest });
109
+ if (rest === 'image' && t !== null) svc.extra.image = t;
110
+ if (rest === 'user' && t !== null) svc.user = { value: t, line };
111
+ if (rest === 'ports' && t !== null) svc.extra.ports = t;
112
+ if (rest === 'volumes' && t !== null) svc.extra.volumes = t;
113
+ const env = /^environment\.(.+)$/.exec(rest);
114
+ if (env && t !== null) svc.env[env[1]] = { text: t, line, file };
115
+ }
116
+ }
117
+ stack.push({ n: b.value, path: full });
118
+ }
119
+ }
120
+ for (const k of Object.keys(n)) { if (k === 'span') continue; const c = n[k]; if (c && typeof c === 'object' && !(n.type === 'attrset' && k === 'bindings')) { if (Array.isArray(c)) for (const x of c) stack.push({ n: x, path }); else stack.push({ n: c, path }); } }
121
+ }
122
+ }
123
+ return [...services.values()];
124
+ }
125
+
126
+ const finding = (rule, svc, cmd, o) => ({
127
+ id: `${rule}:${svc.file}:${o.line || svc.line}`, file: svc.file, line: o.line || svc.line, parser: 'NIX-AGENT', family: 'agent-config', rule,
128
+ severity: o.severity, cwe: o.cwe, vuln: o.vuln, owaspLlm: 'LLM06',
129
+ description: `${o.desc} This is configuration evidence of a declared capability; nothing shows an agent has exercised it.`,
130
+ remediation: o.fix, language: 'nix', capability: 'iac', analysisKind: 'configuration', evidenceKind: 'config', scope: 'system', confidence: o.confidence ?? 0.7,
131
+ declaredCapability: true, exercised: 'not-established', service: { name: svc.name, kind: svc.kind }, subject: `${svc.kind}:${svc.name}`,
132
+ attrPath: `${svc.kind === 'oci' ? 'virtualisation.oci-containers.containers' : 'systemd.services'}.${svc.name}`,
133
+ chain: [{ file: svc.file, line: o.line || svc.line, label: o.label || o.vuln, kind: 'config' }],
134
+ });
135
+
136
+ /**
137
+ * @param {Record<string,string>} files Nix sources (and, for bridges, Haskell sources and .cabal manifests)
138
+ */
139
+ function analyzeNixAgents(files) {
140
+ const nix = Object.fromEntries(Object.entries(files).filter(([p, t]) => /\.nix$/i.test(p) && typeof t === 'string'));
141
+ const findings = []; const agents = []; const bridges = [];
142
+ const services = collectServices(nix);
143
+ for (const svc of services) {
144
+ const all = svc.commands.map((c) => c.text).join('\n');
145
+ const isMcp = MCP_RE.test(all) || MCP_RE.test(svc.name) || (svc.extra.image && MCP_RE.test(svc.extra.image));
146
+ const isAgent = AGENT_RE.test(all);
147
+ if (!isMcp && !isAgent) continue;
148
+ const cmd = svc.commands.find((c) => MCP_RE.test(c.text) || AGENT_RE.test(c.text)) || svc.commands[0] || { text: '', line: svc.line };
149
+ const entry = { name: svc.name, kind: isMcp ? 'mcp-server' : 'agent', launcher: svc.kind, file: svc.file, line: cmd.line || svc.line, user: svc.user ? svc.user.value : null, dynamicUser: svc.dynamicUser ? svc.dynamicUser.value : null, env: Object.keys(svc.env), declaredCapability: true, exercised: 'not-established' };
150
+ agents.push(entry);
151
+ const text = ` ${all.replace(new RegExp(PH, 'g'), ' ')} `;
152
+ if (SHELL_MCP_RE.test(all)) findings.push(finding('nix-mcp-shell-server', svc, cmd, { severity: 'high', cwe: 'CWE-250', vuln: 'An MCP server that executes shell commands is launched as a service', desc: `${svc.name} starts an MCP server whose purpose is to run commands.`, fix: 'Do not expose a general shell to a model. Offer narrow tools, or run it in a sandbox with no network and a read-only root.', line: cmd.line }));
153
+ if (isMcp) for (const [re, what, sev] of BROAD_ROOTS) if (re.test(text)) { findings.push(finding('nix-mcp-broad-filesystem', svc, cmd, { severity: sev, cwe: 'CWE-732', vuln: 'An MCP/agent service is given a broad filesystem root', desc: `${svc.name} is started with ${what} as a root the server may read or write.`, fix: 'Point the server at the one project directory it needs, and add systemd ReadWritePaths/ProtectSystem so the unit cannot reach more.', line: cmd.line })); break; }
154
+ if (AUTO_APPROVE_RE.test(text)) findings.push(finding('nix-agent-auto-approve', svc, cmd, { severity: 'high', cwe: 'CWE-862', vuln: 'An agent is launched with permission prompts disabled', desc: `${svc.name} starts an agent with an auto-approve / skip-permissions flag, so tool use needs no confirmation.`, fix: 'Remove the flag, or run the agent in an isolated unit with a minimal allow-list.', line: cmd.line }));
155
+ const bindAll = BIND_ALL_RE.test(text) || Object.values(svc.env).some((e) => BIND_ALL_RE.test(` ${e.text} `) || /^0\.0\.0\.0$/.test(String(e.text).trim()) && false);
156
+ const envBind = Object.entries(svc.env).find(([k, e]) => /^(?:HOST|LISTEN_ADDR|BIND|BIND_ADDRESS|MCP_HOST)$/i.test(k) && String(e.text).trim() === '0.0.0.0');
157
+ if (bindAll || envBind) findings.push(finding('nix-agent-network-exposed', svc, cmd, { severity: 'medium', cwe: 'CWE-668', vuln: 'An agent/MCP service listens on every interface', desc: `${svc.name} binds 0.0.0.0, so anything that can reach the host can reach the tool surface.`, fix: 'Bind 127.0.0.1 (or a unix socket) and put authentication in front of anything that must be remote.', line: envBind ? envBind[1].line : cmd.line }));
158
+ const root = svc.kind === 'systemd' && (!svc.user || svc.user.value === 'root') && !(svc.dynamicUser && svc.dynamicUser.value);
159
+ if (root) findings.push(finding('nix-agent-runs-as-root', svc, cmd, { severity: 'medium', cwe: 'CWE-250', vuln: 'An agent/MCP service runs as root', desc: `${svc.name} has no User or DynamicUser, so systemd runs it as root.`, fix: 'Set serviceConfig.DynamicUser = true (or a dedicated User) and grant only the paths it needs.', line: svc.line }));
160
+ entry.environment = Object.entries(svc.env).map(([k, e]) => ({ name: k, line: e.line, valueRecorded: false }));
161
+ }
162
+ // bridges: a Nix service environment variable and the Haskell program that reads it
163
+ const hsFiles = Object.entries(files).filter(([p, t]) => /\.l?hs$/i.test(p) && typeof t === 'string');
164
+ const manifests = Object.entries(files).filter(([p, t]) => /\.cabal$/i.test(p) && typeof t === 'string').map(([path, text]) => ({ path, text }));
165
+ const executables = new Set();
166
+ if (manifests.length) { try { for (const pkg of (0,haskell_manifests/* analyzeHaskellManifests */.JS)(manifests).packages) for (const c of pkg.components || []) if (c.kind === 'executable') executables.add(c.name); } catch { /* no manifest evidence */ } }
167
+ const reads = [];
168
+ for (const [file, text] of hsFiles) {
169
+ const toks = (0,secrets/* tokenizeHaskell */.cH)(text);
170
+ for (let i = 0; i < toks.length; i++) {
171
+ const t = toks[i];
172
+ if (t.k !== 'i' || !/^(?:getEnv|lookupEnv)$/.test(t.v.replace(/^.*\./, ''))) continue;
173
+ let j = i + 1; while (toks[j] && toks[j].k === 'x' && toks[j].v === '(') j++;
174
+ if (toks[j] && toks[j].k === 's') { let line = 1; for (let q = 0; q < t.start; q++) if (text.charCodeAt(q) === 10) line++; reads.push({ file, line, variable: toks[j].v }); }
175
+ }
176
+ }
177
+ for (const svc of services) {
178
+ const exec = svc.commands.map((c) => c.text).join(' ');
179
+ const launched = [...executables].filter((e) => new RegExp(`(?:^|[/\\s])${e.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?:\\s|$)`).test(exec.replace(new RegExp(PH, 'g'), ' ')));
180
+ for (const [variable, e] of Object.entries(svc.env)) {
181
+ for (const r of reads.filter((x) => x.variable === variable)) {
182
+ bridges.push({ kind: 'env-read', variable, status: launched.length ? 'executable-linked' : 'candidate', executable: launched[0] || null,
183
+ nix: { service: svc.name, file: e.file, line: e.line, role: 'declares-environment' }, haskell: { file: r.file, line: r.line, role: 'reads-environment' },
184
+ provenance: { nix: 'systemd service environment (configuration)', haskell: 'getEnv/lookupEnv literal (source)', link: launched.length ? `the service launches ${launched[0]}, an executable built by the project's cabal file` : 'only the variable name matches: no evidence this service runs that program' },
185
+ flow: 'a runtime configuration bridge, not a data-flow proof' });
186
+ }
187
+ }
188
+ }
189
+ return { version: NIX_AGENTS_VERSION, findings, agents, bridges };
190
+ }
191
+
192
+ // EXTERNAL MODULE: ./src/language/nixos-hardening.js
193
+ var nixos_hardening = __webpack_require__(188);
194
+ ;// CONCATENATED MODULE: ./src/language/engine-pass.js
195
+ // Language supply-chain pass for the scan engine. Nix fetch/build/cache trust findings are
196
+ // supply-chain entries, so they join the ordinary `supplyChain` bucket (and from there SCA
197
+ // policy, scan health and every report) instead of a side channel. Static only: nothing here
198
+ // evaluates Nix, fetches a URL or runs a builder.
199
+
200
+
201
+
202
+
203
+
204
+
205
+
206
+
207
+
208
+
209
+ const isNixSource = (p) => /\.nix$/i.test(p);
210
+ const isFlakeLock = (p) => /(^|\/)flake\.lock$/i.test(p);
211
+
212
+ /**
213
+ * @param {Record<string,string>} files rel path -> content (sources and manifests as the engine read them)
214
+ * @returns {{supplyChain: object[], gaps: object[], analyzed: number}}
215
+ */
216
+ const NIXOS_OPTION = /(?:^|[\s;{])(?:services|networking|security|systemd|users|environment|programs|boot|virtualisation|nix)\.[A-Za-z_]|(?:^|[\s;{])(?:services|networking|security|systemd|users|environment|programs|boot|virtualisation)\s*=\s*\{/m;
217
+
218
+ /** NixOS entry points of a Nix file set: every configuration.nix, else every module nothing imports (capped). */
219
+ function nixosEntries(nix, { max = 15 } = {}) {
220
+ const conf = Object.keys(nix).filter((p) => /(?:^|\/)configuration\.nix$/i.test(p)).sort().slice(0, 5);
221
+ const imported = new Set();
222
+ try { const { graph } = (0,discovery/* buildImportGraph */.oK)(nix); for (const deps of graph.values()) for (const d of deps) imported.add(d); } catch { /* no import graph */ }
223
+ const standalone = Object.keys(nix).filter((p) => !conf.includes(p) && !imported.has(p) && !/(?:^|\/)flake\.nix$/i.test(p) && NIXOS_OPTION.test(nix[p])).sort().slice(0, max);
224
+ return [...conf, ...standalone];
225
+ }
226
+
227
+ function _baseSupplyChain(files = {}, opts = {}) {
228
+ const nix = {};
229
+ for (const [p, text] of Object.entries(files)) {
230
+ if (typeof text !== 'string') continue;
231
+ if (!(isNixSource(p) || isFlakeLock(p))) continue;
232
+ if ((0,discovery/* isLanguageExcludedPath */.EG)(p)) continue;
233
+ nix[p] = text;
234
+ }
235
+ // Haskell: Hackage advisories and source/name policy (manifests, a freeze or a plan, and the .hs imports).
236
+ const hs = (0,haskell_supply/* analyzeHaskellSupply */.i1)(files, { root: opts.scanRoot || null, resolved: opts.resolved || null });
237
+ const sources = Object.keys(nix).filter(isNixSource);
238
+ if (!sources.length) {
239
+ if (!hs.components.length) return { supplyChain: [], gaps: [], analyzed: 0 };
240
+ return { supplyChain: hs.supplyChain, gaps: hs.gaps, analyzed: hs.components.length, haskell: hs };
241
+ }
242
+ // The effective configuration is decided from an ENTRY: configuration.nix when there is one, otherwise each NixOS module that
243
+ // nothing imports (a flake's host file under any name). With no entry at all only the file-local rules run.
244
+ const entries = nixosEntries(nix);
245
+ const reports = entries.length ? entries.map((entry) => (0,nix_build_trust/* analyzeNixBuildTrust */.OQ)({ files: nix, entry })) : [(0,nix_build_trust/* analyzeNixBuildTrust */.OQ)({ files: nix })];
246
+ const seenIds = new Set(); const merged = []; const gaps = [];
247
+ for (const r of reports) {
248
+ for (const f of r.findings) { const k = f.id || `${f.rule}|${f.file}|${f.line}`; if (!seenIds.has(k)) { seenIds.add(k); merged.push(f); } }
249
+ for (const g of r.gaps) gaps.push(g);
250
+ }
251
+ return { supplyChain: [...hs.supplyChain, ...merged], gaps: [...hs.gaps, ...gaps], analyzed: sources.length + hs.components.length, haskell: hs };
252
+ }
253
+
254
+ /**
255
+ * Findings (not supply-chain entries) from Nix: embedded-shell taint (NIX-003). Returns the findings plus the
256
+ * coverage gaps (unsupported shells, scripts built by expressions) so scan health can report them.
257
+ */
258
+ function analyzeLanguageFindings(files = {}) {
259
+ const nix = {};
260
+ for (const [p, text] of Object.entries(files)) {
261
+ if (typeof text !== 'string' || !isNixSource(p) || (0,discovery/* isLanguageExcludedPath */.EG)(p)) continue;
262
+ nix[p] = text;
263
+ }
264
+ if (!Object.keys(nix).length) return { findings: [], gaps: [], analyzed: 0 };
265
+ const r = (0,nix_script_taint/* analyzeNixScripts */.S3)({ files: nix });
266
+ // secret placement sees referenced data files (sopsFile / agenix file) as well as the Nix sources
267
+ const sec = (0,nix_secrets/* analyzeNixSecrets */.D0)({ files: { ...files, ...nix } });
268
+ let agents = { findings: [] };
269
+ try { agents = analyzeNixAgents(files); } catch { /* agent/MCP rules are best effort */ }
270
+ // NIX-004: effective-configuration hardening, judged per NixOS entry point (configuration.nix). A finding with no
271
+ // option source (a catalog default decided it) is anchored to the entry file at line 1 and says so, so it can be
272
+ // reported, attributed and suppressed instead of being lineless. The files that decided the value travel with it.
273
+ const hardening = [];
274
+ const effective = [];
275
+ // Entry points: every configuration.nix, plus NixOS modules NOTHING imports (a standalone module, a flake's host file under any
276
+ // name): its options are the only place their values are decided, and a file name is not what makes a configuration real.
277
+ const confEntries = Object.keys(nix).filter((p) => /(?:^|\/)configuration\.nix$/i.test(p)).sort().slice(0, 5);
278
+ const entryList = nixosEntries(nix);
279
+ for (const entry of entryList) {
280
+ try {
281
+ const h = (0,nixos_hardening/* analyzeNixosHardening */.QC)({ entry, files: nix });
282
+ effective.push({ entry, completeness: h.reconciliation && h.reconciliation.completeness, unresolved: (h.reconciliation && h.reconciliation.unanalyzed) || [], truncated: [], target: h.target });
283
+ for (const f of h.findings) {
284
+ const ctl = [...new Set((f.evidence || []).flatMap((e) => (e.sources || []).map((x) => x.file)).filter(Boolean))];
285
+ const anchored = Number.isInteger(f.line) && f.file;
286
+ hardening.push({ ...f, file: anchored ? f.file : entry, line: anchored ? f.line : 1, locationKind: anchored ? 'option-source' : 'target-level', controlFiles: ctl, entry, entryKind: confEntries.includes(entry) ? 'configuration' : 'standalone-module', optionEvidence: f.evidence, option: f.evidence && f.evidence[0] ? f.evidence[0].option : null });
287
+ }
288
+ } catch (e) { r.gaps.push({ kind: 'hardening-failed', file: entry, reason: String((e && e.message) || e) }); }
289
+ }
290
+ // Two different rules of one family can land on the same line (a one-line configuration, or several defaults that
291
+ // anchor at the entry). The engine's per-(file, line, family) de-duplication must not merge them into one finding, so
292
+ // each carries a site key built from what makes it a different finding. The key also feeds the stable id.
293
+ const keyed = (list, parts) => list.map((f) => (f && f.siteKey ? f : { ...f, siteKey: parts(f).filter((x) => x !== null && x !== undefined && x !== '').join('|') }));
294
+ const out = [
295
+ ...keyed(r.findings, (f) => [f.rule, f.attrPath, f.sink && f.sink.command, f.generatedLocation && `${f.generatedLocation.line}:${f.generatedLocation.startColumn}`]),
296
+ ...keyed(sec.findings, (f) => [f.rule, f.source && f.source.detail, f.destination && f.destination.label]),
297
+ ...agents.findings,
298
+ ...keyed(hardening, (f) => [f.rule, f.subject]),
299
+ ];
300
+ return { findings: out, gaps: [...r.gaps, ...sec.gaps], analyzed: r.analyzed, effectiveConfigs: effective };
301
+ }
302
+
303
+ /**
304
+ * The language supply chain: declared and resolved Hackage dependencies, Nix inputs and build trust, and (when the project exported
305
+ * one) the Nix closure matched against an advisory snapshot. The resolved data comes from EXPLICIT export files only.
306
+ */
307
+ function analyzeLanguageSupplyChain(files = {}, opts = {}) {
308
+ let resolved = null; let closure = null;
309
+ try { resolved = (0,resolved_pass.resolvedHackageComponents)(files); } catch { resolved = null; }
310
+ try { closure = (0,resolved_pass.analyzeNixClosure)(files, { scanRoot: opts.scanRoot || null }); } catch (e) { closure = { findings: [], gaps: [{ kind: 'closure-analysis-failed', detail: String((e && e.message) || e).slice(0, 160) }], statuses: [], sources: [], status: 'failed' }; }
311
+ const base = _baseSupplyChain(files, { ...opts, resolved });
312
+ if (!closure) return resolved && resolved.summary ? { ...base, resolved: resolved.summary } : base;
313
+ return { ...base, supplyChain: [...base.supplyChain, ...closure.findings], gaps: [...(base.gaps || []), ...closure.gaps], analyzed: (base.analyzed || 0) + closure.statuses.length, resolved: resolved && resolved.summary, nixClosure: closure };
314
+ }
315
+
316
+
317
+ /***/ })
318
+
319
+ };