sfscanner 0.0.0-stage → 0.1.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.
@@ -0,0 +1,818 @@
1
+ // src/v2/metadata-output.ts
2
+ import { readFileSync as readFileSync2 } from "node:fs";
3
+
4
+ // src/v2/commands.ts
5
+ import { createHash } from "node:crypto";
6
+ import { readFileSync } from "node:fs";
7
+
8
+ // ../scanner-core/src/canonical-json.ts
9
+ function canonicalizeJson(value) {
10
+ return encodeCanonical(value, /* @__PURE__ */ new Set());
11
+ }
12
+ function encodeCanonical(value, seen) {
13
+ if (value === null) return "null";
14
+ if (typeof value === "string") return JSON.stringify(value);
15
+ if (typeof value === "boolean") return value ? "true" : "false";
16
+ if (typeof value === "number") {
17
+ if (!Number.isFinite(value)) throw new TypeError("Cannot encode non-finite number as canonical JSON");
18
+ return JSON.stringify(Object.is(value, -0) ? 0 : value);
19
+ }
20
+ if (typeof value !== "object" || value === void 0) {
21
+ throw new TypeError(`Cannot encode ${typeof value} as canonical JSON`);
22
+ }
23
+ if (seen.has(value)) throw new TypeError("Cannot encode cyclic value as canonical JSON");
24
+ seen.add(value);
25
+ try {
26
+ if (Array.isArray(value)) return encodeArray(value, seen);
27
+ return encodeObject(value, seen);
28
+ } finally {
29
+ seen.delete(value);
30
+ }
31
+ }
32
+ function encodeArray(value, seen) {
33
+ const values = Array.from({ length: value.length }, (_, index) => {
34
+ if (!Object.prototype.hasOwnProperty.call(value, index)) {
35
+ throw new TypeError("Cannot encode sparse array as canonical JSON");
36
+ }
37
+ return encodeCanonical(value[index], seen);
38
+ });
39
+ return `[${values.join(",")}]`;
40
+ }
41
+ function encodeObject(value, seen) {
42
+ const prototype = Object.getPrototypeOf(value);
43
+ if (prototype !== Object.prototype && prototype !== null) {
44
+ throw new TypeError("Cannot encode a non-plain object as canonical JSON");
45
+ }
46
+ if (Object.getOwnPropertySymbols(value).some((symbol) => Object.prototype.propertyIsEnumerable.call(value, symbol))) {
47
+ throw new TypeError("Cannot encode symbol properties as canonical JSON");
48
+ }
49
+ const pairs = Object.keys(value).sort(compareCanonicalStrings).map((key) => `${JSON.stringify(key)}:${encodeCanonical(value[key], seen)}`);
50
+ return `{${pairs.join(",")}}`;
51
+ }
52
+ function compareCanonicalStrings(left, right) {
53
+ if (left === right) return 0;
54
+ return left < right ? -1 : 1;
55
+ }
56
+
57
+ // ../scanner-core/src/policy/rule-catalog.ts
58
+ var betaRule = (policyId, defaultSeverity, defaultConfidence, precision, cweIds) => ({
59
+ policyId,
60
+ defaultSeverity,
61
+ defaultConfidence,
62
+ precision,
63
+ policyEffect: "LEGACY_SCORE",
64
+ curationAction: "NONE",
65
+ serveAction: "DISPLAY",
66
+ cweIds
67
+ });
68
+ var betaDefaultRuleCatalog = [
69
+ betaRule("N8N_EXECUTE_COMMAND", "CRITICAL", "HIGH", "HIGH", ["CWE-78"]),
70
+ betaRule("N8N_CODE_NODE", "LOW", "HIGH", "HIGH", ["CWE-94"]),
71
+ // Literal Authorization values are a hardcoded credential (CWE-798); expression/credential-
72
+ // driven values that still leaked as a raw header are an insufficiently protected transmission
73
+ // (CWE-522). Both apply to this rule depending on evidence shape, so both are catalogued.
74
+ betaRule("RAW_AUTH_HEADER", "HIGH", "MEDIUM", "MEDIUM", ["CWE-798", "CWE-522"]),
75
+ betaRule("RAW_SECRET_INDICATOR", "HIGH", "MEDIUM", "MEDIUM", ["CWE-798"]),
76
+ betaRule("UNAPPROVED_DOMAIN", "MEDIUM", "HIGH", "HIGH", []),
77
+ betaRule("UNKNOWN_NODE_PACKAGE", "LOW", "MEDIUM", "MEDIUM", ["CWE-1357"]),
78
+ betaRule("WEBHOOK_EXPOSURE", "MEDIUM", "MEDIUM", "MEDIUM", ["CWE-306"])
79
+ ];
80
+ var reportOnlyRule = (policyId, defaultSeverity, defaultConfidence, precision, cweIds, owaspAgenticIds) => ({
81
+ policyId,
82
+ defaultSeverity,
83
+ defaultConfidence,
84
+ precision,
85
+ policyEffect: "REPORT_ONLY",
86
+ curationAction: "REVIEW",
87
+ serveAction: "DISPLAY",
88
+ cweIds,
89
+ ...owaspAgenticIds ? { owaspAgenticIds } : {}
90
+ });
91
+ var betaHardenedRuleCatalog = [
92
+ ...betaDefaultRuleCatalog,
93
+ // CWE-95 ("Eval Injection") precisely fits only this rule's EVAL/FUNCTION_CONSTRUCTOR
94
+ // indicators; CHILD_PROCESS_* is closer to CWE-78 and FILESYSTEM_ACCESS to CWE-73. One CWE per
95
+ // policy ID is a simplification of the eight indicators this rule reports under; see the
96
+ // `[^js]`/`[^py]` footnotes in docs/scanner/policy-inventory.md.
97
+ reportOnlyRule("JAVASCRIPT_DYNAMIC_EXECUTION", "HIGH", "HIGH", "HIGH", ["CWE-95"]),
98
+ reportOnlyRule("PYTHON_DYNAMIC_EXECUTION", "HIGH", "MEDIUM", "MEDIUM", ["CWE-95"]),
99
+ reportOnlyRule("TAINTED_COMMAND", "CRITICAL", "MEDIUM", "MEDIUM", ["CWE-78"]),
100
+ reportOnlyRule("TAINTED_SQL_QUERY", "HIGH", "MEDIUM", "MEDIUM", ["CWE-89"]),
101
+ reportOnlyRule("TAINTED_REQUEST_URL", "HIGH", "MEDIUM", "MEDIUM", ["CWE-918"]),
102
+ reportOnlyRule("TAINTED_FILE_PATH", "HIGH", "MEDIUM", "MEDIUM", ["CWE-22"]),
103
+ reportOnlyRule("TAINTED_CODE_INPUT", "HIGH", "MEDIUM", "MEDIUM", ["CWE-94"]),
104
+ reportOnlyRule("TAINTED_REQUEST_HEADER", "MEDIUM", "MEDIUM", "MEDIUM", ["CWE-113"]),
105
+ // No CWE is dedicated to LLM prompt injection; CWE-77 ("Command Injection") is the nearest
106
+ // structural analogy (untrusted data reaching an interpreter that blends instructions and
107
+ // data). OWASP Agentic's "Prompt Injection" is the precise, purpose-built taxonomy tag and
108
+ // should be treated as primary; CWE-77 is a bridge for CWE-only tooling.
109
+ reportOnlyRule("TAINTED_AI_PROMPT", "MEDIUM", "LOW", "LOW", ["CWE-77"], ["Prompt Injection"]),
110
+ // CWE-269 (Improper Privilege Management) frames the agent as holding privilege it should not
111
+ // autonomously exercise; the alternative CWE-862 (Missing Authorization) was reviewed and is
112
+ // also defensible, but the OWASP Agentic "Excessive Agency" pairing does the precise work
113
+ // either way, so CWE-269 is kept per docs/scanner/policy-inventory.md.
114
+ reportOnlyRule("AI_AGENT_UNGATED_TOOL", "MEDIUM", "LOW", "LOW", ["CWE-269"], ["Excessive Agency"])
115
+ ];
116
+
117
+ // src/v2/contracts.ts
118
+ var SCAN_CONFIG_SCHEMA_V2 = "n8n-scan-config-v2";
119
+ var ADMITTED_PROFILE_V2 = Object.freeze({ id: "beta-default", version: "2.0.0", digest: "3ca5d69db2554383e22c44afe507cc98e381ea58a7e42aaac0ee8f7497fb4f44" });
120
+ var ADMITTED_PROFILE_SPECIFIERS_V2 = Object.freeze([
121
+ ADMITTED_PROFILE_V2.id,
122
+ `${ADMITTED_PROFILE_V2.id}@${ADMITTED_PROFILE_V2.version}`
123
+ ]);
124
+ function isAdmittedProfileSpecifierV2(value) {
125
+ return typeof value === "string" && ADMITTED_PROFILE_SPECIFIERS_V2.includes(value);
126
+ }
127
+ var BUILTIN_AUTHORITIES_V2 = Object.freeze({
128
+ profile: ADMITTED_PROFILE_V2,
129
+ rules: Object.freeze({ id: "scanner-core-builtins", version: "2.0.0", digest: "a36d00564bbf8e8c38f05820480782cdc53fdbd3712558850066c4e3f5c3413d", signatureStatus: "VERIFIED" }),
130
+ semantics: Object.freeze({ id: "scanner-core-builtins", version: "2.0.0", digest: "1bb11f63269d427cb28e141dfe0a6a1512e53c298abdb4d6450545ceb56d11c4" }),
131
+ limits: Object.freeze({ schemaVersion: "scanner-limits-v2", digest: "fca787753ca88f71cf0d174427f4ac730a7aef1c823aaf73aa1ea72af92b4077" })
132
+ });
133
+ var PROFILE_MANIFEST_DIGEST_V2 = ADMITTED_PROFILE_V2.digest;
134
+ var BUILTIN_RULESET_AUTHORITY_V2 = BUILTIN_AUTHORITIES_V2.rules;
135
+ var BUILTIN_SEMANTICS_AUTHORITY_V2 = BUILTIN_AUTHORITIES_V2.semantics;
136
+ var BUILTIN_LIMITS_AUTHORITY_V2 = BUILTIN_AUTHORITIES_V2.limits;
137
+ var RULE_PACK_DIGEST_V2 = BUILTIN_RULESET_AUTHORITY_V2.digest;
138
+ var SEMANTIC_CATALOGUE_DIGEST_V2 = BUILTIN_SEMANTICS_AUTHORITY_V2.digest;
139
+ var DEFAULT_SCAN_CONFIG_V2 = Object.freeze({
140
+ schemaVersion: SCAN_CONFIG_SCHEMA_V2,
141
+ format: "json",
142
+ profile: ADMITTED_PROFILE_V2.id,
143
+ failOn: "INFO",
144
+ requireComplete: false,
145
+ concurrency: 1,
146
+ rules: "builtin",
147
+ catalogue: "builtin",
148
+ limits: "builtin",
149
+ include: Object.freeze(["**/*.json"]),
150
+ exclude: Object.freeze([".git/**", "node_modules/**"]),
151
+ allowUnsignedDevelopmentRules: false,
152
+ noColor: false
153
+ });
154
+ var SCAN_CONFIG_V2_FIELDS = Object.freeze([
155
+ "schemaVersion",
156
+ "format",
157
+ "profile",
158
+ "output",
159
+ "inputs",
160
+ "configPath",
161
+ "failOn",
162
+ "requireComplete",
163
+ "concurrency",
164
+ "rules",
165
+ "catalogue",
166
+ "limits",
167
+ "target",
168
+ "include",
169
+ "exclude",
170
+ "allowUnsignedDevelopmentRules",
171
+ "noColor"
172
+ ]);
173
+ var SCAN_CONFIG_V2_MAX_STRING = 4096;
174
+ var SCAN_CONFIG_V2_MAX_PATTERNS = 1024;
175
+ var SCAN_CONFIG_V2_MAX_CONCURRENCY = 16;
176
+
177
+ // src/v2/config.ts
178
+ var BUILTIN = { builtin: true };
179
+ var CLI_NAMES = Object.freeze({
180
+ "fail-on": "failOn",
181
+ "require-complete": "requireComplete",
182
+ "max-concurrency": "concurrency",
183
+ "allow-unsigned-development-rules": "allowUnsignedDevelopmentRules",
184
+ "no-color": "noColor",
185
+ config: "configPath"
186
+ });
187
+ var REPEATABLE_CLI = { inputs: true, include: true, exclude: true };
188
+ function loadExplicitConfig(input, options = {}) {
189
+ const errors = [];
190
+ const config = parseConfig(input, errors);
191
+ const cli = readCli(options.cli, errors);
192
+ if (typeof options.configDirectory !== "undefined" && typeof options.configDirectory !== "string") errors.push("config directory is invalid");
193
+ if (!config || !cli) return failure(errors);
194
+ const configPathCount = Number(Object.prototype.hasOwnProperty.call(config, "configPath")) + Number(Object.prototype.hasOwnProperty.call(cli, "configPath")) + Number(Object.prototype.hasOwnProperty.call(cli, "_configPath"));
195
+ if (configPathCount > 1) errors.push("config path supplied more than once");
196
+ const merged = Object.assign(/* @__PURE__ */ Object.create(null), DEFAULT_SCAN_CONFIG_V2, config);
197
+ for (const key of Object.keys(cli)) if (key !== "configPath" && key !== "_configPath") merged[key] = cli[key];
198
+ if (Object.prototype.hasOwnProperty.call(cli, "configPath")) merged.configPath = cli.configPath;
199
+ else if (Object.prototype.hasOwnProperty.call(cli, "_configPath")) merged.configPath = cli._configPath;
200
+ const result = normalize(merged, config, cli, errors);
201
+ return result ? Object.freeze({ ok: true, config: deepFreeze(result) }) : failure(errors);
202
+ }
203
+ function parseConfig(input, errors) {
204
+ if (typeof input === "string") {
205
+ if (input.length > SCAN_CONFIG_V2_MAX_STRING * 16 || duplicateKeys(input)) {
206
+ errors.push("configuration JSON is invalid");
207
+ return void 0;
208
+ }
209
+ try {
210
+ input = JSON.parse(input);
211
+ } catch {
212
+ errors.push("configuration JSON is invalid");
213
+ return void 0;
214
+ }
215
+ }
216
+ const value = record(input);
217
+ if (!value) {
218
+ errors.push("configuration must be a closed object");
219
+ return void 0;
220
+ }
221
+ for (const key of Object.keys(value)) if (!SCAN_CONFIG_V2_FIELDS.includes(key)) errors.push(`unknown configuration key: ${safeErrorKey(key)}`);
222
+ if (value.schemaVersion !== "n8n-scan-config-v2") errors.push("unsupported schemaVersion");
223
+ return Object.keys(value).every((key) => SCAN_CONFIG_V2_FIELDS.includes(key)) ? value : void 0;
224
+ }
225
+ function readCli(input, errors) {
226
+ if (input === void 0) return {};
227
+ const value = record(input);
228
+ if (!value) {
229
+ errors.push("CLI configuration is invalid");
230
+ return void 0;
231
+ }
232
+ const out = /* @__PURE__ */ Object.create(null);
233
+ for (const [rawKey, rawValue] of Object.entries(value)) {
234
+ if (rawKey === "config") {
235
+ if (Object.prototype.hasOwnProperty.call(out, "_configPath")) errors.push("config path supplied more than once");
236
+ out._configPath = rawValue;
237
+ continue;
238
+ }
239
+ const key = CLI_NAMES[rawKey] ?? rawKey;
240
+ if (!SCAN_CONFIG_V2_FIELDS.includes(key)) {
241
+ errors.push(`unknown CLI configuration key: ${safeErrorKey(rawKey)}`);
242
+ continue;
243
+ }
244
+ out[key] = REPEATABLE_CLI[key] === true && typeof rawValue === "string" ? [rawValue] : rawValue;
245
+ }
246
+ return out;
247
+ }
248
+ function normalize(value, config, cli, errors) {
249
+ const out = Object.assign(/* @__PURE__ */ Object.create(null), { schemaVersion: "n8n-scan-config-v2" });
250
+ const format = value.format;
251
+ if (format !== "json" && format !== "ndjson" && format !== "sarif" && format !== "table") errors.push("format is invalid");
252
+ else out.format = format;
253
+ const profile = value.profile;
254
+ if (!isAdmittedProfileSpecifierV2(profile)) errors.push("profile is not admitted");
255
+ else out.profile = ADMITTED_PROFILE_V2.id;
256
+ if (value.failOn !== "CRITICAL" && value.failOn !== "HIGH" && value.failOn !== "MEDIUM" && value.failOn !== "LOW" && value.failOn !== "INFO") errors.push("failOn is invalid");
257
+ else out.failOn = value.failOn;
258
+ for (const key of ["requireComplete", "allowUnsignedDevelopmentRules", "noColor"]) {
259
+ if (typeof value[key] !== "boolean") errors.push(`${key} is invalid`);
260
+ else out[key] = value[key];
261
+ }
262
+ if (!Number.isSafeInteger(value.concurrency) || value.concurrency < 1 || value.concurrency > SCAN_CONFIG_V2_MAX_CONCURRENCY) errors.push("concurrency is outside the admitted range");
263
+ else out.concurrency = value.concurrency;
264
+ for (const key of ["rules", "catalogue"]) {
265
+ const raw = value[key];
266
+ if (typeof raw !== "string" || BUILTIN[raw] !== true) errors.push(`${key} is not admitted in built-in-only v2`);
267
+ else out[key] = "builtin";
268
+ }
269
+ for (const key of ["limits", "target", "output", "configPath"]) if (value[key] !== void 0 || Object.prototype.hasOwnProperty.call(value, key)) {
270
+ const raw = value[key];
271
+ if (typeof raw !== "string") errors.push(`${key} is invalid`);
272
+ else {
273
+ const path = localPath(raw, false, errors, key === "limits");
274
+ if (path) out[key] = path;
275
+ }
276
+ }
277
+ for (const key of ["inputs", "include", "exclude"]) {
278
+ const raw = value[key];
279
+ if (raw === void 0) {
280
+ if (Object.prototype.hasOwnProperty.call(value, key)) errors.push(`${key} is invalid`);
281
+ continue;
282
+ }
283
+ const items = arrayValues(raw);
284
+ if (!items || items.length > SCAN_CONFIG_V2_MAX_PATTERNS) {
285
+ errors.push(`${key} is outside the admitted limit`);
286
+ continue;
287
+ }
288
+ const paths = [];
289
+ for (const item of items) if (typeof item !== "string") errors.push(`${key} contains an invalid path`);
290
+ else {
291
+ const path = localPath(item, true, errors);
292
+ if (path) paths.push(path);
293
+ }
294
+ out[key] = Object.freeze(paths);
295
+ }
296
+ if (!Object.prototype.hasOwnProperty.call(config, "include") && !Object.prototype.hasOwnProperty.call(cli, "include")) out.include = DEFAULT_SCAN_CONFIG_V2.include;
297
+ if (!Object.prototype.hasOwnProperty.call(config, "exclude") && !Object.prototype.hasOwnProperty.call(cli, "exclude")) out.exclude = DEFAULT_SCAN_CONFIG_V2.exclude;
298
+ return errors.length === 0 ? out : void 0;
299
+ }
300
+ function localPath(raw, pattern, errors, allowBuiltin = false) {
301
+ if (raw.length === 0 || raw.length > SCAN_CONFIG_V2_MAX_STRING || /[\u0000-\u001f\u007f]/.test(raw)) {
302
+ errors.push("path is invalid");
303
+ return void 0;
304
+ }
305
+ if (BUILTIN[raw] === true) {
306
+ if (allowBuiltin) return "builtin";
307
+ errors.push("built-in marker is not valid for this path");
308
+ return void 0;
309
+ }
310
+ const normalized = raw.replaceAll("\\", "/");
311
+ if (/^(?:[a-z][a-z\d+.-]*:|\/|[a-z]:)/i.test(normalized)) {
312
+ errors.push("absolute or remote path is not admitted");
313
+ return void 0;
314
+ }
315
+ const parts = [];
316
+ for (const part of normalized.split("/")) {
317
+ if (!part || part === ".") continue;
318
+ if (part === "..") {
319
+ if (pattern || parts.length === 0) {
320
+ errors.push("path escapes its config directory");
321
+ return void 0;
322
+ }
323
+ parts.pop();
324
+ continue;
325
+ }
326
+ parts.push(part);
327
+ }
328
+ if (parts.length === 0) {
329
+ errors.push("path is invalid");
330
+ return void 0;
331
+ }
332
+ return parts.join("/");
333
+ }
334
+ function arrayValues(value) {
335
+ try {
336
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || Object.getOwnPropertySymbols(value).length > 0) return void 0;
337
+ const names = Object.getOwnPropertyNames(value);
338
+ if (names.length !== value.length + 1) return void 0;
339
+ const output = [];
340
+ for (let index = 0; index < value.length; index += 1) {
341
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
342
+ if (!descriptor || !("value" in descriptor)) return void 0;
343
+ output.push(descriptor.value);
344
+ }
345
+ return output;
346
+ } catch {
347
+ return void 0;
348
+ }
349
+ }
350
+ function safeErrorKey(value) {
351
+ return /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(value) ? value : "field";
352
+ }
353
+ function record(value) {
354
+ try {
355
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
356
+ const proto = Object.getPrototypeOf(value);
357
+ if (proto !== Object.prototype && proto !== null) return void 0;
358
+ if (Object.getOwnPropertySymbols(value).length > 0) return void 0;
359
+ const out = /* @__PURE__ */ Object.create(null);
360
+ for (const key of Object.getOwnPropertyNames(value)) {
361
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
362
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) return void 0;
363
+ out[key] = descriptor.value;
364
+ }
365
+ return out;
366
+ } catch {
367
+ return void 0;
368
+ }
369
+ }
370
+ function duplicateKeys(text) {
371
+ let index = 0;
372
+ const skip = () => {
373
+ while (/\s/.test(text[index] ?? "")) index += 1;
374
+ };
375
+ const string = () => {
376
+ index += 1;
377
+ while (index < text.length) {
378
+ const char = text[index++];
379
+ if (char === "\\") index += 1;
380
+ else if (char === '"') return;
381
+ }
382
+ throw new Error();
383
+ };
384
+ const value = () => {
385
+ skip();
386
+ const char = text[index];
387
+ if (char === "{") return object();
388
+ if (char === "[") return array();
389
+ if (char === '"') {
390
+ string();
391
+ return;
392
+ }
393
+ const start = index;
394
+ while (index < text.length && !/[\s,\]}]/.test(text[index])) index += 1;
395
+ if (start === index) throw new Error();
396
+ };
397
+ const object = () => {
398
+ index += 1;
399
+ const keys = /* @__PURE__ */ new Set();
400
+ skip();
401
+ if (text[index] === "}") {
402
+ index += 1;
403
+ return;
404
+ }
405
+ while (index < text.length) {
406
+ skip();
407
+ if (text[index] !== '"') throw new Error();
408
+ const start = index;
409
+ string();
410
+ const key = text.slice(start, index);
411
+ if (keys.has(key)) throw new Error();
412
+ keys.add(key);
413
+ skip();
414
+ if (text[index++] !== ":") throw new Error();
415
+ value();
416
+ skip();
417
+ if (text[index] === "}") {
418
+ index += 1;
419
+ return;
420
+ }
421
+ if (text[index++] !== ",") throw new Error();
422
+ }
423
+ throw new Error();
424
+ };
425
+ const array = () => {
426
+ index += 1;
427
+ skip();
428
+ if (text[index] === "]") {
429
+ index += 1;
430
+ return;
431
+ }
432
+ while (index < text.length) {
433
+ value();
434
+ skip();
435
+ if (text[index] === "]") {
436
+ index += 1;
437
+ return;
438
+ }
439
+ if (text[index++] !== ",") throw new Error();
440
+ }
441
+ throw new Error();
442
+ };
443
+ try {
444
+ value();
445
+ skip();
446
+ if (index !== text.length) throw new Error();
447
+ return false;
448
+ } catch {
449
+ return true;
450
+ }
451
+ }
452
+ function failure(errors) {
453
+ return Object.freeze({ ok: false, code: "INVALID_CONFIG", errors: Object.freeze([...new Set(errors)].sort()) });
454
+ }
455
+ function deepFreeze(value, seen = /* @__PURE__ */ new WeakSet()) {
456
+ if (value && typeof value === "object") {
457
+ if (seen.has(value)) return value;
458
+ seen.add(value);
459
+ for (const key of Object.getOwnPropertyNames(value)) deepFreeze(value[key], seen);
460
+ Object.freeze(value);
461
+ }
462
+ return value;
463
+ }
464
+
465
+ // src/v2/commands.ts
466
+ var CLI_VERSION = "0.1.0";
467
+ var hash = (value) => createHash("sha256").update(canonicalizeJson(value)).digest("hex");
468
+ var targetBody = Object.freeze({ edition: "UNKNOWN", deployment: "UNKNOWN", role: "UNKNOWN", taskRunner: "UNKNOWN", communityPackages: Object.freeze([]), configuration: Object.freeze([]) });
469
+ var TOOL = Object.freeze({ name: "n8n-scan", version: CLI_VERSION, buildDigest: hash({ engine: "secureflow-scanner", version: "2.0.0", profile: ADMITTED_PROFILE_V2, ruleset: BUILTIN_RULESET_AUTHORITY_V2.digest, semantics: BUILTIN_SEMANTICS_AUTHORITY_V2.digest }) });
470
+ var EVIDENCE = { N8N_EXECUTE_COMMAND: ["nodeType"], N8N_CODE_NODE: ["nodeType"], RAW_AUTH_HEADER: ["header", "literalValue"], RAW_SECRET_INDICATOR: ["parameterPath", "secretKind", "indicator", "hasLiteralValue"], UNAPPROVED_DOMAIN: ["hostname"], UNKNOWN_NODE_PACKAGE: ["nodeType", "nodePackage"], WEBHOOK_EXPOSURE: ["authentication"] };
471
+ var EVIDENCE_SCHEMA = { N8N_EXECUTE_COMMAND: "v1.node-type", N8N_CODE_NODE: "v1.node-type", RAW_AUTH_HEADER: "v1.auth-header", RAW_SECRET_INDICATOR: "v1.secret-indicator", UNAPPROVED_DOMAIN: "v1.domain", UNKNOWN_NODE_PACKAGE: "v1.node-package", WEBHOOK_EXPOSURE: "v1.webhook-auth" };
472
+ var PROSE = {
473
+ N8N_EXECUTE_COMMAND: { title: "Execute Command node can run shell commands", message: "This workflow can execute shell commands.", explanation: "The Execute Command node allows the workflow to launch shell commands from the n8n runtime environment.", whyItMatters: "Shell command access can cross from workflow automation into the host or worker environment that runs n8n.", possibleImpact: "A malicious or compromised workflow path could lead to command execution, file access, lateral movement, or service disruption.", remediation: "Remove the Execute Command node or isolate it behind strict approval.", remediationSteps: ["Remove the Execute Command node when shell access is not explicitly required.", "Replace shell logic with a narrow, reviewed integration or managed service call.", "If it must remain, isolate the workflow runner and require manual approval before execution."] },
474
+ N8N_CODE_NODE: { title: "Custom code node requires security review", message: "This workflow contains custom script execution.", explanation: "Code and legacy Function nodes can transform data with arbitrary JavaScript inside the workflow runtime.", whyItMatters: "Custom code is harder to audit than declarative node configuration and may handle secrets or external data.", possibleImpact: "Unsafe code can leak data, mishandle credentials, or create behavior that reviewers cannot understand from node settings alone.", remediation: "Review the code path, remove unnecessary scripting, and require approval.", remediationSteps: ["Review the node source code for secret access, outbound requests, and unsafe parsing.", "Move repeatable logic into a reviewed service or reusable library when possible.", "Require approval for changes to custom code nodes in production workflows."] },
475
+ RAW_AUTH_HEADER: { title: "Raw Authorization header may expose credentials", message: "This node configures a raw Authorization header.", explanation: "The workflow parameters appear to define an Authorization header directly instead of using managed n8n credentials.", whyItMatters: "Raw credential material in node parameters is easier to copy, persist, log, or expose in exported workflow JSON.", possibleImpact: "API keys or bearer tokens can be leaked through workflow exports, scan history, logs, or support bundles.", remediation: "Use n8n credentials or a managed secret reference instead of raw headers.", remediationSteps: ["Move the secret into n8n credentials or an approved secret manager.", "Rotate any token that has already been stored in raw workflow JSON.", "Re-scan after replacing the raw header with a managed credential reference."] },
476
+ RAW_SECRET_INDICATOR: { title: "Workflow parameters contain raw secret material", message: "This node contains a parameter that looks like a raw secret.", explanation: "The scanner found a parameter key or value pattern commonly used for tokens, passwords, API keys, cookies, or credentials.", whyItMatters: "Raw secrets in workflow JSON can be exposed through exports, scan history, logs, backups, or support bundles.", possibleImpact: "An exposed token or password can allow unauthorized access to connected systems even if the workflow itself is not malicious.", remediation: "Move the value into n8n credentials or an approved secret manager, then rotate any exposed secret.", remediationSteps: ["Move the raw value into n8n credentials or an approved secret manager.", "Rotate the secret if it has already been exported, shared, or stored in history.", "Re-scan the workflow after replacing the raw value with a managed credential reference."] },
477
+ UNAPPROVED_DOMAIN: { title: "External domain is outside the approved allowlist", message: "This workflow calls a domain outside the approved allowlist.", explanation: "The node parameters contain a URL whose hostname is not present in the approved domain list supplied for this scan.", whyItMatters: "Unexpected outbound domains can indicate data exfiltration, shadow integrations, or environment drift.", possibleImpact: "Workflow data may be sent to an unreviewed third party or an endpoint that is not covered by enterprise controls.", remediation: "Add the domain to the approved list or replace the external call.", remediationSteps: ["Confirm the business owner and purpose for the external domain.", "Add the domain to the approved list only after security and compliance review.", "Replace the call if the destination is unnecessary or untrusted."] },
478
+ UNKNOWN_NODE_PACKAGE: { title: "Node package is not from an official n8n namespace", message: "This workflow uses a community or custom node package.", explanation: "The node type does not start with an official n8n package namespace, so it comes from a community or custom package.", whyItMatters: "Community and custom packages can add supply-chain, maintenance, and permission risks that differ from core nodes.", possibleImpact: "A vulnerable or unmaintained package could process sensitive workflow data or run unexpected code.", remediation: "Review the package source, version, permissions, and maintenance status.", remediationSteps: ["Verify the package source, maintainer, and version history.", "Check whether the node requires credentials or broad runtime permissions.", "Pin approved versions and remove packages that are not actively maintained."] },
479
+ WEBHOOK_EXPOSURE: { title: "Webhook trigger lacks clear authentication", message: "This webhook does not show a clear authentication marker.", explanation: "The webhook node appears to accept inbound requests without an explicit authentication mode in the workflow parameters.", whyItMatters: "Unauthenticated inbound endpoints can be abused for spam, data injection, or unexpected workflow execution.", possibleImpact: "External callers may trigger automations, create records, or send data into connected systems without authorization.", remediation: "Require webhook authentication or place the endpoint behind trusted controls.", remediationSteps: ["Enable webhook authentication in n8n when the endpoint is not intentionally public.", "Place the endpoint behind a gateway, allowlist, or rate limiter for production use.", "Monitor webhook usage for unexpected request volume or payloads."] }
480
+ };
481
+ var CAPABILITIES = Object.freeze([
482
+ { id: "source-normalization", version: "2.0.0", required: true, dependsOn: [], ruleIds: [] },
483
+ { id: "workflow-insights", version: "2.0.0", required: true, dependsOn: ["source-normalization"], ruleIds: [] },
484
+ { id: "structural-policies", version: "2.0.0", required: true, dependsOn: ["source-normalization"], ruleIds: Object.freeze(betaDefaultRuleCatalog.map(({ policyId }) => policyId)) },
485
+ { id: "assessment", version: "2.0.0", required: true, dependsOn: ["workflow-insights", "structural-policies"], ruleIds: [] },
486
+ { id: "presentation-facts", version: "2.0.0", required: true, dependsOn: ["assessment"], ruleIds: [] }
487
+ ]);
488
+ var HARDENED_CAPABILITIES = Object.freeze([
489
+ CAPABILITIES[0],
490
+ CAPABILITIES[1],
491
+ CAPABILITIES[2],
492
+ { id: "expression-security", version: "2.0.0", required: false, dependsOn: ["source-normalization"], ruleIds: [] },
493
+ { id: "javascript-security", version: "2.0.0", required: false, dependsOn: ["source-normalization"], ruleIds: ["JAVASCRIPT_DYNAMIC_EXECUTION"] },
494
+ { id: "python-security", version: "2.0.0", required: false, dependsOn: ["source-normalization"], ruleIds: ["PYTHON_DYNAMIC_EXECUTION"] },
495
+ { id: "graph-semantics", version: "2.0.0", required: false, dependsOn: ["source-normalization"], ruleIds: [] },
496
+ { id: "taint-confidentiality", version: "2.0.0", required: false, dependsOn: ["graph-semantics", "expression-security", "javascript-security", "python-security"], ruleIds: [] },
497
+ { id: "taint-integrity", version: "2.0.0", required: false, dependsOn: ["graph-semantics", "expression-security", "javascript-security", "python-security"], ruleIds: ["TAINTED_COMMAND", "TAINTED_SQL_QUERY", "TAINTED_REQUEST_URL", "TAINTED_FILE_PATH", "TAINTED_CODE_INPUT", "TAINTED_REQUEST_HEADER", "TAINTED_AI_PROMPT"] },
498
+ { id: "agentic-control", version: "2.0.0", required: false, dependsOn: ["graph-semantics", "taint-confidentiality", "taint-integrity"], ruleIds: ["AI_AGENT_UNGATED_TOOL"] },
499
+ CAPABILITIES[3],
500
+ CAPABILITIES[4]
501
+ ]);
502
+ function artifacts(profile = ADMITTED_PROFILE_V2.id) {
503
+ const target = Object.freeze({ ...targetBody, contextDigest: hash(targetBody) });
504
+ return Object.freeze({ profile: Object.freeze({ id: profile, version: "2.0.0" }), ruleset: Object.freeze({ ...BUILTIN_RULESET_AUTHORITY_V2 }), semantics: Object.freeze({ ...BUILTIN_SEMANTICS_AUTHORITY_V2 }), target, limits: Object.freeze({ schemaVersion: "scanner-limits-v2", digest: BUILTIN_LIMITS_AUTHORITY_V2.digest }) });
505
+ }
506
+ function ruleSummary(entry) {
507
+ const capabilityId = "structural-policies";
508
+ return Object.freeze({ ruleId: entry.policyId, revision: "1", effect: entry.policyEffect === "REPORT_ONLY" ? "REPORT_ONLY" : "SCORING", severity: entry.defaultSeverity, confidence: entry.defaultConfidence, capabilityId, evidenceSchemaId: EVIDENCE_SCHEMA[entry.policyId] ?? "v1.policy", remediationId: "r", cweIds: Object.freeze([...entry.cweIds].sort()), owaspAgenticIds: Object.freeze([...entry.owaspAgenticIds ?? []].sort()) });
509
+ }
510
+ function rulesDocumentV2() {
511
+ return Object.freeze({ schemaVersion: "n8n-scan-rules-v2", tool: TOOL, artifacts: artifacts(), rules: Object.freeze(betaDefaultRuleCatalog.map(ruleSummary)) });
512
+ }
513
+ function explainDocumentV2(ruleId) {
514
+ const entry = betaDefaultRuleCatalog.find((item) => item.policyId === ruleId);
515
+ const prose = PROSE[ruleId];
516
+ if (!entry || !prose) return Object.freeze({ ok: false, code: "USAGE", message: "Unknown rule." });
517
+ const rule = ruleSummary(entry);
518
+ return Object.freeze({ ok: true, document: Object.freeze({ schemaVersion: "n8n-scan-explain-v2", tool: TOOL, artifacts: artifacts(), rule, prose: Object.freeze({ ...prose, remediationSteps: Object.freeze([...prose.remediationSteps]) }), evidenceProperties: Object.freeze([...EVIDENCE[ruleId] ?? []]), provenance: Object.freeze({ packDigest: BUILTIN_RULESET_AUTHORITY_V2.digest, semanticsDigest: BUILTIN_SEMANTICS_AUTHORITY_V2.digest }) }) });
519
+ }
520
+ function profilesDocumentV2() {
521
+ const body = (id, admitted, capabilities, advertised = true) => Object.freeze({ id, version: "2.0.0", admitted, advertised, digest: hash({ schemaVersion: "scanner-profile-v2", id, version: "2.0.0", capabilities, scoringPolicyVersion: "secureflow-risk-v2@2.0.0" }), scoringPolicyVersion: "secureflow-risk-v2@2.0.0", capabilities: Object.freeze(capabilities.map((item) => Object.freeze({ ...item, dependsOn: Object.freeze([...item.dependsOn]), ruleIds: Object.freeze([...item.ruleIds]) }))) });
522
+ return Object.freeze({ schemaVersion: "n8n-scan-profiles-v2", tool: TOOL, profiles: Object.freeze([body("beta-default", true, CAPABILITIES), body("beta-hardened", false, HARDENED_CAPABILITIES)]) });
523
+ }
524
+ function versionDocumentV2() {
525
+ return Object.freeze({ schemaVersion: "n8n-scan-version-v2", name: "n8n-scan", cliVersion: CLI_VERSION, engine: Object.freeze({ id: "secureflow-scanner", version: "2.0.0", buildDigest: TOOL.buildDigest }), resultSchemas: Object.freeze(["scanner-result-v1", "scanner-result-v2"]), configSchema: "n8n-scan-config-v2", rulesetDigest: BUILTIN_RULESET_AUTHORITY_V2.digest, semanticsDigest: BUILTIN_SEMANTICS_AUTHORITY_V2.digest, trustStoreDigest: hash({ schemaVersion: "scanner-trust-store-v2", version: "2.0.0", keys: [] }), mode: "offline" });
526
+ }
527
+ function validateConfigDocumentV2(input, configDirectory) {
528
+ const result = loadExplicitConfig(input, { configDirectory });
529
+ if (!result.ok) return Object.freeze({ ok: false, code: "USAGE", message: "Configuration is invalid." });
530
+ return Object.freeze({ ok: true, document: Object.freeze({ schemaVersion: "n8n-scan-config-validation-v2", valid: true, artifacts: artifacts() }) });
531
+ }
532
+ function isAdmittedConfigPathV2(path) {
533
+ if (typeof path !== "string" || path.length === 0 || path.length > 4096 || /[\u0000-\u001f\u007f]/.test(path)) return false;
534
+ const normalized = path.replaceAll("\\", "/");
535
+ if (/^(?:[a-z][a-z\d+.-]*:|\/|[a-z]:|\\\\)/i.test(normalized) || /(?:^|\/)\.\.(?:\/|$)/.test(normalized) || /(?:^|\/)%2e(?:%2e)?(?:\/|$)/i.test(normalized)) return false;
536
+ return normalized.split("/").some((part) => part !== "" && part !== ".");
537
+ }
538
+ function runCommandV2(command, configInput, configDirectory) {
539
+ if (command.kind === "scan") return command.profile === "beta-default" ? Object.freeze({ ok: true, command }) : Object.freeze({ ok: false, code: "USAGE", message: "Profile is not admitted." });
540
+ if (command.kind === "rules") return Object.freeze({ ok: true, document: rulesDocumentV2() });
541
+ if (command.kind === "profiles") return Object.freeze({ ok: true, document: profilesDocumentV2() });
542
+ if (command.kind === "version") return Object.freeze({ ok: true, document: versionDocumentV2() });
543
+ if (command.kind === "explain") return explainDocumentV2(command.ruleId);
544
+ if (!isAdmittedConfigPathV2(command.path)) return Object.freeze({ ok: false, code: "USAGE", message: "Configuration path is not admitted." });
545
+ let input = configInput;
546
+ if (input === void 0) {
547
+ try {
548
+ input = readFileSync(command.path, "utf8");
549
+ } catch {
550
+ return Object.freeze({ ok: false, code: "USAGE", message: "Configuration could not be read." });
551
+ }
552
+ }
553
+ return validateConfigDocumentV2(input, configDirectory);
554
+ }
555
+
556
+ // ../../contracts/stable-order.ts
557
+ function compareCanonicalStrings2(left, right) {
558
+ if (left === right) return 0;
559
+ return left < right ? -1 : 1;
560
+ }
561
+
562
+ // src/v2/metadata-validation.ts
563
+ var KEYS = {
564
+ "n8n-scan-rules-v2": ["schemaVersion", "tool", "artifacts", "rules"],
565
+ "n8n-scan-explain-v2": ["schemaVersion", "tool", "artifacts", "rule", "prose", "evidenceProperties", "provenance"],
566
+ "n8n-scan-profiles-v2": ["schemaVersion", "tool", "profiles"],
567
+ "n8n-scan-version-v2": ["schemaVersion", "name", "cliVersion", "engine", "resultSchemas", "configSchema", "rulesetDigest", "semanticsDigest", "trustStoreDigest", "mode"],
568
+ "n8n-scan-config-validation-v2": ["schemaVersion", "valid", "artifacts"],
569
+ tool: ["name", "version", "buildDigest"],
570
+ artifacts: ["profile", "ruleset", "semantics", "target", "limits"],
571
+ profileId: ["id", "version"],
572
+ ruleset: ["id", "version", "digest", "signatureStatus"],
573
+ semantics: ["id", "version", "digest"],
574
+ target: ["edition", "deployment", "role", "taskRunner", "communityPackages", "configuration", "contextDigest"],
575
+ limits: ["schemaVersion", "digest"],
576
+ rule: ["ruleId", "revision", "effect", "severity", "confidence", "capabilityId", "evidenceSchemaId", "remediationId", "cweIds", "owaspAgenticIds"],
577
+ prose: ["title", "message", "explanation", "whyItMatters", "possibleImpact", "remediation", "remediationSteps"],
578
+ provenance: ["packDigest", "semanticsDigest"],
579
+ profile: ["id", "version", "admitted", "advertised", "digest", "scoringPolicyVersion", "capabilities"],
580
+ capability: ["id", "version", "required", "dependsOn", "ruleIds"],
581
+ engine: ["id", "version", "buildDigest"]
582
+ };
583
+ var DIGEST = /^[0-9a-f]{64}$/;
584
+ var fail = () => {
585
+ throw new TypeError("Invalid metadata document");
586
+ };
587
+ function checkPlain(o) {
588
+ if (!o || typeof o !== "object" || Array.isArray(o)) return fail();
589
+ if (Object.getPrototypeOf(o) !== Object.prototype && Object.getPrototypeOf(o) !== null) return fail();
590
+ if (Object.getOwnPropertySymbols(o).length > 0) return fail();
591
+ for (const key of Object.getOwnPropertyNames(o)) {
592
+ const descriptor = Object.getOwnPropertyDescriptor(o, key);
593
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) return fail();
594
+ }
595
+ return o;
596
+ }
597
+ function checkDenseArray(arr) {
598
+ if (!Array.isArray(arr) || Object.getPrototypeOf(arr) !== Array.prototype || Object.getOwnPropertySymbols(arr).length > 0) return fail();
599
+ const names = Object.getOwnPropertyNames(arr);
600
+ if (names.length !== arr.length + 1) return fail();
601
+ for (const name of names) {
602
+ if (name === "length") continue;
603
+ if (!/^(?:0|[1-9]\d*)$/.test(name) || Number(name) >= arr.length) return fail();
604
+ const descriptor = Object.getOwnPropertyDescriptor(arr, name);
605
+ if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) return fail();
606
+ }
607
+ const length = Object.getOwnPropertyDescriptor(arr, "length");
608
+ if (!length || !("value" in length) || !Number.isSafeInteger(length.value)) return fail();
609
+ return arr;
610
+ }
611
+ function safeString(value) {
612
+ if (typeof value !== "string" || value.length === 0 || value.length > 4096 || /[\u0000-\u001f\u007f\ud800-\udfff]/.test(value) || /^[a-z][a-z0-9+.-]*:\/\//i.test(value) || /^[A-Za-z]:[\\/]|^\/|^\\\\/.test(value) || /(?:^|[\\/])\.\.(?:[\\/]|$)/.test(value)) return fail();
613
+ }
614
+ function walk(value, seen = /* @__PURE__ */ new WeakSet()) {
615
+ if (value === null) return;
616
+ if (typeof value === "string") return safeString(value);
617
+ if (typeof value === "boolean") return;
618
+ if (typeof value === "number") {
619
+ if (!Number.isFinite(value)) fail();
620
+ return;
621
+ }
622
+ if (typeof value !== "object") return fail();
623
+ if (seen.has(value)) return fail();
624
+ seen.add(value);
625
+ if (Array.isArray(value)) for (const item of checkDenseArray(value)) walk(item, seen);
626
+ else for (const item of Object.values(checkPlain(value))) walk(item, seen);
627
+ seen.delete(value);
628
+ }
629
+ function checkKeys(value, expected) {
630
+ const object = checkPlain(value);
631
+ const actual = Object.keys(object);
632
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) return fail();
633
+ return object;
634
+ }
635
+ function digest(value) {
636
+ if (typeof value !== "string" || !DIGEST.test(value)) fail();
637
+ }
638
+ function sortedStrings(value) {
639
+ const values = checkDenseArray(value);
640
+ if (values.some((item) => typeof item !== "string")) return fail();
641
+ for (let i = 1; i < values.length; i += 1) if (compareCanonicalStrings2(values[i - 1], values[i]) >= 0) return fail();
642
+ }
643
+ function artifacts2(value) {
644
+ const object = checkKeys(value, KEYS.artifacts);
645
+ checkKeys(object.profile, KEYS.profileId);
646
+ const ruleset = checkKeys(object.ruleset, KEYS.ruleset);
647
+ digest(ruleset.digest);
648
+ if (ruleset.signatureStatus !== "VERIFIED" && ruleset.signatureStatus !== "UNVERIFIED") return fail();
649
+ checkKeys(object.semantics, KEYS.semantics);
650
+ const target = checkKeys(object.target, KEYS.target);
651
+ checkDenseArray(target.communityPackages);
652
+ checkDenseArray(target.configuration);
653
+ digest(target.contextDigest);
654
+ const limits = checkKeys(object.limits, KEYS.limits);
655
+ if (limits.schemaVersion !== "scanner-limits-v2") return fail();
656
+ digest(limits.digest);
657
+ }
658
+ function exact(left, right) {
659
+ if (left === right) return true;
660
+ if (typeof left !== typeof right || left === null || right === null) return false;
661
+ if (Array.isArray(left) || Array.isArray(right)) {
662
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
663
+ return left.every((item, index) => exact(item, right[index]));
664
+ }
665
+ if (typeof left !== "object" || typeof right !== "object") return Object.is(left, right);
666
+ const a = Object.keys(left);
667
+ const b = Object.keys(right);
668
+ return a.length === b.length && a.every((key, index) => key === b[index] && exact(left[key], right[key]));
669
+ }
670
+ function authority(schema, root) {
671
+ if (schema === "n8n-scan-rules-v2") return rulesDocumentV2();
672
+ if (schema === "n8n-scan-explain-v2") {
673
+ const result2 = explainDocumentV2(String(root.rule.ruleId));
674
+ if (!result2.ok) return fail();
675
+ return result2.document;
676
+ }
677
+ if (schema === "n8n-scan-profiles-v2") return profilesDocumentV2();
678
+ if (schema === "n8n-scan-version-v2") return versionDocumentV2();
679
+ const result = validateConfigDocumentV2({ schemaVersion: "n8n-scan-config-v2" });
680
+ if (!result.ok) return fail();
681
+ return result.document;
682
+ }
683
+ function validateDocumentShape(doc) {
684
+ walk(doc);
685
+ const root = checkPlain(doc);
686
+ const schema = root.schemaVersion;
687
+ if (typeof schema !== "string" || !Object.prototype.hasOwnProperty.call(KEYS, schema)) return fail();
688
+ checkKeys(root, KEYS[schema]);
689
+ if (schema === "n8n-scan-rules-v2") {
690
+ checkKeys(root.tool, KEYS.tool);
691
+ artifacts2(root.artifacts);
692
+ const rules = checkDenseArray(root.rules);
693
+ for (const rule of rules) {
694
+ const item = checkKeys(rule, KEYS.rule);
695
+ sortedStrings(item.cweIds);
696
+ sortedStrings(item.owaspAgenticIds);
697
+ }
698
+ } else if (schema === "n8n-scan-explain-v2") {
699
+ checkKeys(root.tool, KEYS.tool);
700
+ artifacts2(root.artifacts);
701
+ checkKeys(root.rule, KEYS.rule);
702
+ const prose = checkKeys(root.prose, KEYS.prose);
703
+ checkDenseArray(prose.remediationSteps);
704
+ checkDenseArray(root.evidenceProperties);
705
+ checkKeys(root.provenance, KEYS.provenance);
706
+ } else if (schema === "n8n-scan-profiles-v2") {
707
+ checkKeys(root.tool, KEYS.tool);
708
+ const profiles = checkDenseArray(root.profiles);
709
+ for (const profile of profiles) {
710
+ const item = checkKeys(profile, KEYS.profile);
711
+ for (const capability of checkDenseArray(item.capabilities)) {
712
+ const value = checkKeys(capability, KEYS.capability);
713
+ checkDenseArray(value.dependsOn);
714
+ checkDenseArray(value.ruleIds);
715
+ }
716
+ }
717
+ } else if (schema === "n8n-scan-version-v2") {
718
+ checkKeys(root.engine, KEYS.engine);
719
+ checkDenseArray(root.resultSchemas);
720
+ digest(root.rulesetDigest);
721
+ digest(root.semanticsDigest);
722
+ digest(root.trustStoreDigest);
723
+ } else {
724
+ if (root.valid !== true) return fail();
725
+ artifacts2(root.artifacts);
726
+ }
727
+ if (!exact(root, authority(schema, root))) return fail();
728
+ }
729
+
730
+ // src/v2/metadata-output.ts
731
+ function formatTable(headers, rows) {
732
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
733
+ const sep = widths.map((w) => "-".repeat(w)).join(" ");
734
+ const formatRow = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(" ");
735
+ const body = rows.map(formatRow).join("\n");
736
+ return `${formatRow(headers)}
737
+ ${sep}${body ? `
738
+ ${body}` : ""}
739
+ `;
740
+ }
741
+ function renderExplainTable(doc) {
742
+ const { rule: r, prose: p } = doc;
743
+ const steps = p.remediationSteps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
744
+ const ev = doc.evidenceProperties.length ? doc.evidenceProperties.join(", ") : "none";
745
+ return [`Rule: ${r.ruleId} (${r.severity}, ${r.confidence}, ${r.effect})`, `Title: ${p.title}`, `Message: ${p.message}`, `Explanation: ${p.explanation}`, `Why it matters: ${p.whyItMatters}`, `Possible impact: ${p.possibleImpact}`, `Remediation: ${p.remediation}`, `Remediation steps:
746
+ ${steps}`, `Evidence properties: ${ev}`].join("\n\n") + "\n";
747
+ }
748
+ function renderMetadataTableV2(document, _options) {
749
+ validateDocumentShape(document);
750
+ if (document.schemaVersion === "n8n-scan-rules-v2") return formatTable(["Rule ID", "Severity", "Confidence", "Effect", "CWE"], document.rules.map((r) => [r.ruleId, r.severity, r.confidence, r.effect, r.cweIds.join(",") || "-"]));
751
+ if (document.schemaVersion === "n8n-scan-explain-v2") return renderExplainTable(document);
752
+ if (document.schemaVersion === "n8n-scan-profiles-v2") return formatTable(["Profile", "Version", "Admitted", "Capabilities", "Digest"], document.profiles.map((p) => [p.id, p.version, p.admitted ? "yes" : "no", String(p.capabilities.length), p.digest.slice(0, 16)]));
753
+ if (document.schemaVersion === "n8n-scan-version-v2") return [`${document.name} v${document.cliVersion} (mode: ${document.mode})`, `Engine: ${document.engine.id} v${document.engine.version} (${document.engine.buildDigest.slice(0, 16)})`, `Config schema: ${document.configSchema}`, `Ruleset digest: ${document.rulesetDigest.slice(0, 16)}`, `Semantics digest: ${document.semanticsDigest.slice(0, 16)}`].join("\n") + "\n";
754
+ return "Configuration is valid.\n";
755
+ }
756
+ function serializeMetadataJsonV2(document) {
757
+ validateDocumentShape(document);
758
+ return `${JSON.stringify(document, null, 2)}
759
+ `;
760
+ }
761
+ function serializeMetadataV2(document, format = "json", options) {
762
+ if (format === "table") return renderMetadataTableV2(document, options);
763
+ if (format === "json") return serializeMetadataJsonV2(document);
764
+ throw new TypeError("Invalid format");
765
+ }
766
+ function emitRulesDocumentV2(format = "json", options) {
767
+ return serializeMetadataV2(rulesDocumentV2(), format, options);
768
+ }
769
+ function emitExplainDocumentV2(ruleId, format = "json", options) {
770
+ const res = explainDocumentV2(ruleId);
771
+ if (!res.ok) return res;
772
+ return Object.freeze({ ok: true, output: serializeMetadataV2(res.document, format, options) });
773
+ }
774
+ function emitProfilesDocumentV2(format = "json", options) {
775
+ return serializeMetadataV2(profilesDocumentV2(), format, options);
776
+ }
777
+ function emitVersionDocumentV2(format = "json", options) {
778
+ return serializeMetadataV2(versionDocumentV2(), format, options);
779
+ }
780
+ function emitConfigValidationDocumentV2(input, format = "json", options) {
781
+ const res = validateConfigDocumentV2(input, options?.configDirectory);
782
+ if (!res.ok) return res;
783
+ return Object.freeze({ ok: true, output: serializeMetadataV2(res.document, format, options) });
784
+ }
785
+ function emitMetadataCommandResultV2(command, configInput, options) {
786
+ if (command.kind === "scan") return Object.freeze({ ok: false, code: "USAGE", message: "Scan command is not a metadata command." });
787
+ if (command.kind === "validate-config") {
788
+ if (!isAdmittedConfigPathV2(command.path)) {
789
+ return Object.freeze({ ok: false, code: "USAGE", message: "Configuration path is not admitted." });
790
+ }
791
+ let input = configInput;
792
+ if (input === void 0) {
793
+ try {
794
+ input = readFileSync2(command.path, "utf8");
795
+ } catch {
796
+ return Object.freeze({ ok: false, code: "USAGE", message: "Configuration could not be read." });
797
+ }
798
+ }
799
+ const res2 = validateConfigDocumentV2(input, options?.configDirectory);
800
+ if (!res2.ok) return res2;
801
+ return Object.freeze({ ok: true, output: serializeMetadataV2(res2.document, command.format, options) });
802
+ }
803
+ const res = runCommandV2(command, configInput, options?.configDirectory);
804
+ if (!res.ok) return res;
805
+ if ("command" in res) return Object.freeze({ ok: false, code: "USAGE", message: "Unexpected scan command." });
806
+ return Object.freeze({ ok: true, output: serializeMetadataV2(res.document, command.format, options) });
807
+ }
808
+ export {
809
+ emitConfigValidationDocumentV2,
810
+ emitExplainDocumentV2,
811
+ emitMetadataCommandResultV2,
812
+ emitProfilesDocumentV2,
813
+ emitRulesDocumentV2,
814
+ emitVersionDocumentV2,
815
+ renderMetadataTableV2,
816
+ serializeMetadataJsonV2,
817
+ serializeMetadataV2
818
+ };