@pho9ubenaa/siro 0.6.0 → 0.6.2

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.
@@ -1,3063 +0,0 @@
1
- import path from "node:path";
2
- import { readFileSync, readdirSync, statSync } from "node:fs";
3
- import { pathToFileURL } from "node:url";
4
- import satisfies from "semver/functions/satisfies.js";
5
- import * as vb from "valibot";
6
- import { lt, parse } from "semver";
7
- import valid from "semver/functions/valid.js";
8
- import { Minimatch } from "minimatch";
9
- import ini from "ini";
10
- import { parse as parse$1 } from "smol-toml";
11
- import { parseDocument } from "yaml";
12
- import pc from "picocolors";
13
- //#region src/core/contracts/errors.ts
14
- var SiroError = class extends Error {
15
- exitCode;
16
- constructor(message, exitCode) {
17
- super(message);
18
- this.name = "SiroError";
19
- this.exitCode = exitCode;
20
- }
21
- };
22
- var ConfigError = class extends SiroError {
23
- constructor(message) {
24
- super(message, 2);
25
- this.name = "ConfigError";
26
- }
27
- };
28
- var UsageError = class extends SiroError {
29
- constructor(message) {
30
- super(message, 2);
31
- this.name = "UsageError";
32
- }
33
- };
34
- /**
35
- * Run `fn` and wrap any non-`ConfigError` failure as a `ConfigError` prefixed
36
- * with `filePath`. A `ConfigError` that bubbles up from a nested call is re-thrown unchanged so
37
- * the original `path: message` framing is preserved.
38
- */
39
- const wrapCodecError = (filePath, fn) => {
40
- try {
41
- return fn();
42
- } catch (error) {
43
- if (error instanceof ConfigError) throw error;
44
- let msg = String(error);
45
- if (error instanceof Error) msg = error.message;
46
- throw new ConfigError(`${filePath}: ${msg}`);
47
- }
48
- };
49
- //#endregion
50
- //#region src/core/contracts/paths.ts
51
- const isRelPath = (value) => typeof value === "string" && value.length > 0 && !value.includes("\0") && !/^[\\/]/u.test(value) && !/^[a-z]:/iu.test(value) && !value.split(/[\\/]/u).includes("..");
52
- const asRelPath = (value) => {
53
- if (!isRelPath(value)) throw new TypeError("Expected a repository-relative path without parent traversal.");
54
- return value;
55
- };
56
- //#endregion
57
- //#region src/adapters/node-paths.ts
58
- const isAbsPath = (value) => typeof value === "string" && !value.includes("\0") && path.isAbsolute(value);
59
- const asAbsPath = (value) => {
60
- if (!isAbsPath(value)) throw new TypeError("Expected an absolute filesystem path.");
61
- return value;
62
- };
63
- const nodePaths = {
64
- isAbsolute: isAbsPath,
65
- resolve(root, relative) {
66
- if (typeof relative !== "string" || relative.includes("\0") || path.isAbsolute(relative) || relative.split(path.sep === "\\" ? /[\\/]/u : /\//u).includes("..")) throw new ConfigError(`Invalid repository-relative path: ${String(relative)}`);
67
- const resolved = path.join(root, relative);
68
- const within = path.relative(root, resolved);
69
- if (within === ".." || within.startsWith(`..${path.sep}`) || path.isAbsolute(within)) throw new ConfigError("Path escapes repository root.");
70
- return asAbsPath(resolved);
71
- },
72
- child(parent, name) {
73
- if (typeof name !== "string" || !name || name === "." || name === ".." || name.includes("\0") || name.includes("/") || path.sep === "\\" && (name.includes("\\") || name.includes(":"))) throw new ConfigError(`FileSystem.readDirectories returned an invalid child directory name: ${String(name)}`);
74
- return parent === "." ? name : `${parent}/${name}`;
75
- }
76
- };
77
- //#endregion
78
- //#region src/adapters/node-errors.ts
79
- /** Only the Error and code properties are established by this guard. */
80
- const isNodeError = (value) => value instanceof Error && "code" in value && typeof value.code === "string";
81
- //#endregion
82
- //#region src/adapters/node-file-system.ts
83
- const assertRegularFile = (filePath) => {
84
- if (!statSync(filePath).isFile()) throw new ConfigError(`${filePath}: expected a regular file.`);
85
- };
86
- const nodeFileSystem = {
87
- readDirectories(directory) {
88
- return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
89
- },
90
- exists(filePath) {
91
- try {
92
- assertRegularFile(filePath);
93
- return true;
94
- } catch (error) {
95
- if (isNodeError(error) && error.code === "ENOENT") return false;
96
- throw error;
97
- }
98
- },
99
- readText(filePath) {
100
- try {
101
- assertRegularFile(filePath);
102
- return readFileSync(filePath, "utf8");
103
- } catch (error) {
104
- if (isNodeError(error) && error.code === "ENOENT") return;
105
- throw error;
106
- }
107
- }
108
- };
109
- const resolveIn = (root, relPath) => nodePaths.resolve(root, relPath);
110
- const assertDirectory = (root) => {
111
- if (!statSync(root).isDirectory()) throw new UsageError("The lint target must be a directory.");
112
- };
113
- //#endregion
114
- //#region package.json
115
- var version$1 = "0.6.0";
116
- var engines = { "node": "^22.18.0 || ^24.0.0" };
117
- //#endregion
118
- //#region src/version.ts
119
- const version = version$1;
120
- const SUPPORTED_NODE_RANGE = engines.node;
121
- //#endregion
122
- //#region src/adapters/node-version.ts
123
- const isSupportedNodeVersion = (version) => satisfies(version, SUPPORTED_NODE_RANGE);
124
- //#endregion
125
- //#region src/core/contracts/records.ts
126
- /** Configuration maps are ordinary objects or null-prototype dictionaries. */
127
- const isPlainRecord = (value) => {
128
- if (typeof value !== "object" || value === null) return false;
129
- const prototype = Object.getPrototypeOf(value);
130
- return prototype === Object.prototype || prototype === null;
131
- };
132
- //#endregion
133
- //#region src/core/contracts/pms.ts
134
- /**
135
- * Single source of truth for package managers and severities.
136
- * Types, runtime sets, and ordering derive from these tuples. Adding a manager
137
- * still requires local detection signals, applicable rule bindings and verified
138
- * policy/availability evidence; the tuple alone does not implement support.
139
- */
140
- const PMS = [
141
- "npm",
142
- "pnpm",
143
- "yarn",
144
- "bun",
145
- "deno",
146
- "aube"
147
- ];
148
- const SEVERITIES = [
149
- "error",
150
- "warn",
151
- "info"
152
- ];
153
- /**
154
- * Numeric ordering of severities for threshold comparisons. Co-located with
155
- * `Severity` so `filter`, `runLint`, and any future severity-aware code share
156
- * one source of truth — drifting the order in one place silently changes the
157
- * fail-on-threshold contract everywhere.
158
- */
159
- const SEVERITY_RANK = {
160
- error: 3,
161
- info: 1,
162
- warn: 2
163
- };
164
- const PM_SET = new Set(PMS);
165
- const SEVERITY_SET = new Set(SEVERITIES);
166
- const isPM = (value) => PM_SET.has(value);
167
- const isSeverity = (value) => SEVERITY_SET.has(value);
168
- const PACKAGE_MANAGER_FIELD = /^(?<pmName>[a-z]+)(?:@.+)?$/u;
169
- const parsePackageManagerField = (value) => {
170
- const name = PACKAGE_MANAGER_FIELD.exec(value.trim())?.groups?.pmName;
171
- if (typeof name !== "undefined" && isPM(name)) return name;
172
- };
173
- //#endregion
174
- //#region src/core/pm-versions.ts
175
- /** Release history establishes stable versions, not ranges or prerelease builds. */
176
- const isStableVersion = (value) => {
177
- if (typeof value !== "string" || !/^\d/u.test(value) || value.trim() !== value) return false;
178
- const version = parse(value);
179
- return version !== null && version.prerelease.length === 0;
180
- };
181
- /** Keep name-only detection independent from whether the declaration pins a version. */
182
- const declaredPMVersion = (declaration) => {
183
- const [pm, version, ...extra] = declaration?.trim().split("@") ?? [];
184
- return pm && isPM(pm) && isStableVersion(version) && extra.length === 0 ? { [pm]: version } : {};
185
- };
186
- //#endregion
187
- //#region src/core/inspection-options.ts
188
- const denseArray = (value, name) => {
189
- if (!Array.isArray(value)) throw new ConfigError(`${name} must be a dense array.`);
190
- for (let index = 0; index < value.length; index += 1) if (!Object.hasOwn(value, index)) throw new ConfigError(`${name} must be a dense array.`);
191
- return value;
192
- };
193
- const relativeInput = (value, name) => {
194
- if (typeof value !== "string" || !value || value.includes("\0") || name === "exclude" && (value.includes("\\") || /^[a-z]:/iu.test(value)) || /^[\\/]/u.test(value) || /^[a-z]:[\\/]/iu.test(value) || value.split("/").includes("..")) throw new ConfigError(`${name}: expected a nonempty cwd-relative /-separated path without parent traversal.`);
195
- return value.split("/").filter((part) => part && part !== ".").join("/") || ".";
196
- };
197
- const parseExcludes = (value) => [...new Set(denseArray(value, "exclude").map((input) => {
198
- const pattern = relativeInput(input, "exclude");
199
- if (pattern === "." || pattern.startsWith("!")) throw new ConfigError("exclude cannot exclude cwd (.) or re-include with leading !.");
200
- return pattern;
201
- }))];
202
- const parseInstallationRoots = (value) => {
203
- const roots = /* @__PURE__ */ new Map();
204
- for (const input of denseArray(value, "installationRoots")) {
205
- if (typeof input !== "string" && (!isPlainRecord(input) || Object.keys(input).some((key) => ![
206
- "path",
207
- "pm",
208
- "pmVersion"
209
- ].includes(key)))) throw new ConfigError("installationRoots entries must be paths or { path, pm?, pmVersion? }.");
210
- const entry = typeof input === "string" ? { path: input } : input;
211
- const path = relativeInput(entry.path, "installationRoots");
212
- if (entry.pm !== void 0 && (typeof entry.pm !== "string" || !isPM(entry.pm)) || entry.pmVersion !== void 0 && (!entry.pm || typeof entry.pmVersion !== "string" || !isStableVersion(entry.pmVersion))) throw new ConfigError(`${path}: pmVersion requires pm and an exact stable version; pm must be a supported manager.`);
213
- if (path === "." && (entry.pm !== void 0 || entry.pmVersion !== void 0)) throw new ConfigError("Use top-level pm / pmVersion / pms / pmVersions for installation root \".\".");
214
- const root = {
215
- path,
216
- pm: entry.pm,
217
- pmVersion: entry.pmVersion
218
- };
219
- const previous = roots.get(path);
220
- if (previous && (previous.pm !== root.pm || previous.pmVersion !== root.pmVersion)) throw new ConfigError(`${path}: conflicting installationRoots entries.`);
221
- roots.set(path, root);
222
- }
223
- return [...roots.values()];
224
- };
225
- //#endregion
226
- //#region src/core/contracts/project-type.ts
227
- const PROJECT_TYPES = ["application", "package"];
228
- const isProjectType = (value) => PROJECT_TYPES.some((projectType) => projectType === value);
229
- //#endregion
230
- //#region src/core/contracts/reporter.ts
231
- const isReporterShape = (value) => {
232
- return isPlainRecord(value) && typeof value.name === "string" && typeof value.format === "function";
233
- };
234
- //#endregion
235
- //#region src/core/contracts/config-value.ts
236
- const CODEC_KINDS = [
237
- "json",
238
- "npmrc",
239
- "toml",
240
- "yaml"
241
- ];
242
- /** Read an own key path; absent or non-mapping parents yield undefined. */
243
- const getByPath = (config, keyPath) => {
244
- let current = config;
245
- for (const key of keyPath) {
246
- if (!isPlainRecord(current)) return;
247
- if (!Object.hasOwn(current, key)) return;
248
- current = current[key];
249
- }
250
- return current;
251
- };
252
- /** Accept only mapping roots while preserving unvalidated values inside them. */
253
- const toParsedConfig = (value) => {
254
- if (isPlainRecord(value)) return value;
255
- throw new TypeError("Config root must be a mapping.");
256
- };
257
- //#endregion
258
- //#region src/core/contracts/config-file-ref.ts
259
- const CONFIG_FILE_KINDS = new Set(CODEC_KINDS);
260
- const isConfigFileRefShape = (value) => {
261
- if (!isPlainRecord(value) || typeof value.kind !== "string" || !isRelPath(value.path)) return false;
262
- return CONFIG_FILE_KINDS.has(value.kind);
263
- };
264
- //#endregion
265
- //#region src/core/contracts/rule.ts
266
- const defineRule = (rule) => rule;
267
- const isOptionalString = (value) => typeof value === "undefined" || typeof value === "string";
268
- const isConfigValueShape = (value) => typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
269
- const isDenseStringArray = (value) => Array.isArray(value) && Array.from(value).every((item) => typeof item === "string");
270
- const isCheckStatusShape = (value) => {
271
- if (!isPlainRecord(value)) return false;
272
- if (value.state === "ok" || value.state === "na") return true;
273
- if (value.state === "violations") return Array.isArray(value.violations) && value.violations.length > 0 && Array.from(value.violations).every(isViolationStatusShape);
274
- return isViolationStatusShape(value);
275
- };
276
- const isViolationStatusShape = (value) => {
277
- if (!isPlainRecord(value)) return false;
278
- return value.state === "violation" && typeof value.message === "string" && (value.file === void 0 || isRelPath(value.file)) && (value.expected === void 0 || isConfigValueShape(value.expected)) && (value.severity === void 0 || typeof value.severity === "string" && isSeverity(value.severity)) && !("manualSteps" in value) && !("fix" in value) && !("fixable" in value) && (value.remediation === void 0 || isRemediationShape(value.remediation));
279
- };
280
- const isSetKeyOperation = (value) => isPlainRecord(value) && value.op === "setKey" && isConfigFileRefShape(value.file) && isDenseStringArray(value.keyPath) && value.keyPath.length > 0 && isConfigValueShape(value.value);
281
- const isRemediationShape = (value) => {
282
- if (!isPlainRecord(value)) return false;
283
- if (value.kind === "manual") return !("operations" in value) && isDenseStringArray(value.steps) && value.steps.length > 0;
284
- return value.kind === "automatic" && !("steps" in value) && Array.isArray(value.operations) && value.operations.length > 0 && Array.from(value.operations).every(isSetKeyOperation);
285
- };
286
- const isVersionNoteShape = (value) => typeof value === "undefined" || isPlainRecord(value) && isOptionalString(value.configAvailableSince) && isOptionalString(value.defaultSafeSince) && isOptionalString(value.note);
287
- const isRuleBindingShape = (value) => {
288
- if (!isPlainRecord(value)) return false;
289
- return (value.file === void 0 || isConfigFileRefShape(value.file)) && !("fix" in value) && !("fixKind" in value) && !("fileGlob" in value) && isOptionalString(value.docs) && (typeof value.severity === "undefined" || typeof value.severity === "string" && isSeverity(value.severity)) && isVersionNoteShape(value.versionNote) && typeof value.check === "function";
290
- };
291
- const isBindingsShape = (value) => isPlainRecord(value) && Object.keys(value).every(isPM) && PMS.every((pm) => value[pm] === void 0 || isRuleBindingShape(value[pm]));
292
- const isProjectTypeValue = (value) => typeof value === "string" && isProjectType(value);
293
- const isProjectTypesShape = (value) => typeof value === "undefined" || Array.isArray(value) && Array.from(value).every((projectType) => isProjectTypeValue(projectType));
294
- const isRuleShape = (value) => {
295
- if (!isPlainRecord(value)) return false;
296
- return typeof value.id === "string" && typeof value.title === "string" && typeof value.description === "string" && typeof value.severity === "string" && isSeverity(value.severity) && isOptionalString(value.docs) && isProjectTypesShape(value.projectTypes) && isBindingsShape(value.bindings);
297
- };
298
- //#endregion
299
- //#region src/core/contracts/synchronous.ts
300
- /** Reject only the submitted async value, without a process-wide rejection handler. */
301
- const assertSynchronous = (value, label) => {
302
- if (value !== null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function") {
303
- Promise.resolve(value).catch(() => {});
304
- throw new ConfigError(`${label} must be synchronous; Promise/thenable values are not supported.`);
305
- }
306
- };
307
- //#endregion
308
- //#region src/core/parse-siro-config.ts
309
- const RuleSettingSchema = vb.union([vb.picklist(SEVERITIES), vb.literal("off")]);
310
- const ConfigSchema = vb.strictObject({
311
- exclude: vb.optional(vb.custom((value) => {
312
- parseExcludes(value);
313
- return true;
314
- })),
315
- installationRoots: vb.optional(vb.custom((value) => {
316
- parseInstallationRoots(value);
317
- return true;
318
- })),
319
- customRules: vb.optional(vb.array(vb.custom(isRuleShape, "must be a structurally valid rule"))),
320
- pms: vb.optional(vb.pipe(vb.array(vb.picklist(PMS)), vb.minLength(1, "must not be empty (omit the key to auto-detect, or list at least one PM)"))),
321
- projectType: vb.optional(vb.picklist(PROJECT_TYPES)),
322
- pmVersions: vb.optional(vb.pipe(vb.custom(isPlainRecord, "must be an object of PM versions"), vb.check((value) => Object.keys(value).every(isPM), "unknown package manager in pmVersions"), vb.record(vb.picklist(PMS), vb.custom(isStableVersion, "must be an exact stable version such as 10.16.0")))),
323
- reporters: vb.optional(vb.array(vb.custom(isReporterShape, "must be a { name, format } reporter"))),
324
- rules: vb.optional(vb.pipe(vb.custom(isPlainRecord, "must be an object of rule settings"), vb.rawTransform(({ dataset, addIssue }) => {
325
- const entries = [];
326
- for (const [key, value] of Object.entries(dataset.value)) {
327
- const result = vb.safeParse(RuleSettingSchema, value);
328
- if (result.success) entries.push([key, result.output]);
329
- else addIssue({
330
- message: result.issues[0].message,
331
- path: [{
332
- input: dataset.value,
333
- key,
334
- origin: "value",
335
- type: "object",
336
- value
337
- }]
338
- });
339
- }
340
- return Object.fromEntries(entries);
341
- })))
342
- }, "unknown config key (check for a typo)");
343
- const formatIssues = (issues) => issues.map((issue) => {
344
- const keyPath = (issue.path ?? []).map((seg) => seg.key).filter((key) => typeof key === "string" || typeof key === "number").join(".");
345
- if (keyPath) return `${keyPath}: ${issue.message}`;
346
- return issue.message;
347
- }).join("; ");
348
- const parseConfig = (candidate, name = "siro.config") => {
349
- assertSynchronous(candidate, name);
350
- if (!isPlainRecord(candidate)) throw new ConfigError(`${name} must export a config object (got ${Array.isArray(candidate) ? "an array" : typeof candidate}).`);
351
- const result = vb.safeParse(ConfigSchema, Object.fromEntries(Object.entries(candidate)));
352
- if (!result.success) throw new ConfigError(`${name}: ${formatIssues(result.issues)}`);
353
- return result.output;
354
- };
355
- //#endregion
356
- //#region src/load-config.ts
357
- const CONFIG_NAMES = [
358
- "siro.config.ts",
359
- "siro.config.mjs",
360
- "siro.config.js"
361
- ];
362
- const describeError = (error) => error instanceof Error ? error.message : String(error);
363
- let loadCounter = 0;
364
- /** Load the first matching config; executable imports always use the real filesystem. */
365
- const loadConfig = async (cwd, nodeVersion = process.versions.node) => {
366
- const name = CONFIG_NAMES.find((candidate) => nodeFileSystem.exists(asAbsPath(path.join(cwd, candidate))));
367
- if (name === void 0) return;
368
- if (name.endsWith(".ts") && !isSupportedNodeVersion(nodeVersion)) throw new ConfigError(`${name} requires Node.js with native type stripping (${SUPPORTED_NODE_RANGE}); current is v${nodeVersion}. Rename the config to siro.config.mjs (plain JS) or upgrade Node.js.`);
369
- const url = pathToFileURL(path.join(cwd, name));
370
- url.searchParams.set("siro-load", String(++loadCounter));
371
- let mod;
372
- try {
373
- mod = await import(url.href);
374
- } catch (error) {
375
- throw new ConfigError(`Failed to load ${name}: ${describeError(error)}`);
376
- }
377
- return parseConfig(mod !== null && typeof mod === "object" && "default" in mod ? mod.default : mod, name);
378
- };
379
- //#endregion
380
- //#region src/core/config-files.ts
381
- /** Known configuration locations and their parsers. */
382
- const CONFIG_FILES = {
383
- aubeWorkspace: {
384
- kind: "yaml",
385
- path: asRelPath("aube-workspace.yaml")
386
- },
387
- bunfig: {
388
- kind: "toml",
389
- path: asRelPath("bunfig.toml")
390
- },
391
- denoJson: {
392
- kind: "json",
393
- path: asRelPath("deno.json")
394
- },
395
- npmrc: {
396
- kind: "npmrc",
397
- path: asRelPath(".npmrc")
398
- },
399
- packageJson: {
400
- kind: "json",
401
- path: asRelPath("package.json")
402
- },
403
- pnpmWorkspace: {
404
- kind: "yaml",
405
- path: asRelPath("pnpm-workspace.yaml")
406
- },
407
- yarnrc: {
408
- kind: "yaml",
409
- path: asRelPath(".yarnrc.yml")
410
- }
411
- };
412
- //#endregion
413
- //#region src/core/rules/setting-availability.ts
414
- const { npmrc: npmrc$7, packageJson: packageJson$2, pnpmWorkspace: pnpmWorkspace$11, yarnrc: yarnrc$10, bunfig: bunfig$6 } = CONFIG_FILES;
415
- const settingAvailability = [
416
- {
417
- pm: "npm",
418
- file: npmrc$7,
419
- keyPath: ["provenance"],
420
- since: "9.5.0",
421
- source: "https://github.com/npm/cli/releases/tag/v9.5.0"
422
- },
423
- {
424
- pm: "npm",
425
- file: packageJson$2,
426
- keyPath: ["publishConfig", "provenance"],
427
- since: "9.5.0",
428
- source: "https://github.com/npm/cli/releases/tag/v9.5.0"
429
- },
430
- {
431
- pm: "npm",
432
- file: npmrc$7,
433
- keyPath: ["min-release-age"],
434
- since: "11.10.0",
435
- source: "https://github.com/npm/cli/releases/tag/v11.10.0"
436
- },
437
- {
438
- pm: "pnpm",
439
- file: pnpmWorkspace$11,
440
- keyPath: ["strictDepBuilds"],
441
- since: "10.6.0",
442
- source: "https://github.com/pnpm/pnpm/releases/tag/v10.6.0"
443
- },
444
- {
445
- pm: "pnpm",
446
- file: pnpmWorkspace$11,
447
- keyPath: ["dangerouslyAllowAllBuilds"],
448
- since: "10.9.0",
449
- source: "https://github.com/pnpm/pnpm/releases/tag/v10.9.0"
450
- },
451
- {
452
- pm: "pnpm",
453
- file: pnpmWorkspace$11,
454
- keyPath: ["minimumReleaseAge"],
455
- since: "10.16.0",
456
- source: "https://github.com/pnpm/pnpm/releases/tag/v10.16.0"
457
- },
458
- {
459
- pm: "pnpm",
460
- file: pnpmWorkspace$11,
461
- keyPath: ["minimumReleaseAgeExclude"],
462
- since: "10.16.0",
463
- source: "https://github.com/pnpm/pnpm/releases/tag/v10.16.0"
464
- },
465
- {
466
- pm: "pnpm",
467
- file: pnpmWorkspace$11,
468
- keyPath: ["trustPolicy"],
469
- since: "10.21.0",
470
- source: "https://github.com/pnpm/pnpm/releases/tag/v10.21.0"
471
- },
472
- {
473
- pm: "pnpm",
474
- file: pnpmWorkspace$11,
475
- keyPath: ["blockExoticSubdeps"],
476
- since: "10.26.0",
477
- source: "https://github.com/pnpm/pnpm/releases/tag/v10.26.0"
478
- },
479
- {
480
- pm: "pnpm",
481
- file: pnpmWorkspace$11,
482
- keyPath: ["frozenStore"],
483
- since: "11.7.0",
484
- source: "https://github.com/pnpm/pnpm/releases/tag/v11.7.0"
485
- },
486
- {
487
- pm: "yarn",
488
- file: yarnrc$10,
489
- keyPath: ["enableHardenedMode"],
490
- since: "4.0.0",
491
- source: "https://yarnpkg.com/blog/release/4.0"
492
- },
493
- {
494
- pm: "yarn",
495
- file: yarnrc$10,
496
- keyPath: ["npmMinimalAgeGate"],
497
- since: "4.10.0",
498
- source: "https://github.com/yarnpkg/berry/releases/tag/@yarnpkg/cli/4.10.0"
499
- },
500
- {
501
- pm: "yarn",
502
- file: yarnrc$10,
503
- keyPath: ["npmPreapprovedPackages"],
504
- since: "4.10.0",
505
- source: "https://github.com/yarnpkg/berry/releases/tag/@yarnpkg/cli/4.10.0"
506
- },
507
- {
508
- pm: "bun",
509
- file: bunfig$6,
510
- keyPath: ["install", "minimumReleaseAge"],
511
- since: "1.3.0",
512
- source: "https://bun.com/blog/bun-v1.3#minimum-release-age"
513
- },
514
- {
515
- pm: "bun",
516
- file: bunfig$6,
517
- keyPath: [
518
- "install",
519
- "security",
520
- "scanner"
521
- ],
522
- since: "1.2.21",
523
- source: "https://bun.com/blog/bun-v1.2.21#security-scanner-api-for-bun-install"
524
- },
525
- {
526
- pm: "deno",
527
- file: npmrc$7,
528
- keyPath: ["min-release-age"],
529
- since: "2.8.1",
530
- source: "https://github.com/denoland/deno/releases/tag/v2.8.1"
531
- }
532
- ];
533
- //#endregion
534
- //#region src/core/rules/remediation-availability.ts
535
- const sameSetting = (setting, target) => target.file.path === setting.file.path && target.file.kind === setting.file.kind && target.keyPath.length === setting.keyPath.length && target.keyPath.every((key, index) => key === setting.keyPath[index]);
536
- /** Undefined means the target version or the setting's introduction is unknown. */
537
- const settingSupportedByTarget = (pm, version, target) => {
538
- if (version === void 0) return void 0;
539
- const availability = settingAvailability.find((setting) => setting.pm === pm && sameSetting(setting, target));
540
- return availability === void 0 ? void 0 : !lt(version, availability.since);
541
- };
542
- /** Keep a proposal atomic and make known version prerequisites explicit. */
543
- const guardRemediationAvailability = (pm, version, remediation, targets = remediation?.kind === "automatic" ? remediation.operations : []) => {
544
- if (version === void 0 || remediation === void 0) return remediation;
545
- const unsupported = settingAvailability.filter((setting) => setting.pm === pm && lt(version, setting.since) && targets.some((target) => sameSetting(setting, target)));
546
- if (unsupported.length === 0) return remediation;
547
- return {
548
- kind: "manual",
549
- steps: [`Target ${pm} ${version} does not support this proposal: ${unsupported.map((setting) => `${setting.file.path}#${setting.keyPath.join(".")} requires ${pm} >=${setting.since}`).join("; ")}. Verify and upgrade the target before applying the steps below, or choose a supported security control. Removing a setting alone does not provide its protection.`, ...remediation.kind === "manual" ? remediation.steps : remediation.operations.map((operation) => `After upgrading, set ${operation.file.path}#${operation.keyPath.join(".")} to ${JSON.stringify(operation.value)}.`)]
550
- };
551
- };
552
- //#endregion
553
- //#region src/core/rules/remediation.ts
554
- /** Scalar writes must not discard an existing container or replace a non-object parent. */
555
- const proposeChanges = (config, operations) => {
556
- for (const operation of operations) {
557
- let current = config;
558
- for (const key of operation.keyPath) {
559
- if (current === void 0) break;
560
- if (!isPlainRecord(current)) return {
561
- kind: "manual",
562
- steps: [`Review ${operation.file.path}: ${operation.keyPath.join(".")} has a non-object parent. Restructure it before setting the value to ${JSON.stringify(operation.value)}.`]
563
- };
564
- current = Object.hasOwn(current, key) ? current[key] : void 0;
565
- }
566
- if (current !== null && typeof current === "object") return {
567
- kind: "manual",
568
- steps: [`Review ${operation.file.path}: ${operation.keyPath.join(".")} contains nested settings. Preserve the required entries when correcting this value.`]
569
- };
570
- }
571
- return {
572
- kind: "automatic",
573
- operations
574
- };
575
- };
576
- //#endregion
577
- //#region src/core/rules/builders/require-config-key.ts
578
- const overrideBindings = (rule, overrides) => ({
579
- ...rule,
580
- bindings: {
581
- ...rule.bindings,
582
- ...overrides
583
- }
584
- });
585
- const accepts = (spec, actual) => spec.accept ? spec.accept(actual) : actual === spec.value;
586
- const checkKeyValue = (spec, config) => {
587
- const actual = getByPath(config, spec.keyPath);
588
- const coveredByDefault = actual === void 0 && spec.documentedDefault !== void 0 && spec.defaultSafety === "unconditional" && accepts(spec, spec.documentedDefault);
589
- if (!coveredByDefault && accepts(spec, actual)) return { state: "ok" };
590
- const severity = coveredByDefault ? spec.defaultSatisfiedSeverity ?? "info" : void 0;
591
- if (severity === "off") return { state: "ok" };
592
- return {
593
- state: "violation",
594
- actual,
595
- expected: spec.value,
596
- message: spec.message,
597
- ...severity === void 0 ? {} : { severity },
598
- remediation: proposeChanges(config, [{
599
- file: spec.file,
600
- keyPath: spec.keyPath,
601
- op: "setKey",
602
- value: spec.value
603
- }])
604
- };
605
- };
606
- const buildBinding = (spec, pm, applies) => ({
607
- check(ctx, config) {
608
- if (typeof applies !== "undefined" && !applies(ctx)) return { state: "na" };
609
- const status = checkKeyValue(spec, config);
610
- return status.state === "violation" ? {
611
- ...status,
612
- remediation: guardRemediationAvailability(pm, ctx.pmVersion, status.remediation, [spec])
613
- } : status;
614
- },
615
- docs: spec.docs,
616
- file: spec.file,
617
- severity: spec.severity,
618
- versionNote: spec.versionNote
619
- });
620
- /** Build a Rule from a per-PM table of {file, keyPath, value, message}. */
621
- const requireConfigKey = (options) => {
622
- const bindings = {};
623
- for (const pm of PMS) {
624
- const spec = options.bindings[pm];
625
- if (typeof spec !== "undefined") {
626
- if ("extraFix" in spec) throw new TypeError("extraFix is no longer supported; use a custom binding.");
627
- bindings[pm] = buildBinding(spec, pm, options.applies);
628
- }
629
- }
630
- return {
631
- bindings,
632
- description: options.description,
633
- docs: options.docs,
634
- id: options.id,
635
- projectTypes: options.projectTypes,
636
- severity: options.severity,
637
- title: options.title
638
- };
639
- };
640
- //#endregion
641
- //#region src/core/rules/builders/with-aube-paranoid.ts
642
- const withAubeParanoid = (rule) => {
643
- const { aube } = rule.bindings;
644
- if (!aube) return rule;
645
- return overrideBindings(rule, { aube: {
646
- ...aube,
647
- check(ctx, config) {
648
- if (getByPath(config, ["paranoid"]) === true) return { state: "ok" };
649
- return aube.check(ctx, config);
650
- }
651
- } });
652
- };
653
- //#endregion
654
- //#region src/core/rules/advisory-check.ts
655
- const { aubeWorkspace: aubeWorkspace$8 } = CONFIG_FILES;
656
- const advisoryCheck = withAubeParanoid(requireConfigKey({
657
- bindings: { aube: {
658
- accept: (value) => value === "on" || value === "required",
659
- docs: "https://aube.jdx.dev/security.html",
660
- documentedDefault: "on",
661
- defaultSafety: "unconditional",
662
- file: aubeWorkspace$8,
663
- keyPath: ["advisoryCheck"],
664
- message: "Set `advisoryCheck: on` (or `required`) in aube-workspace.yaml to check dependencies against known-malicious advisories.",
665
- value: "on"
666
- } },
667
- description: "Query the OSV database for known-malicious packages during dependency resolution.",
668
- docs: "https://aube.jdx.dev/security.html",
669
- id: "advisory-check",
670
- severity: "warn",
671
- title: "Enable malicious-package advisory checks"
672
- }));
673
- //#endregion
674
- //#region src/core/rules/config-predicates.ts
675
- const isNonBlankString = (value) => typeof value === "string" && value.trim().length > 0;
676
- const isStringList = (value) => Array.isArray(value) && Array.from(value).every(isNonBlankString);
677
- //#endregion
678
- //#region src/core/rules/approved-git-repos.ts
679
- const { yarnrc: yarnrc$9 } = CONFIG_FILES;
680
- const message = "Set `approvedGitRepositories: []` in .yarnrc.yml to block all git: protocol dependencies (or list approved repository globs).";
681
- const approvedGitRepos = defineRule({
682
- bindings: { yarn: {
683
- check(_ctx, config) {
684
- const value = getByPath(config, ["approvedGitRepositories"]);
685
- if (isStringList(value)) return { state: "ok" };
686
- return {
687
- remediation: {
688
- kind: "manual",
689
- steps: ["Add `approvedGitRepositories: []` to .yarnrc.yml to block all git deps, or list specific approved repository URL globs."]
690
- },
691
- actual: value,
692
- expected: "[]",
693
- message,
694
- state: "violation"
695
- };
696
- },
697
- docs: "https://yarnpkg.com/configuration/yarnrc#approvedGitRepositories",
698
- file: yarnrc$9,
699
- versionNote: { configAvailableSince: "yarn 4.14.0" }
700
- } },
701
- description: "Restrict git: protocol dependencies to an explicit allowlist of approved repository URL patterns.",
702
- docs: "https://yarnpkg.com/configuration/yarnrc#approvedGitRepositories",
703
- id: "approved-git-repos",
704
- severity: "warn",
705
- title: "Approve git repository dependencies"
706
- });
707
- //#endregion
708
- //#region src/core/rules/audit-suppression.ts
709
- const { yarnrc: yarnrc$8 } = CONFIG_FILES;
710
- const isNonEmptyArray = (value) => Array.isArray(value) && value.length > 0;
711
- const SUPPRESSION_KEYS = ["npmAuditIgnoreAdvisories", "npmAuditExcludePackages"];
712
- const checkSuppression = (config) => {
713
- const present = SUPPRESSION_KEYS.filter((key) => isNonEmptyArray(getByPath(config, [key])));
714
- const [first] = present;
715
- if (typeof first === "undefined") return { state: "ok" };
716
- return {
717
- actual: getByPath(config, [first]),
718
- message: `Review audit suppression in .yarnrc.yml (${present.join(", ")}). Broad glob patterns can silently hide future vulnerabilities.`,
719
- state: "violation",
720
- remediation: {
721
- kind: "manual",
722
- steps: ["Review entries in `npmAuditIgnoreAdvisories` and `npmAuditExcludePackages` — remove stale suppressions and overly broad glob patterns."]
723
- }
724
- };
725
- };
726
- const auditSuppression = defineRule({
727
- bindings: { yarn: {
728
- check(_ctx, config) {
729
- return checkSuppression(config);
730
- },
731
- docs: "https://yarnpkg.com/configuration/yarnrc#npmAuditIgnoreAdvisories",
732
- file: yarnrc$8
733
- } },
734
- description: "Flag audit advisory suppressions that may silently hide future vulnerabilities via broad glob patterns.",
735
- docs: "https://yarnpkg.com/configuration/yarnrc#npmAuditIgnoreAdvisories",
736
- id: "audit-suppression",
737
- severity: "info",
738
- title: "Review audit suppression entries"
739
- });
740
- //#endregion
741
- //#region src/core/rules/block-auto-install.ts
742
- const { bunfig: bunfig$5 } = CONFIG_FILES;
743
- const blockAutoInstall = requireConfigKey({
744
- bindings: { bun: {
745
- docs: "https://bun.sh/docs/runtime/bunfig#install-auto",
746
- file: bunfig$5,
747
- keyPath: ["install", "auto"],
748
- message: "Set `auto = \"disable\"` under [install] in bunfig.toml to prevent auto-install from bypassing security guards.",
749
- value: "disable",
750
- accept: (value) => value === "disable" || value === false
751
- } },
752
- description: "Disable auto-install so dependencies are only installed through an explicit install step where security guards apply.",
753
- docs: "https://bun.sh/docs/runtime/bunfig#install-auto",
754
- id: "block-auto-install",
755
- severity: "warn",
756
- title: "Block automatic package installation"
757
- });
758
- //#endregion
759
- //#region src/core/rules/block-exotic-subdeps.ts
760
- const { aubeWorkspace: aubeWorkspace$7, npmrc: npmrc$6, pnpmWorkspace: pnpmWorkspace$10 } = CONFIG_FILES;
761
- const isRootOrNone = (value) => value === "root" || value === "none";
762
- const npmMessage = "Set `allow-git=none` and `allow-remote=none` in .npmrc to block git/tarball URLs; use `root` only when direct URL dependencies are required.";
763
- const npmBinding$1 = {
764
- check(_ctx, config) {
765
- const git = getByPath(config, ["allow-git"]);
766
- const remote = getByPath(config, ["allow-remote"]);
767
- const invalid = [git, remote].find((value) => value !== void 0 && !isRootOrNone(value));
768
- if (invalid === void 0 && git !== void 0 && remote !== void 0) return { state: "ok" };
769
- return {
770
- state: "violation",
771
- expected: "none",
772
- ...invalid === void 0 ? {} : { actual: invalid },
773
- message: npmMessage,
774
- remediation: proposeChanges(config, [{
775
- file: npmrc$6,
776
- keyPath: ["allow-git"],
777
- op: "setKey",
778
- value: "none"
779
- }, {
780
- file: npmrc$6,
781
- keyPath: ["allow-remote"],
782
- op: "setKey",
783
- value: "none"
784
- }])
785
- };
786
- },
787
- docs: "https://docs.npmjs.com/cli/v12/using-npm/config#allow-git",
788
- file: npmrc$6,
789
- versionNote: { defaultSafeSince: "npm 12.0.0" }
790
- };
791
- const blockExoticSubdeps = overrideBindings(requireConfigKey({
792
- bindings: {
793
- aube: {
794
- docs: "https://aube.jdx.dev/security.html",
795
- documentedDefault: true,
796
- defaultSafety: "unconditional",
797
- file: aubeWorkspace$7,
798
- keyPath: ["blockExoticSubdeps"],
799
- message: "Set `blockExoticSubdeps: true` in aube-workspace.yaml to block git/tarball URLs in transitive dependencies.",
800
- value: true
801
- },
802
- pnpm: {
803
- docs: "https://pnpm.io/settings/dependency-resolution#blockexoticsubdeps",
804
- documentedDefault: true,
805
- defaultSafety: "conditional",
806
- file: pnpmWorkspace$10,
807
- keyPath: ["blockExoticSubdeps"],
808
- message: "Set `blockExoticSubdeps: true` in pnpm-workspace.yaml to block git/tarball URLs in transitive dependencies.",
809
- value: true,
810
- versionNote: {
811
- configAvailableSince: "pnpm 10.26.0",
812
- defaultSafeSince: "pnpm 10.26.0"
813
- }
814
- }
815
- },
816
- description: "Refuse to install transitive dependencies sourced from git or tarball URLs, which bypass registry integrity checking.",
817
- docs: "https://pnpm.io/settings/dependency-resolution#blockexoticsubdeps",
818
- id: "block-exotic-subdeps",
819
- severity: "warn",
820
- title: "Block exotic subdependencies"
821
- }), { npm: npmBinding$1 });
822
- //#endregion
823
- //#region src/core/rules/bun-security-scanner.ts
824
- const { bunfig: bunfig$4 } = CONFIG_FILES;
825
- const bunSecurityScanner = defineRule({
826
- bindings: { bun: {
827
- check(ctx, config) {
828
- const scanner = getByPath(config, [
829
- "install",
830
- "security",
831
- "scanner"
832
- ]);
833
- if (isNonBlankString(scanner)) return { state: "ok" };
834
- return {
835
- remediation: guardRemediationAvailability("bun", ctx.pmVersion, {
836
- kind: "manual",
837
- steps: ["Add `[install.security] scanner = \"@socketsecurity/bun-security-scanner\"` (or another bun-compatible scanner) to bunfig.toml."]
838
- }, [{
839
- file: bunfig$4,
840
- keyPath: [
841
- "install",
842
- "security",
843
- "scanner"
844
- ]
845
- }]),
846
- actual: scanner,
847
- message: "Configure `[install.security] scanner = \"...\"` in bunfig.toml (e.g. `@socketsecurity/bun-security-scanner`) to scan new packages on install.",
848
- state: "violation"
849
- };
850
- },
851
- docs: "https://bun.com/docs/pm/security-scanner-api",
852
- file: bunfig$4,
853
- versionNote: { configAvailableSince: "bun 1.2.21" }
854
- } },
855
- description: "Bun supports a Security Scanner API that intercepts new packages at install time (e.g. Socket Firewall).",
856
- docs: "https://github.com/bodadotsh/npm-security-best-practices#preinstall-scanners",
857
- id: "bun-security-scanner",
858
- severity: "info",
859
- title: "Enable a bun install-time security scanner"
860
- });
861
- //#endregion
862
- //#region src/core/rules/checksum-verification.ts
863
- const { yarnrc: yarnrc$7 } = CONFIG_FILES;
864
- const checksumVerification = requireConfigKey({
865
- bindings: { yarn: {
866
- docs: "https://yarnpkg.com/configuration/yarnrc#checksumBehavior",
867
- documentedDefault: "throw",
868
- defaultSafety: "unconditional",
869
- file: yarnrc$7,
870
- keyPath: ["checksumBehavior"],
871
- message: "Set `checksumBehavior: throw` in .yarnrc.yml to reject packages whose checksum does not match the lockfile.",
872
- value: "throw"
873
- } },
874
- description: "Throw on checksum mismatches so tampered or corrupted packages are never silently installed.",
875
- docs: "https://yarnpkg.com/configuration/yarnrc#checksumBehavior",
876
- id: "checksum-verification",
877
- severity: "warn",
878
- title: "Verify package checksums"
879
- });
880
- //#endregion
881
- //#region src/core/signals.ts
882
- const PM_SIGNALS = {
883
- aube: {
884
- configs: [CONFIG_FILES.aubeWorkspace.path],
885
- lockfiles: ["aube-lock.yaml"],
886
- reusesLockfiles: [
887
- "pnpm-lock.yaml",
888
- "package-lock.json",
889
- "npm-shrinkwrap.json",
890
- "yarn.lock",
891
- "bun.lock"
892
- ]
893
- },
894
- bun: {
895
- configs: [CONFIG_FILES.bunfig.path],
896
- lockfiles: ["bun.lock", "bun.lockb"]
897
- },
898
- deno: {
899
- configs: [CONFIG_FILES.denoJson.path, "deno.jsonc"],
900
- lockfiles: ["deno.lock"]
901
- },
902
- npm: {
903
- configs: [],
904
- lockfiles: ["package-lock.json", "npm-shrinkwrap.json"]
905
- },
906
- pnpm: {
907
- configs: [CONFIG_FILES.pnpmWorkspace.path],
908
- lockfiles: ["pnpm-lock.yaml"]
909
- },
910
- yarn: {
911
- configs: [CONFIG_FILES.yarnrc.path],
912
- lockfiles: ["yarn.lock"]
913
- }
914
- };
915
- //#endregion
916
- //#region src/core/rules/commit-lockfile.ts
917
- const LOCKFILE_DOCS = {
918
- aube: "https://github.com/aubepkg/aube/blob/main/docs/package-manager/lockfiles.md",
919
- bun: "https://bun.com/docs/install/lockfile",
920
- deno: "https://docs.deno.com/runtime/reference/deno_json/#lockfile",
921
- npm: "https://docs.npmjs.com/cli/v12/configuring-npm/package-lock-json",
922
- pnpm: "https://pnpm.io/git#lockfiles",
923
- yarn: "https://yarnpkg.com/getting-started/qa#should-lockfiles-be-committed-to-the-repository"
924
- };
925
- const LOCKFILE_VERSION_NOTES = { deno: { configAvailableSince: "deno 1.28.0" } };
926
- const lockfileBinding = (pm) => {
927
- const { lockfiles, reusesLockfiles } = PM_SIGNALS[pm];
928
- const [primary] = lockfiles;
929
- const accepted = [...lockfiles, ...reusesLockfiles ?? []];
930
- return {
931
- check(ctx) {
932
- if (pm === "aube" && ctx.exists(asRelPath("bun.lockb")) && !ctx.exists(asRelPath("bun.lock"))) return {
933
- state: "violation",
934
- message: "Aube cannot read bun.lockb, even when another lockfile exists.",
935
- remediation: {
936
- kind: "manual",
937
- steps: ["Run `bun install --save-text-lockfile` to generate bun.lock, then commit it."]
938
- }
939
- };
940
- if (pm === "npm") {
941
- if (ctx.exists(asRelPath(primary))) return { state: "ok" };
942
- if (ctx.exists(asRelPath("npm-shrinkwrap.json"))) {
943
- if (ctx.pmVersion && lt(ctx.pmVersion, "12.0.0")) return { state: "ok" };
944
- return {
945
- state: "violation",
946
- message: ctx.pmVersion ? `npm ${ctx.pmVersion} no longer reads npm-shrinkwrap.json. Use package-lock.json.` : "Found npm-shrinkwrap.json, but the npm target version is unknown; npm 12 no longer reads it.",
947
- remediation: {
948
- kind: "manual",
949
- steps: ["Verify and declare the npm version used by this project. For npm 12 or newer, migrate to package-lock.json and review the resulting dependency resolutions."]
950
- }
951
- };
952
- }
953
- } else if (accepted.some((lf) => ctx.exists(asRelPath(lf)))) return { state: "ok" };
954
- return {
955
- remediation: {
956
- kind: "manual",
957
- steps: [`Install dependencies to generate ${primary}, then commit it.`]
958
- },
959
- message: `No lockfile found. Generate and commit ${primary}.`,
960
- state: "violation"
961
- };
962
- },
963
- docs: LOCKFILE_DOCS[pm],
964
- versionNote: LOCKFILE_VERSION_NOTES[pm]
965
- };
966
- };
967
- const denoBinding$1 = {
968
- file: CONFIG_FILES.denoJson,
969
- docs: LOCKFILE_DOCS.deno,
970
- versionNote: LOCKFILE_VERSION_NOTES.deno,
971
- check(ctx, config) {
972
- const lock = getByPath(config, ["lock"]);
973
- if (lock != null && typeof lock !== "boolean" && typeof lock !== "string" && !(isPlainRecord(lock) && (lock.path == null || typeof lock.path === "string") && (lock.frozen == null || typeof lock.frozen === "boolean"))) return {
974
- state: "violation",
975
- message: "Invalid Deno lock setting.",
976
- remediation: {
977
- kind: "manual",
978
- steps: ["Use a boolean, a lockfile path, or an object with optional path and frozen fields."]
979
- }
980
- };
981
- const selected = typeof lock === "string" ? lock : getByPath(config, ["lock", "path"]);
982
- const lockPath = selected == null ? asRelPath("deno.lock") : selected;
983
- if (lock !== false && isRelPath(lockPath) && ctx.exists(lockPath)) return { state: "ok" };
984
- return {
985
- state: "violation",
986
- message: lock === false ? "Enable the Deno lockfile; `lock: false` disables it even when deno.lock exists." : `No readable project lockfile found at ${String(lockPath)}.`,
987
- remediation: {
988
- kind: "manual",
989
- steps: ["Enable lockfile use in deno.json and generate and commit the configured lockfile within the project."]
990
- }
991
- };
992
- }
993
- };
994
- const commitLockfile = defineRule({
995
- bindings: {
996
- aube: lockfileBinding("aube"),
997
- bun: lockfileBinding("bun"),
998
- deno: denoBinding$1,
999
- npm: lockfileBinding("npm"),
1000
- pnpm: lockfileBinding("pnpm"),
1001
- yarn: lockfileBinding("yarn")
1002
- },
1003
- description: "Lockfiles pin the full dependency tree and integrity hashes, enabling reproducible, verifiable installs (e.g. `npm ci`).",
1004
- docs: "https://github.com/bodadotsh/npm-security-best-practices#3-include-lockfiles",
1005
- id: "commit-lockfile",
1006
- severity: "error",
1007
- title: "Commit a lockfile"
1008
- });
1009
- //#endregion
1010
- //#region src/core/rules/dependency-overrides.ts
1011
- const { aubeWorkspace: aubeWorkspace$6, pnpmWorkspace: pnpmWorkspace$9 } = CONFIG_FILES;
1012
- const isNonEmptyObject$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
1013
- const checkOverrides = (config, file) => {
1014
- const value = getByPath(config, ["overrides"]);
1015
- if (!isNonEmptyObject$2(value)) return { state: "ok" };
1016
- return {
1017
- actual: value,
1018
- message: `Review \`overrides\` in ${file}. Overrides can replace transitive dependencies with arbitrary versions or forks.`,
1019
- state: "violation",
1020
- remediation: {
1021
- kind: "manual",
1022
- steps: ["Review each entry in `overrides` — verify pinned versions address a known CVE and are not redirecting to untrusted packages."]
1023
- }
1024
- };
1025
- };
1026
- const makeBinding = (file, fileName, docs) => ({
1027
- check(_ctx, config) {
1028
- return checkOverrides(config, fileName);
1029
- },
1030
- docs,
1031
- file
1032
- });
1033
- const dependencyOverrides = defineRule({
1034
- bindings: {
1035
- aube: makeBinding(aubeWorkspace$6, "aube-workspace.yaml", "https://aube.sh/settings/"),
1036
- pnpm: makeBinding(pnpmWorkspace$9, "pnpm-workspace.yaml", "https://pnpm.io/settings/dependency-resolution#overrides")
1037
- },
1038
- description: "Flag dependency overrides that can replace transitive packages with arbitrary versions or forks — a supply-chain injection vector.",
1039
- docs: "https://pnpm.io/settings/dependency-resolution#overrides",
1040
- id: "dependency-overrides",
1041
- severity: "info",
1042
- title: "Review dependency overrides"
1043
- });
1044
- //#endregion
1045
- //#region src/core/rules/disable-lifecycle-scripts.ts
1046
- const { npmrc: npmrc$5, pnpmWorkspace: pnpmWorkspace$8, yarnrc: yarnrc$6, aubeWorkspace: aubeWorkspace$5, bunfig: bunfig$3 } = CONFIG_FILES;
1047
- const pnpmBinding$4 = {
1048
- check(ctx, config) {
1049
- if (getByPath(config, ["ignoreScripts"]) === true) return { state: "ok" };
1050
- const bypass = getByPath(config, ["dangerouslyAllowAllBuilds"]);
1051
- const strict = getByPath(config, ["strictDepBuilds"]);
1052
- if (bypass === true) {
1053
- const bypassTarget = {
1054
- file: pnpmWorkspace$8,
1055
- keyPath: ["dangerouslyAllowAllBuilds"]
1056
- };
1057
- if (settingSupportedByTarget("pnpm", ctx.pmVersion, bypassTarget) === false) {
1058
- const proposal = {
1059
- kind: "manual",
1060
- steps: ["Remove `dangerouslyAllowAllBuilds: true` before upgrading to pnpm 10.9.0 or newer; the declared target ignores it, but a later version would activate the bypass.", ...strict === true ? [] : ["Set `strictDepBuilds: true` in pnpm-workspace.yaml to enable lifecycle-script gating for the declared target."]]
1061
- };
1062
- return {
1063
- state: "violation",
1064
- actual: bypass,
1065
- expected: false,
1066
- message: "The declared target ignores this future-version bypass. Removing the ignored bypass alone does not provide protection against lifecycle scripts, and leaving it configured would disable gating after an upgrade.",
1067
- remediation: strict === true ? proposal : guardRemediationAvailability("pnpm", ctx.pmVersion, proposal, [{
1068
- file: pnpmWorkspace$8,
1069
- keyPath: ["strictDepBuilds"]
1070
- }])
1071
- };
1072
- }
1073
- const proposal = {
1074
- kind: "manual",
1075
- steps: ["After upgrading, remove `dangerouslyAllowAllBuilds: true` (or set it to false) and configure lifecycle-script gating."]
1076
- };
1077
- const guarded = guardRemediationAvailability("pnpm", ctx.pmVersion, proposal, [{
1078
- file: pnpmWorkspace$8,
1079
- keyPath: ["strictDepBuilds"]
1080
- }, {
1081
- file: pnpmWorkspace$8,
1082
- keyPath: ["dangerouslyAllowAllBuilds"]
1083
- }]);
1084
- if (guarded !== proposal) return {
1085
- state: "violation",
1086
- actual: bypass,
1087
- expected: false,
1088
- message: "The target cannot use the proposed YAML lifecycle-script controls. Removing an unsupported bypass alone does not provide protection.",
1089
- remediation: guarded
1090
- };
1091
- return {
1092
- actual: bypass,
1093
- expected: false,
1094
- remediation: {
1095
- kind: "manual",
1096
- steps: ["Remove `dangerouslyAllowAllBuilds: true` from pnpm-workspace.yaml (or set it to `false`). Setting `strictDepBuilds: true` alone has no effect while the bypass remains."]
1097
- },
1098
- message: "`dangerouslyAllowAllBuilds: true` in pnpm-workspace.yaml bypasses strictDepBuilds — remove it (or set it to false) to restore lifecycle-script gating.",
1099
- state: "violation"
1100
- };
1101
- }
1102
- if (strict === true) return { state: "ok" };
1103
- return {
1104
- state: "violation",
1105
- actual: strict,
1106
- expected: true,
1107
- message: strict === void 0 ? "Set `strictDepBuilds: true` in pnpm-workspace.yaml to pin lifecycle-script gating across versions." : "Set `strictDepBuilds: true` in pnpm-workspace.yaml to block silent skips of un-approved dep builds.",
1108
- remediation: guardRemediationAvailability("pnpm", ctx.pmVersion, proposeChanges(config, [{
1109
- file: pnpmWorkspace$8,
1110
- keyPath: ["strictDepBuilds"],
1111
- op: "setKey",
1112
- value: true
1113
- }]), [{
1114
- file: pnpmWorkspace$8,
1115
- keyPath: ["strictDepBuilds"]
1116
- }])
1117
- };
1118
- },
1119
- docs: "https://pnpm.io/settings/build#strictdepbuilds",
1120
- file: pnpmWorkspace$8,
1121
- versionNote: {
1122
- configAvailableSince: "pnpm 10.3.0",
1123
- note: "pnpm-workspace.yaml settings require pnpm 10.6.0",
1124
- defaultSafeSince: "pnpm 11.0.0"
1125
- }
1126
- };
1127
- const aubeBinding$2 = {
1128
- check(ctx, config) {
1129
- const npmConfig = ctx.readConfig(npmrc$5);
1130
- const jail = getByPath(config, ["jailBuilds"]);
1131
- const strict = getByPath(npmConfig, ["strictDepBuilds"]);
1132
- if (jail === true && strict === true) return { state: "ok" };
1133
- const violations = [];
1134
- if (jail !== true) violations.push({
1135
- state: "violation",
1136
- file: aubeWorkspace$5.path,
1137
- actual: jail,
1138
- expected: true,
1139
- message: "Set `jailBuilds: true` in aube-workspace.yaml to sandbox approved builds.",
1140
- remediation: proposeChanges(config, [{
1141
- file: aubeWorkspace$5,
1142
- keyPath: ["jailBuilds"],
1143
- op: "setKey",
1144
- value: true
1145
- }])
1146
- });
1147
- if (strict !== true) violations.push({
1148
- state: "violation",
1149
- file: npmrc$5.path,
1150
- actual: strict,
1151
- expected: true,
1152
- message: "Set `strictDepBuilds=true` in .npmrc to reject unreviewed lifecycle scripts.",
1153
- remediation: proposeChanges(npmConfig, [{
1154
- file: npmrc$5,
1155
- keyPath: ["strictDepBuilds"],
1156
- op: "setKey",
1157
- value: true
1158
- }])
1159
- });
1160
- const [first, ...rest] = violations;
1161
- return !first ? { state: "ok" } : rest.length === 0 ? first : {
1162
- state: "violations",
1163
- violations: [first, ...rest]
1164
- };
1165
- },
1166
- docs: "https://aube.jdx.dev/security.html",
1167
- file: aubeWorkspace$5
1168
- };
1169
- const bunMessage = "Set `ignoreScripts = true` under [install] in bunfig.toml — or set `\"trustedDependencies\": []` in package.json — to opt out of the curated allow-list (postinstall is already blocked for untrusted packages by default).";
1170
- const bunBinding = {
1171
- check(ctx, config) {
1172
- const ignoreScripts = getByPath(config, ["install", "ignoreScripts"]);
1173
- if (ignoreScripts === true) return { state: "ok" };
1174
- const trusted = ctx.packageJson?.trustedDependencies;
1175
- if (typeof trusted !== "undefined" && trusted.length === 0) return { state: "ok" };
1176
- return {
1177
- remediation: proposeChanges(config, [{
1178
- file: bunfig$3,
1179
- keyPath: ["install", "ignoreScripts"],
1180
- op: "setKey",
1181
- value: true
1182
- }]),
1183
- actual: ignoreScripts,
1184
- expected: true,
1185
- message: bunMessage,
1186
- state: "violation"
1187
- };
1188
- },
1189
- docs: "https://bun.com/docs/pm/lifecycle",
1190
- file: bunfig$3,
1191
- severity: "info",
1192
- versionNote: { configAvailableSince: "bun 1.2.0" }
1193
- };
1194
- const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigKey({
1195
- bindings: {
1196
- npm: {
1197
- docs: "https://docs.npmjs.com/cli/v11/using-npm/config#ignore-scripts",
1198
- file: npmrc$5,
1199
- keyPath: ["ignore-scripts"],
1200
- message: "Set `ignore-scripts=true` in .npmrc to block dependency lifecycle scripts.",
1201
- value: true
1202
- },
1203
- yarn: {
1204
- docs: "https://yarnpkg.com/configuration/yarnrc#enableScripts",
1205
- documentedDefault: false,
1206
- defaultSafety: "conditional",
1207
- file: yarnrc$6,
1208
- keyPath: ["enableScripts"],
1209
- message: "Set `enableScripts: false` in .yarnrc.yml to pin the policy across versions.",
1210
- value: false,
1211
- versionNote: {
1212
- configAvailableSince: "yarn 2.0.0",
1213
- defaultSafeSince: "yarn 4.14.0"
1214
- }
1215
- }
1216
- },
1217
- description: "Malicious postinstall scripts are a primary supply-chain attack vector. Prevent automatic execution of dependency lifecycle scripts.",
1218
- docs: "https://github.com/bodadotsh/npm-security-best-practices#1-disable-lifecycle-scripts",
1219
- id: "disable-lifecycle-scripts",
1220
- severity: "error",
1221
- title: "Disable dependency lifecycle scripts"
1222
- }), {
1223
- aube: aubeBinding$2,
1224
- bun: bunBinding,
1225
- pnpm: pnpmBinding$4
1226
- }));
1227
- //#endregion
1228
- //#region src/core/rules/enforce-strict-ssl.ts
1229
- const { npmrc: npmrc$4, yarnrc: yarnrc$5 } = CONFIG_FILES;
1230
- const yarnMessage = "Set `enableStrictSsl: true` in .yarnrc.yml to enforce SSL certificate validation for registry connections.";
1231
- const yarnBinding = {
1232
- check(_ctx, config) {
1233
- const strictSsl = getByPath(config, ["enableStrictSsl"]);
1234
- const whitelist = getByPath(config, ["unsafeHttpWhitelist"]);
1235
- if (Array.isArray(whitelist) && whitelist.length > 0) return {
1236
- actual: whitelist,
1237
- expected: "",
1238
- remediation: {
1239
- kind: "manual",
1240
- steps: ["Review and remove entries from `unsafeHttpWhitelist` in .yarnrc.yml. Each entry permits unencrypted HTTP to that hostname. Also set `enableStrictSsl: true`."]
1241
- },
1242
- message: "`unsafeHttpWhitelist` in .yarnrc.yml allows unencrypted HTTP connections — remove entries or clear the list to enforce HTTPS-only registry access.",
1243
- state: "violation"
1244
- };
1245
- if (strictSsl === true) return { state: "ok" };
1246
- return {
1247
- state: "violation",
1248
- actual: strictSsl,
1249
- expected: true,
1250
- message: yarnMessage,
1251
- ...strictSsl === void 0 ? { severity: "info" } : {},
1252
- remediation: proposeChanges(config, [{
1253
- file: yarnrc$5,
1254
- keyPath: ["enableStrictSsl"],
1255
- op: "setKey",
1256
- value: true
1257
- }])
1258
- };
1259
- },
1260
- docs: "https://yarnpkg.com/configuration/yarnrc#enableStrictSsl",
1261
- file: yarnrc$5
1262
- };
1263
- const enforceStrictSsl = overrideBindings(requireConfigKey({
1264
- bindings: { npm: {
1265
- docs: "https://docs.npmjs.com/cli/v11/using-npm/config#strict-ssl",
1266
- documentedDefault: true,
1267
- defaultSafety: "unconditional",
1268
- file: npmrc$4,
1269
- keyPath: ["strict-ssl"],
1270
- message: "Set `strict-ssl=true` in .npmrc to enforce SSL certificate validation.",
1271
- value: true
1272
- } },
1273
- description: "Require SSL certificate validation so registry traffic cannot be intercepted or tampered with.",
1274
- docs: "https://docs.npmjs.com/cli/v11/using-npm/config#strict-ssl",
1275
- id: "enforce-strict-ssl",
1276
- severity: "warn",
1277
- title: "Enforce SSL for registry connections"
1278
- }), { yarn: yarnBinding });
1279
- //#endregion
1280
- //#region src/core/resolve-project-type.ts
1281
- const isPublishableName = (value) => typeof value === "string" && value.trim().length > 0;
1282
- const resolveProjectType = (selected, inferredPackage) => selected ?? (inferredPackage ? "package" : "application");
1283
- const resolvePackageJsonProjectType = (ctx) => {
1284
- const pkg = ctx.packageJson;
1285
- const inferredPackage = pkg?.private !== true && isPublishableName(pkg?.name);
1286
- return resolveProjectType(ctx.projectType, inferredPackage);
1287
- };
1288
- const resolveDenoProjectType = (ctx, denoConfig) => resolveProjectType(ctx.projectType, getByPath(denoConfig, ["publish"]) !== false && isPublishableName(getByPath(denoConfig, ["name"])));
1289
- //#endregion
1290
- //#region src/core/rules/publishable.ts
1291
- const isPublishable = (ctx) => resolvePackageJsonProjectType(ctx) === "package";
1292
- //#endregion
1293
- //#region src/core/rules/files-field.ts
1294
- const { packageJson: packageJson$1, denoJson: denoJson$3 } = CONFIG_FILES;
1295
- const packageJsonFilesBinding = {
1296
- check(ctx) {
1297
- if (!isPublishable(ctx)) return { state: "na" };
1298
- const files = ctx.packageJson?.files;
1299
- if (isStringList(files) && files.length > 0) return { state: "ok" };
1300
- return {
1301
- remediation: {
1302
- kind: "manual",
1303
- steps: ["Add a `files` array to package.json (e.g. [\"dist\"]) and verify with `npm pack --dry-run`."]
1304
- },
1305
- actual: files,
1306
- message: "Add a `files` allow-list to package.json to limit what gets published.",
1307
- state: "violation"
1308
- };
1309
- },
1310
- docs: "https://docs.npmjs.com/cli/v11/configuring-npm/package-json#files",
1311
- file: packageJson$1
1312
- };
1313
- const denoPublishBinding = {
1314
- check(ctx, config) {
1315
- if (resolveDenoProjectType(ctx, config) !== "package") return { state: "na" };
1316
- const include = getByPath(config, ["publish", "include"]);
1317
- if (isStringList(include) && include.length > 0) return { state: "ok" };
1318
- return {
1319
- remediation: {
1320
- kind: "manual",
1321
- steps: ["Add a non-empty `publish.include` array to deno.json; use `publish.exclude` for additional exclusions."]
1322
- },
1323
- actual: include,
1324
- message: "Add `publish.include` to deno.json to limit what gets published.",
1325
- state: "violation"
1326
- };
1327
- },
1328
- docs: "https://docs.deno.com/runtime/reference/deno_json/#publish---override-.gitignore",
1329
- file: denoJson$3
1330
- };
1331
- const filesField = defineRule({
1332
- bindings: {
1333
- aube: packageJsonFilesBinding,
1334
- bun: packageJsonFilesBinding,
1335
- deno: denoPublishBinding,
1336
- npm: packageJsonFilesBinding,
1337
- pnpm: packageJsonFilesBinding,
1338
- yarn: packageJsonFilesBinding
1339
- },
1340
- description: "An explicit `files` array in package.json restricts what gets published, preventing accidental inclusion of secrets or local files.",
1341
- docs: "https://github.com/bodadotsh/npm-security-best-practices#12-review-published-files",
1342
- id: "files-field",
1343
- projectTypes: ["package"],
1344
- severity: "info",
1345
- title: "Declare a published files allow-list"
1346
- });
1347
- //#endregion
1348
- //#region src/core/rules/frozen-lockfile.ts
1349
- const { pnpmWorkspace: pnpmWorkspace$7, yarnrc: yarnrc$4, bunfig: bunfig$2, denoJson: denoJson$2 } = CONFIG_FILES;
1350
- const aubeFrozenCommand = "Use `aube ci` or `aube install --frozen-lockfile` to fail when the lockfile would change.";
1351
- const frozenLockfile = overrideBindings(requireConfigKey({
1352
- bindings: {
1353
- deno: {
1354
- docs: "https://docs.deno.com/runtime/reference/deno_json/#lockfile",
1355
- file: denoJson$2,
1356
- keyPath: ["lock", "frozen"],
1357
- message: "Set `lock.frozen: true` in deno.json for reproducible, verified installs.",
1358
- value: true
1359
- },
1360
- bun: {
1361
- docs: "https://bun.com/docs/runtime/bunfig#install-frozenlockfile",
1362
- file: bunfig$2,
1363
- keyPath: ["install", "frozenLockfile"],
1364
- message: "Set `frozenLockfile = true` under [install] in bunfig.toml so installs fail when the lockfile would change.",
1365
- value: true,
1366
- versionNote: { configAvailableSince: "bun 0.6.10" }
1367
- },
1368
- pnpm: {
1369
- docs: "https://pnpm.io/cli/install#--frozen-lockfile",
1370
- file: pnpmWorkspace$7,
1371
- keyPath: ["frozenLockfile"],
1372
- message: "pnpm defaults frozenLockfile to true in CI — set it explicitly in pnpm-workspace.yaml to pin the policy outside CI too.",
1373
- value: true
1374
- },
1375
- yarn: {
1376
- docs: "https://yarnpkg.com/configuration/yarnrc#enableImmutableInstalls",
1377
- documentedDefault: true,
1378
- defaultSafety: "conditional",
1379
- file: yarnrc$4,
1380
- keyPath: ["enableImmutableInstalls"],
1381
- message: "Set `enableImmutableInstalls: true` in .yarnrc.yml to pin the policy outside CI too.",
1382
- value: true,
1383
- versionNote: {
1384
- configAvailableSince: "yarn 2.0.0",
1385
- defaultSafeSince: "yarn 3.0.0 in CI"
1386
- }
1387
- }
1388
- },
1389
- description: "Refuse to mutate the lockfile on install so unexpected dependency changes fail loudly.",
1390
- docs: "https://github.com/bodadotsh/npm-security-best-practices#3-include-lockfiles",
1391
- id: "frozen-lockfile",
1392
- severity: "warn",
1393
- title: "Freeze the lockfile"
1394
- }), { aube: {
1395
- check() {
1396
- return {
1397
- remediation: {
1398
- kind: "manual",
1399
- steps: [aubeFrozenCommand]
1400
- },
1401
- message: "`preferFrozenLockfile` only reuses an up-to-date lockfile; it does not prevent updates. Verify that install commands enforce a frozen lockfile.",
1402
- state: "violation"
1403
- };
1404
- },
1405
- docs: "https://github.com/aubepkg/aube/blob/main/docs/cli/ci.md",
1406
- severity: "info"
1407
- } });
1408
- //#endregion
1409
- //#region src/core/rules/frozen-store.ts
1410
- const { pnpmWorkspace: pnpmWorkspace$6 } = CONFIG_FILES;
1411
- const frozenStore = defineRule({
1412
- bindings: { pnpm: {
1413
- check(ctx, config) {
1414
- const value = getByPath(config, ["frozenStore"]);
1415
- if (value === true) return { state: "ok" };
1416
- return {
1417
- remediation: guardRemediationAvailability("pnpm", ctx.pmVersion, {
1418
- kind: "manual",
1419
- steps: ["Populate the store before enabling `frozenStore: true`. Use it for read-only deployments; it is incompatible with --force and a configured pnpr server."]
1420
- }, [{
1421
- file: pnpmWorkspace$6,
1422
- keyPath: ["frozenStore"]
1423
- }]),
1424
- actual: value,
1425
- message: "For a pre-populated read-only store, consider `frozenStore` in pnpm-workspace.yaml. Normal installs need a writable store.",
1426
- state: "violation"
1427
- };
1428
- },
1429
- docs: "https://pnpm.io/settings/store#frozenstore",
1430
- file: pnpmWorkspace$6,
1431
- versionNote: { configAvailableSince: "pnpm 11.7.0" }
1432
- } },
1433
- description: "Consider read-only store access for deployments whose dependencies are already present.",
1434
- docs: "https://pnpm.io/settings/store#frozenstore",
1435
- id: "frozen-store",
1436
- severity: "info",
1437
- title: "Consider a pre-populated read-only store"
1438
- });
1439
- //#endregion
1440
- //#region src/core/rules/hardened-mode.ts
1441
- const { yarnrc: yarnrc$3 } = CONFIG_FILES;
1442
- const hardenedMode = requireConfigKey({
1443
- bindings: { yarn: {
1444
- docs: "https://yarnpkg.com/configuration/yarnrc#enableHardenedMode",
1445
- documentedDefault: true,
1446
- defaultSafety: "conditional",
1447
- file: yarnrc$3,
1448
- keyPath: ["enableHardenedMode"],
1449
- message: "yarn enables hardened mode automatically only for pull requests on public repositories — set `enableHardenedMode: true` in .yarnrc.yml to extend end-to-end install verification to every install.",
1450
- value: true,
1451
- versionNote: {
1452
- configAvailableSince: "yarn 4.0.0",
1453
- defaultSafeSince: "yarn 4.0.0 (conditional: auto-enabled for PRs on public repositories)"
1454
- }
1455
- } },
1456
- description: "Yarn 4's enableHardenedMode performs end-to-end checksum, lockfile, and version verification at install time.",
1457
- docs: "https://yarnpkg.com/configuration/yarnrc#enableHardenedMode",
1458
- id: "hardened-mode",
1459
- severity: "warn",
1460
- title: "Enable hardened mode (yarn)"
1461
- });
1462
- //#endregion
1463
- //#region src/core/rules/deno-release-age.ts
1464
- const DENO_DURATION = /^\+?P(?:\+?\d+[Ww]|(?:\d+[Dd])*(?:T(?:\d+[HhMm]|\d+(?:\.\d+)?[Ss])+)?)$/u;
1465
- const DENO_MIN_TIMESTAMP = Date.UTC(-262143, 0, 1);
1466
- const DENO_UNIT_SECONDS = {
1467
- w: 604800,
1468
- d: 86400,
1469
- h: 3600,
1470
- m: 60,
1471
- s: 1
1472
- };
1473
- const DENO_DATE = /^\d{4}-\d{2}-\d{2}$/u;
1474
- const DENO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}[Tt ](?:[01]\d|2[0-3]):[0-5]\d:(?:[0-5]\d|60)(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/u;
1475
- const DENO_OFFSET_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d(?::(?:[0-5]\d|60))?[+-](?:[01]\d|2[0-3]):?[0-5]\d$/u;
1476
- const DENO_DURATION_TOKEN = /(\d+)(?:\.(\d+))?([WDHMS])/giu;
1477
- const LEAP_SECOND = /:60(?=\.|Z|z|[+-])/u;
1478
- const isPositiveDenoSeconds = (seconds, now) => seconds > 0 && Number.isFinite(seconds) && now - seconds * 1e3 >= DENO_MIN_TIMESTAMP;
1479
- const durationSeconds = (value) => {
1480
- let seconds = 0;
1481
- for (const [, integer, fraction, unit] of value.matchAll(DENO_DURATION_TOKEN)) {
1482
- const amount = Number(integer) + Number(`0.${(fraction ?? "").slice(0, 9) || "0"}`);
1483
- seconds += amount * (DENO_UNIT_SECONDS[unit?.toLowerCase() ?? ""] ?? 0);
1484
- }
1485
- return seconds;
1486
- };
1487
- /** Match Deno's minimumDependencyAge value grammar and active-cutoff semantics. */
1488
- const isActiveDenoReleaseAge = (value, now, parse) => {
1489
- if (typeof value === "number") return Number.isSafeInteger(value) && isPositiveDenoSeconds(value * 60, now);
1490
- if (typeof value !== "string") return false;
1491
- if (/^\d+$/u.test(value)) return Number.isSafeInteger(Number(value)) && isPositiveDenoSeconds(Number(value) * 60, now);
1492
- if (DENO_DURATION.test(value)) return isPositiveDenoSeconds(durationSeconds(value), now);
1493
- if (!DENO_DATE.test(value) && !DENO_TIMESTAMP.test(value) && !DENO_OFFSET_TIMESTAMP.test(value)) return false;
1494
- const date = value.slice(0, 10);
1495
- const midnight = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
1496
- const leapSecond = LEAP_SECOND.test(value);
1497
- const timestamp = parse(value.replace(LEAP_SECOND, ":59")) + (leapSecond ? 1e3 : 0);
1498
- return !Number.isNaN(midnight.valueOf()) && midnight.toISOString().slice(0, 10) === date && timestamp < now;
1499
- };
1500
- //#endregion
1501
- //#region src/core/rules/minimum-release-age.ts
1502
- const HOURS_PER_DAY = 24;
1503
- const MINUTES_PER_HOUR = 60;
1504
- const SECONDS_PER_MINUTE = 60;
1505
- const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
1506
- const SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE;
1507
- const RECOMMENDED_RELEASE_AGE_DAYS = 3;
1508
- const RECOMMENDED_RELEASE_AGE_MINUTES = RECOMMENDED_RELEASE_AGE_DAYS * MINUTES_PER_DAY;
1509
- const RECOMMENDED_RELEASE_AGE_SECONDS = RECOMMENDED_RELEASE_AGE_DAYS * SECONDS_PER_DAY;
1510
- const DOCUMENTED_DEFAULT_MINUTES = MINUTES_PER_DAY;
1511
- const { npmrc: npmrc$3, pnpmWorkspace: pnpmWorkspace$5, yarnrc: yarnrc$2, bunfig: bunfig$1, denoJson: denoJson$1, aubeWorkspace: aubeWorkspace$4 } = CONFIG_FILES;
1512
- const isPositiveNumber = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
1513
- const isPositiveYarnDuration = (value) => isPositiveNumber(value) || typeof value === "string" && /^\d*\.?\d+(?:ms|s|m|h|d|w)?$/u.test(value) && Number.isFinite(Number.parseFloat(value)) && Number.parseFloat(value) > 0;
1514
- const isNonDisabledDenoReleaseAge = (value, now, parse) => {
1515
- if (!isPlainRecord(value)) return isActiveDenoReleaseAge(value, now, parse);
1516
- if (Object.keys(value).some((key) => key !== "age" && key !== "exclude")) return false;
1517
- if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
1518
- return value.age != null && isActiveDenoReleaseAge(value.age, now, parse);
1519
- };
1520
- const denoAgeUsesFallback = (value) => {
1521
- if (value === void 0 || value === null) return true;
1522
- if (!isPlainRecord(value)) return false;
1523
- if (Object.keys(value).some((key) => key !== "age" && key !== "exclude")) return false;
1524
- if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
1525
- return value.age == null;
1526
- };
1527
- const isPositiveDenoNpmrcDays = (value, now, parse) => typeof value === "number" && Number.isSafeInteger(value) && isActiveDenoReleaseAge(value * MINUTES_PER_DAY, now, parse);
1528
- const baseRule$2 = requireConfigKey({
1529
- bindings: {
1530
- aube: {
1531
- accept: isPositiveNumber,
1532
- docs: "https://aube.sh/settings/",
1533
- documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
1534
- defaultSafety: "unconditional",
1535
- file: aubeWorkspace$4,
1536
- keyPath: ["minimumReleaseAge"],
1537
- message: `Set minimumReleaseAge (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in aube-workspace.yaml.`,
1538
- value: RECOMMENDED_RELEASE_AGE_MINUTES
1539
- },
1540
- bun: {
1541
- accept: isPositiveNumber,
1542
- docs: "https://bun.com/docs/runtime/bunfig#install-minimumreleaseage",
1543
- file: bunfig$1,
1544
- keyPath: ["install", "minimumReleaseAge"],
1545
- message: `Set install.minimumReleaseAge (bun) to ~${RECOMMENDED_RELEASE_AGE_SECONDS} seconds to quarantine brand-new releases.`,
1546
- value: RECOMMENDED_RELEASE_AGE_SECONDS,
1547
- versionNote: { configAvailableSince: "bun 1.3.0" }
1548
- },
1549
- pnpm: {
1550
- accept: isPositiveNumber,
1551
- docs: "https://pnpm.io/settings/dependency-resolution#minimumreleaseage",
1552
- documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
1553
- defaultSafety: "conditional",
1554
- file: pnpmWorkspace$5,
1555
- keyPath: ["minimumReleaseAge"],
1556
- message: `Set minimumReleaseAge (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in pnpm-workspace.yaml.`,
1557
- value: RECOMMENDED_RELEASE_AGE_MINUTES,
1558
- versionNote: {
1559
- configAvailableSince: "pnpm 10.16.0",
1560
- defaultSafeSince: "pnpm 11.0.0 (1440 minutes)"
1561
- }
1562
- },
1563
- yarn: {
1564
- accept: isPositiveYarnDuration,
1565
- docs: "https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate",
1566
- documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
1567
- defaultSafety: "conditional",
1568
- file: yarnrc$2,
1569
- keyPath: ["npmMinimalAgeGate"],
1570
- message: `Set npmMinimalAgeGate (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in .yarnrc.yml.`,
1571
- value: RECOMMENDED_RELEASE_AGE_MINUTES,
1572
- versionNote: {
1573
- configAvailableSince: "yarn 4.10.0",
1574
- defaultSafeSince: "yarn 4.15.0 (1440 minutes)"
1575
- }
1576
- }
1577
- },
1578
- description: "Refuse to install releases newer than a cooldown window so freshly published (possibly compromised) versions are skipped.",
1579
- docs: "https://github.com/bodadotsh/npm-security-best-practices#2-set-cooldowns--minimum-release-age",
1580
- id: "minimum-release-age",
1581
- severity: "warn",
1582
- title: "Set a minimum release age"
1583
- });
1584
- const createNpmBinding = (time) => ({
1585
- file: npmrc$3,
1586
- docs: "https://docs.npmjs.com/cli/v12/using-npm/config#min-release-age",
1587
- versionNote: { note: "min-release-age available since npm 11.10.0" },
1588
- check(ctx, config) {
1589
- const now = time.now();
1590
- if (Object.hasOwn(config, "before")) {
1591
- const actual = config.before;
1592
- if ((typeof actual === "string" || typeof actual === "number") && time.parse(String(actual)) < now) return { state: "ok" };
1593
- return {
1594
- state: "violation",
1595
- actual,
1596
- expected: "a date in the past",
1597
- message: "Use a valid past before cutoff, or remove before and set min-release-age.",
1598
- remediation: {
1599
- kind: "manual",
1600
- steps: ["In .npmrc, set before to a valid past date. A future or disabled before overrides min-release-age in this file.", ...guardRemediationAvailability("npm", ctx.pmVersion, {
1601
- kind: "manual",
1602
- steps: [`Alternatively, remove before and set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days.`]
1603
- }, [{
1604
- file: npmrc$3,
1605
- keyPath: ["min-release-age"]
1606
- }])?.steps ?? []]
1607
- }
1608
- };
1609
- }
1610
- const actual = getByPath(config, ["min-release-age"]);
1611
- const age = typeof actual === "number" || typeof actual === "string" ? Number(actual) : NaN;
1612
- const cutoff = (/* @__PURE__ */ new Date(now - SECONDS_PER_DAY * 1e3 * age)).valueOf();
1613
- if (age > 0 && Number.isFinite(cutoff) && cutoff < now) return { state: "ok" };
1614
- return {
1615
- state: "violation",
1616
- actual,
1617
- expected: RECOMMENDED_RELEASE_AGE_DAYS,
1618
- message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days to quarantine brand-new releases.`,
1619
- remediation: guardRemediationAvailability("npm", ctx.pmVersion, proposeChanges(config, [{
1620
- file: npmrc$3,
1621
- op: "setKey",
1622
- keyPath: ["min-release-age"],
1623
- value: RECOMMENDED_RELEASE_AGE_DAYS
1624
- }]), [{
1625
- file: npmrc$3,
1626
- keyPath: ["min-release-age"]
1627
- }])
1628
- };
1629
- }
1630
- });
1631
- const createDenoBinding = (time) => ({
1632
- file: denoJson$1,
1633
- docs: "https://docs.deno.com/runtime/reference/deno_json/",
1634
- versionNote: {
1635
- defaultSafeSince: "deno 2.9.0 (1440 minutes)",
1636
- note: "object age may be omitted; project .npmrc fallback available since deno 2.8.1"
1637
- },
1638
- check(ctx, config) {
1639
- const actual = getByPath(config, ["minimumDependencyAge"]);
1640
- const now = time.now();
1641
- if (denoAgeUsesFallback(actual)) {
1642
- const npmrcConfig = ctx.readConfig(npmrc$3);
1643
- const npmrcAge = getByPath(npmrcConfig, ["min-release-age"]);
1644
- if (isPositiveDenoNpmrcDays(npmrcAge, now, time.parse)) return { state: "ok" };
1645
- if (npmrcAge === 0) return {
1646
- state: "violation",
1647
- actual: npmrcAge,
1648
- expected: RECOMMENDED_RELEASE_AGE_DAYS,
1649
- file: npmrc$3.path,
1650
- message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days in .npmrc, or set minimumDependencyAge in deno.json.`,
1651
- remediation: proposeChanges(npmrcConfig, [{
1652
- file: npmrc$3,
1653
- op: "setKey",
1654
- keyPath: ["min-release-age"],
1655
- value: RECOMMENDED_RELEASE_AGE_DAYS
1656
- }])
1657
- };
1658
- }
1659
- if (isNonDisabledDenoReleaseAge(actual, now, time.parse)) return { state: "ok" };
1660
- const objectAge = isPlainRecord(actual);
1661
- const invalidObject = objectAge && (Object.keys(actual).some((key) => key !== "age" && key !== "exclude") || actual.exclude !== void 0 && !isStringList(actual.exclude));
1662
- return {
1663
- state: "violation",
1664
- actual,
1665
- expected: "P3D",
1666
- message: `Set minimumDependencyAge (e.g. "P3D" for a ${RECOMMENDED_RELEASE_AGE_DAYS}-day cooldown) in deno.json.`,
1667
- remediation: invalidObject ? {
1668
- kind: "manual",
1669
- steps: ["Use only age and exclude in minimumDependencyAge. Make exclude a list of package names and set age to a positive supported duration."]
1670
- } : proposeChanges(config, [{
1671
- file: denoJson$1,
1672
- op: "setKey",
1673
- keyPath: objectAge ? ["minimumDependencyAge", "age"] : ["minimumDependencyAge"],
1674
- value: "P3D"
1675
- }])
1676
- };
1677
- }
1678
- });
1679
- const createMinimumReleaseAge = (time) => overrideBindings(baseRule$2, {
1680
- npm: createNpmBinding(time),
1681
- deno: createDenoBinding(time)
1682
- });
1683
- //#endregion
1684
- //#region src/core/rules/named-registries.ts
1685
- const { pnpmWorkspace: pnpmWorkspace$4 } = CONFIG_FILES;
1686
- const isNonEmptyObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
1687
- const namedRegistries = defineRule({
1688
- bindings: { pnpm: {
1689
- check(_ctx, config) {
1690
- const value = getByPath(config, ["namedRegistries"]);
1691
- if (!isNonEmptyObject$1(value)) return { state: "ok" };
1692
- return {
1693
- remediation: {
1694
- kind: "manual",
1695
- steps: ["Verify each named registry points to a trusted source and that scoped packages cannot be hijacked via a public registry of the same name."]
1696
- },
1697
- actual: value,
1698
- message: "Review `namedRegistries` in pnpm-workspace.yaml. Custom registry mappings can redirect package resolution away from the public registry, enabling dependency confusion attacks.",
1699
- state: "violation"
1700
- };
1701
- },
1702
- docs: "https://pnpm.io/settings/dependency-resolution#namedregistries",
1703
- file: pnpmWorkspace$4
1704
- } },
1705
- description: "Flag named registry mappings that redirect package resolution to custom registries, which may enable dependency confusion attacks.",
1706
- docs: "https://pnpm.io/settings/dependency-resolution#namedregistries",
1707
- id: "named-registries",
1708
- severity: "info",
1709
- title: "Review named registry mappings"
1710
- });
1711
- //#endregion
1712
- //#region src/core/rules/paranoid-mode.ts
1713
- const { aubeWorkspace: aubeWorkspace$3 } = CONFIG_FILES;
1714
- const paranoidMode = requireConfigKey({
1715
- bindings: { aube: {
1716
- docs: "https://aube.jdx.dev/security.html",
1717
- file: aubeWorkspace$3,
1718
- keyPath: ["paranoid"],
1719
- message: "Set `paranoid: true` in aube-workspace.yaml to enable the strict-security bundle (trustPolicy, jailBuilds, minimumReleaseAgeStrict, strictStoreIntegrity, strictDepBuilds, advisoryCheck).",
1720
- value: true
1721
- } },
1722
- description: "Activate the strict-security setting bundle that forces trustPolicy, jailBuilds, minimumReleaseAgeStrict, strictStoreIntegrity, strictDepBuilds, and advisoryCheck on in one switch.",
1723
- docs: "https://aube.jdx.dev/security.html",
1724
- id: "paranoid-mode",
1725
- severity: "info",
1726
- title: "Enable paranoid security mode"
1727
- });
1728
- //#endregion
1729
- //#region src/core/rules/patched-dependencies.ts
1730
- const { pnpmWorkspace: pnpmWorkspace$3 } = CONFIG_FILES;
1731
- const isNonEmptyObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
1732
- const patchedDependencies = defineRule({
1733
- bindings: { pnpm: {
1734
- check(_ctx, config) {
1735
- const value = getByPath(config, ["patchedDependencies"]);
1736
- if (!isNonEmptyObject(value)) return { state: "ok" };
1737
- return {
1738
- remediation: {
1739
- kind: "manual",
1740
- steps: ["Review each entry in `patchedDependencies` — verify patches address a known issue and have not been tampered with."]
1741
- },
1742
- actual: value,
1743
- message: "Review `patchedDependencies` in pnpm-workspace.yaml. Local patches modify dependency code after the registry artifact is verified; review the patch files separately.",
1744
- state: "violation"
1745
- };
1746
- },
1747
- docs: "https://pnpm.io/cli/patch#patcheddependencies",
1748
- file: pnpmWorkspace$3
1749
- } },
1750
- description: "Review local patches separately from the registry artifacts they modify.",
1751
- docs: "https://pnpm.io/cli/patch#patcheddependencies",
1752
- id: "patched-dependencies",
1753
- severity: "info",
1754
- title: "Review patched dependencies"
1755
- });
1756
- //#endregion
1757
- //#region src/core/rules/pin-exact-versions.ts
1758
- const { npmrc: npmrc$2, pnpmWorkspace: pnpmWorkspace$2, yarnrc: yarnrc$1, bunfig, denoJson } = CONFIG_FILES;
1759
- const REGISTRY_VERSION = /^(?:npm|jsr):(?:@[^/@]+\/)?[^/@]+@(?<version>[^/]*)/u;
1760
- const isUnpinnedRegistryImport = (specifier) => {
1761
- if (!specifier.startsWith("npm:") && !specifier.startsWith("jsr:")) return false;
1762
- const version = REGISTRY_VERSION.exec(specifier)?.groups?.version;
1763
- const normalized = version?.startsWith("=") ? version.slice(1) : version;
1764
- return normalized === void 0 || valid(normalized) === null;
1765
- };
1766
- const collectUnpinnedImports = (imports, location) => {
1767
- if (!isPlainRecord(imports)) throw new ConfigError(`deno.json: ${location} must be an import mapping object.`);
1768
- const offenders = [];
1769
- for (const [name, value] of Object.entries(imports)) {
1770
- if (value === null) continue;
1771
- if (typeof value !== "string") throw new ConfigError(`deno.json: ${location}.${name} must be a string or null.`);
1772
- if (isUnpinnedRegistryImport(value)) offenders.push(`${location}.${name}=${value}`);
1773
- }
1774
- return offenders;
1775
- };
1776
- const MAX_SAMPLE_COUNT = 3;
1777
- const formatOffenders = (offenders) => {
1778
- const sample = offenders.slice(0, MAX_SAMPLE_COUNT).join(", ");
1779
- let more = "";
1780
- if (offenders.length > MAX_SAMPLE_COUNT) more = ` (and ${offenders.length - MAX_SAMPLE_COUNT} more)`;
1781
- return {
1782
- remediation: {
1783
- kind: "manual",
1784
- steps: ["Pin each reported imports/scopes entry in deno.json to an exact version. For top-level imports, `deno add --save-exact <pkg>` can help; edit scoped mappings at their reported locations."]
1785
- },
1786
- message: `${offenders.length} deno imports are not pinned: ${sample}${more}. Use \`deno add --save-exact\` or pin manually.`,
1787
- state: "violation"
1788
- };
1789
- };
1790
- const OK = { state: "ok" };
1791
- const unpinnedInlineImports = (config) => {
1792
- const imports = getByPath(config, ["imports"]);
1793
- const offenders = imports == null ? [] : collectUnpinnedImports(imports, "imports");
1794
- const scopes = getByPath(config, ["scopes"]);
1795
- if (scopes != null) {
1796
- if (!isPlainRecord(scopes)) throw new ConfigError("deno.json: scopes must be an object.");
1797
- for (const [scope, mapping] of Object.entries(scopes)) offenders.push(...collectUnpinnedImports(mapping, `scopes[${JSON.stringify(scope)}]`));
1798
- }
1799
- return offenders;
1800
- };
1801
- const denoBinding = {
1802
- check(_ctx, config) {
1803
- const offenders = unpinnedInlineImports(config);
1804
- if (offenders.length === 0) return OK;
1805
- return formatOffenders(offenders);
1806
- },
1807
- docs: "https://docs.deno.com/runtime/reference/cli/add/",
1808
- file: denoJson,
1809
- versionNote: { configAvailableSince: "deno 1.30.0" }
1810
- };
1811
- const npmBinding = {
1812
- file: npmrc$2,
1813
- docs: "https://docs.npmjs.com/cli/v12/using-npm/config#save-exact",
1814
- check(_ctx, config) {
1815
- const exact = getByPath(config, ["save-exact"]);
1816
- const prefix = getByPath(config, ["save-prefix"]);
1817
- if (exact === true || prefix === "" || prefix === "=") return OK;
1818
- return {
1819
- remediation: proposeChanges(config, [{
1820
- op: "setKey",
1821
- file: npmrc$2,
1822
- keyPath: ["save-exact"],
1823
- value: true
1824
- }]),
1825
- state: "violation",
1826
- actual: exact,
1827
- expected: true,
1828
- message: "Set `save-exact=true` in .npmrc to save exact versions by default."
1829
- };
1830
- }
1831
- };
1832
- const aubeBinding = {
1833
- file: npmrc$2,
1834
- docs: "https://aube.sh/settings/#setting-saveprefix",
1835
- check(_ctx, config) {
1836
- const prefixes = ["save-prefix", "savePrefix"].filter((key) => Object.hasOwn(config, key)).map((key) => config[key]);
1837
- if (prefixes.length > 0 && prefixes.every((value) => value === "")) return OK;
1838
- return {
1839
- remediation: {
1840
- kind: "manual",
1841
- steps: ["Use one `save-prefix=` entry in .npmrc. Aube resolves aliases by line order; reconcile conflicting entries before changing them."]
1842
- },
1843
- state: "violation",
1844
- message: "Set `save-prefix=` in .npmrc; remove any conflicting `savePrefix` alias."
1845
- };
1846
- }
1847
- };
1848
- const pinExactVersions = overrideBindings(requireConfigKey({
1849
- bindings: {
1850
- bun: {
1851
- docs: "https://bun.com/docs/runtime/bunfig#install-exact",
1852
- file: bunfig,
1853
- keyPath: ["install", "exact"],
1854
- message: "Set `exact = true` under [install] in bunfig.toml to pin exact versions.",
1855
- value: true,
1856
- versionNote: { note: "install.exact verified in bun 1.2.0" }
1857
- },
1858
- pnpm: {
1859
- accept: (value) => value === "" || value === "=",
1860
- docs: "https://pnpm.io/settings/other#saveprefix",
1861
- file: pnpmWorkspace$2,
1862
- keyPath: ["savePrefix"],
1863
- message: "Set `savePrefix: ''` in pnpm-workspace.yaml to pin exact versions.",
1864
- value: ""
1865
- },
1866
- yarn: {
1867
- docs: "https://yarnpkg.com/configuration/yarnrc#defaultSemverRangePrefix",
1868
- file: yarnrc$1,
1869
- keyPath: ["defaultSemverRangePrefix"],
1870
- message: "Set `defaultSemverRangePrefix: ''` in .yarnrc.yml to pin exact versions.",
1871
- value: "",
1872
- versionNote: { configAvailableSince: "yarn 2.0.0" }
1873
- }
1874
- },
1875
- description: "Semver ranges (^, ~) auto-adopt new releases, including compromised ones. Save exact versions by default; for Deno, inspect registry mappings in both inline imports and scopes (not external import maps).",
1876
- docs: "https://github.com/bodadotsh/npm-security-best-practices#4-pin-dependency-versions",
1877
- id: "pin-exact-versions",
1878
- severity: "error",
1879
- title: "Pin exact dependency versions"
1880
- }), {
1881
- aube: aubeBinding,
1882
- deno: denoBinding,
1883
- npm: npmBinding
1884
- });
1885
- //#endregion
1886
- //#region src/core/rules/provenance.ts
1887
- const { npmrc: npmrc$1, yarnrc } = CONFIG_FILES;
1888
- const npmrcProvenance = {
1889
- file: npmrc$1,
1890
- keyPath: ["provenance"],
1891
- message: "Set `provenance=true` (and publish from CI) to attest releases.",
1892
- value: true
1893
- };
1894
- const baseRule = requireConfigKey({
1895
- applies: isPublishable,
1896
- bindings: {
1897
- bun: {
1898
- ...npmrcProvenance,
1899
- docs: "https://github.com/oven-sh/bun/issues/15601",
1900
- message: "Set `provenance=true` in .npmrc and publish via `bunx npm publish` from CI — `bun publish` does not emit provenance attestations natively (tracking: oven-sh/bun#15601)."
1901
- },
1902
- npm: {
1903
- ...npmrcProvenance,
1904
- docs: "https://docs.npmjs.com/cli/v11/using-npm/config#provenance",
1905
- versionNote: { configAvailableSince: "npm 9.5.0" }
1906
- },
1907
- pnpm: {
1908
- ...npmrcProvenance,
1909
- docs: "https://pnpm.io/cli/publish"
1910
- },
1911
- yarn: {
1912
- docs: "https://yarnpkg.com/configuration/yarnrc#npmPublishProvenance",
1913
- file: yarnrc,
1914
- keyPath: ["npmPublishProvenance"],
1915
- message: "Set `npmPublishProvenance: true` (and publish from CI) to attest releases.",
1916
- value: true
1917
- }
1918
- },
1919
- description: "Provenance statements (via Sigstore) bind a published artifact to its recorded source and build.",
1920
- docs: "https://github.com/bodadotsh/npm-security-best-practices#11-generate-provenance-statements",
1921
- id: "provenance",
1922
- projectTypes: ["package"],
1923
- severity: "warn",
1924
- title: "Publish with provenance"
1925
- });
1926
- const provenance = overrideBindings(baseRule, { npm: {
1927
- ...baseRule.bindings.npm,
1928
- check(ctx, config) {
1929
- if (!isPublishable(ctx)) return { state: "na" };
1930
- const publishConfig = ctx.packageJson?.publishConfig;
1931
- if (!publishConfig || !Object.hasOwn(publishConfig, "provenance")) return baseRule.bindings.npm.check(ctx, config);
1932
- if (publishConfig.provenance === true) return { state: "ok" };
1933
- const file = CONFIG_FILES.packageJson;
1934
- return {
1935
- state: "violation",
1936
- file: file.path,
1937
- actual: publishConfig.provenance,
1938
- expected: true,
1939
- message: "Set publishConfig.provenance=true in package.json (and publish from CI) to attest releases.",
1940
- remediation: guardRemediationAvailability("npm", ctx.pmVersion, proposeChanges(ctx.readConfig(file), [{
1941
- file,
1942
- op: "setKey",
1943
- keyPath: ["publishConfig", "provenance"],
1944
- value: true
1945
- }]))
1946
- };
1947
- }
1948
- } });
1949
- //#endregion
1950
- //#region src/core/rules/publish-access.ts
1951
- const { packageJson } = CONFIG_FILES;
1952
- const publishAccessBinding = {
1953
- check(ctx) {
1954
- if (!isPublishable(ctx)) return { state: "na" };
1955
- const access = ctx.packageJson?.publishConfig?.access;
1956
- if (access === "public" || access === "restricted") return { state: "ok" };
1957
- return {
1958
- remediation: {
1959
- kind: "manual",
1960
- steps: ["Add `\"publishConfig\": { \"access\": \"public\" }` (or `\"restricted\"`) to package.json so publishes never default unexpectedly."]
1961
- },
1962
- actual: access,
1963
- message: "Set `publishConfig.access` in package.json to `public` or `restricted` to declare publish scope explicitly.",
1964
- state: "violation"
1965
- };
1966
- },
1967
- docs: "https://docs.npmjs.com/cli/v11/configuring-npm/package-json#publishconfig",
1968
- file: packageJson
1969
- };
1970
- const publishAccess = defineRule({
1971
- bindings: {
1972
- aube: publishAccessBinding,
1973
- bun: publishAccessBinding,
1974
- npm: {
1975
- ...publishAccessBinding,
1976
- docs: "https://docs.npmjs.com/cli/v12/using-npm/config#access",
1977
- check(ctx, config) {
1978
- if (isPublishable(ctx) && ctx.packageJson?.publishConfig?.access === "private") return { state: "ok" };
1979
- return publishAccessBinding.check(ctx, config);
1980
- }
1981
- },
1982
- pnpm: publishAccessBinding,
1983
- yarn: publishAccessBinding
1984
- },
1985
- description: "Set `publishConfig.access` so a misconfigured scope or registry never accidentally publishes an internal package publicly.",
1986
- docs: "https://github.com/bodadotsh/npm-security-best-practices#for-maintainers",
1987
- id: "publish-access",
1988
- projectTypes: ["package"],
1989
- severity: "info",
1990
- title: "Declare publish access explicitly"
1991
- });
1992
- //#endregion
1993
- //#region src/core/rules/store-server.ts
1994
- const { pnpmWorkspace: pnpmWorkspace$1 } = CONFIG_FILES;
1995
- const storeServer = defineRule({
1996
- bindings: { pnpm: {
1997
- check(_ctx, config) {
1998
- const value = getByPath(config, ["useRunningStoreServer"]);
1999
- if (value !== true) return { state: "ok" };
2000
- return {
2001
- remediation: {
2002
- kind: "manual",
2003
- steps: ["Verify the store server is running in a trusted environment and that its communication channel is not exposed to untrusted networks."]
2004
- },
2005
- actual: value,
2006
- message: "Review `useRunningStoreServer` in pnpm-workspace.yaml. Delegating store operations to an external process introduces a trust boundary — the server process can serve tampered packages.",
2007
- state: "violation"
2008
- };
2009
- },
2010
- docs: "https://pnpm.io/settings/store#userunningstoreserver",
2011
- file: pnpmWorkspace$1
2012
- } },
2013
- description: "Flag use of an external store server process, which introduces a trust boundary where tampered packages could be served.",
2014
- docs: "https://pnpm.io/settings/store#userunningstoreserver",
2015
- id: "store-server",
2016
- severity: "info",
2017
- title: "Review store server usage"
2018
- });
2019
- //#endregion
2020
- //#region src/core/rules/strict-allow-scripts.ts
2021
- const { npmrc } = CONFIG_FILES;
2022
- const strictAllowScripts = defineRule({
2023
- bindings: { npm: {
2024
- check(_ctx, config) {
2025
- if (getByPath(config, ["ignore-scripts"]) === true) return { state: "ok" };
2026
- const bypass = getByPath(config, ["dangerously-allow-all-scripts"]);
2027
- if (bypass === true) return {
2028
- actual: bypass,
2029
- expected: false,
2030
- remediation: {
2031
- kind: "manual",
2032
- steps: ["Remove `dangerously-allow-all-scripts=true` from .npmrc (or set it to false), then set `strict-allow-scripts=true`. The bypass overrides strict script approval."]
2033
- },
2034
- message: "`dangerously-allow-all-scripts=true` bypasses script approval even when `strict-allow-scripts=true`.",
2035
- state: "violation"
2036
- };
2037
- const strict = getByPath(config, ["strict-allow-scripts"]);
2038
- if (strict === true) return { state: "ok" };
2039
- return {
2040
- remediation: proposeChanges(config, [{
2041
- file: npmrc,
2042
- keyPath: ["strict-allow-scripts"],
2043
- op: "setKey",
2044
- value: true
2045
- }]),
2046
- actual: strict,
2047
- expected: true,
2048
- message: "Set `strict-allow-scripts=true` in .npmrc to fail installation for unreviewed install scripts.",
2049
- state: "violation"
2050
- };
2051
- },
2052
- docs: "https://docs.npmjs.com/cli/v12/using-npm/config#strict-allow-scripts",
2053
- file: npmrc
2054
- } },
2055
- description: "Turn install-script policy warnings into hard errors so unapproved lifecycle scripts block installation.",
2056
- docs: "https://docs.npmjs.com/cli/v12/using-npm/config#strict-allow-scripts",
2057
- id: "strict-allow-scripts",
2058
- severity: "warn",
2059
- title: "Require explicit script allow-listing"
2060
- });
2061
- //#endregion
2062
- //#region src/core/rules/strict-release-age.ts
2063
- const { aubeWorkspace: aubeWorkspace$2 } = CONFIG_FILES;
2064
- const strictReleaseAge = withAubeParanoid(requireConfigKey({
2065
- bindings: { aube: {
2066
- docs: "https://aube.jdx.dev/settings/",
2067
- file: aubeWorkspace$2,
2068
- keyPath: ["minimumReleaseAgeStrict"],
2069
- message: "Set `minimumReleaseAgeStrict: true` in aube-workspace.yaml to make the release age gate a hard block.",
2070
- value: true
2071
- } },
2072
- description: "Fail when no satisfying version meets the release age, instead of falling back to the lowest satisfying version.",
2073
- docs: "https://aube.jdx.dev/security.html",
2074
- id: "strict-release-age",
2075
- severity: "info",
2076
- title: "Enforce strict release age gate"
2077
- }));
2078
- //#endregion
2079
- //#region src/core/rules/strict-store-integrity.ts
2080
- const { aubeWorkspace: aubeWorkspace$1 } = CONFIG_FILES;
2081
- const strictStoreIntegrity = defineRule({
2082
- bindings: { aube: {
2083
- check(_ctx, config) {
2084
- if (getByPath(config, ["verifyStoreIntegrity"]) === false) return {
2085
- actual: false,
2086
- expected: true,
2087
- remediation: {
2088
- kind: "manual",
2089
- steps: ["Set `verifyStoreIntegrity: true` (or remove `verifyStoreIntegrity: false`) in aube-workspace.yaml, and enable `strictStoreIntegrity: true` or `paranoid: true`."]
2090
- },
2091
- message: "`verifyStoreIntegrity: false` bypasses strict integrity checks, including when `paranoid: true`.",
2092
- state: "violation"
2093
- };
2094
- const strict = getByPath(config, ["strictStoreIntegrity"]);
2095
- if (strict === true || getByPath(config, ["paranoid"]) === true) return { state: "ok" };
2096
- return {
2097
- remediation: proposeChanges(config, [{
2098
- file: aubeWorkspace$1,
2099
- keyPath: ["strictStoreIntegrity"],
2100
- op: "setKey",
2101
- value: true
2102
- }]),
2103
- actual: strict,
2104
- expected: true,
2105
- message: "Set `strictStoreIntegrity: true` in aube-workspace.yaml to refuse tarballs that lack integrity metadata.",
2106
- state: "violation"
2107
- };
2108
- },
2109
- docs: "https://aube.jdx.dev/settings/",
2110
- file: aubeWorkspace$1
2111
- } },
2112
- description: "Refuse to import tarballs from the registry when the packument lacks a dist.integrity field, preventing silent integrity bypass.",
2113
- docs: "https://aube.jdx.dev/security.html",
2114
- id: "strict-store-integrity",
2115
- severity: "warn",
2116
- title: "Require tarball integrity metadata"
2117
- });
2118
- //#endregion
2119
- //#region src/core/rules/trust-policy.ts
2120
- const { aubeWorkspace, pnpmWorkspace } = CONFIG_FILES;
2121
- const trustPolicy = withAubeParanoid(requireConfigKey({
2122
- bindings: {
2123
- aube: {
2124
- docs: "https://aube.jdx.dev/security.html",
2125
- documentedDefault: "no-downgrade",
2126
- defaultSafety: "unconditional",
2127
- file: aubeWorkspace,
2128
- keyPath: ["trustPolicy"],
2129
- message: "Set `trustPolicy: no-downgrade` in aube-workspace.yaml to block packages whose trust level has decreased.",
2130
- value: "no-downgrade"
2131
- },
2132
- pnpm: {
2133
- docs: "https://pnpm.io/settings/dependency-resolution#trustpolicy",
2134
- file: pnpmWorkspace,
2135
- keyPath: ["trustPolicy"],
2136
- message: "Set `trustPolicy: no-downgrade` in pnpm-workspace.yaml to block packages whose trust level has decreased.",
2137
- value: "no-downgrade",
2138
- versionNote: { configAvailableSince: "pnpm 10.21.0" }
2139
- }
2140
- },
2141
- description: "Fail installation when a package trust level has decreased compared to previous releases, catching publisher credential downgrades.",
2142
- docs: "https://pnpm.io/settings/dependency-resolution#trustpolicy",
2143
- id: "trust-policy",
2144
- severity: "warn",
2145
- title: "Enforce trust policy on dependency updates"
2146
- }));
2147
- //#endregion
2148
- //#region src/core/rules/unsupported-settings.ts
2149
- const bindingFor = (pm, includesFile) => {
2150
- const settings = settingAvailability.filter((setting) => setting.pm === pm && includesFile(setting.file.path));
2151
- return { check(ctx) {
2152
- const version = ctx.pmVersion;
2153
- if (version === void 0) return { state: "na" };
2154
- const unsupported = settings.filter((setting) => lt(version, setting.since) && getByPath(ctx.readConfig(setting.file), setting.keyPath) !== void 0);
2155
- const [first, ...rest] = [...new Set(unsupported.map((setting) => setting.file.path))].map((file) => {
2156
- const fileSettings = unsupported.filter((setting) => setting.file.path === file);
2157
- return {
2158
- state: "violation",
2159
- file,
2160
- message: `Target ${pm} ${version} predates support for ${file}: ${fileSettings.map((setting) => `${setting.keyPath.join(".")} (requires ${pm} >=${setting.since})`).join("; ")}.`,
2161
- remediation: {
2162
- kind: "manual",
2163
- steps: ["Verify the PM version used by your install or publish command. If the declared target is accurate, upgrade the PM or use a security control supported by that target; removing a setting alone does not provide its protection.", ...fileSettings.map((setting) => `${setting.file.path}#${setting.keyPath.join(".")}: ${setting.source}`)]
2164
- }
2165
- };
2166
- });
2167
- return first ? {
2168
- state: "violations",
2169
- violations: [first, ...rest]
2170
- } : { state: "ok" };
2171
- } };
2172
- };
2173
- const createUnsupportedSettings = (includesFile) => defineRule({
2174
- id: "unsupported-settings",
2175
- title: "Use settings available in the target PM version",
2176
- description: "Report configured settings whose recorded introduction version is newer than the declared or explicit stable PM target. Reports each affected file separately, grouping its unsupported keys. Findings retain their evaluation directory. Only the coverage table below is checked. Unknown targets and unlisted settings are not evaluated for availability.",
2177
- severity: "error",
2178
- docs: "https://github.com/pHo9UBenaA/siro/blob/main/docs/rules.md#unsupported-settings--error",
2179
- bindings: {
2180
- npm: bindingFor("npm", includesFile),
2181
- pnpm: bindingFor("pnpm", includesFile),
2182
- yarn: bindingFor("yarn", includesFile),
2183
- bun: bindingFor("bun", includesFile),
2184
- deno: bindingFor("deno", includesFile)
2185
- }
2186
- });
2187
- const unsupportedSettings = createUnsupportedSettings(() => true);
2188
- //#endregion
2189
- //#region src/core/rules/builtin-rules.ts
2190
- const createBuiltinRules = (time) => [
2191
- advisoryCheck,
2192
- approvedGitRepos,
2193
- auditSuppression,
2194
- blockAutoInstall,
2195
- blockExoticSubdeps,
2196
- bunSecurityScanner,
2197
- checksumVerification,
2198
- commitLockfile,
2199
- dependencyOverrides,
2200
- disableLifecycleScripts,
2201
- enforceStrictSsl,
2202
- filesField,
2203
- frozenLockfile,
2204
- frozenStore,
2205
- hardenedMode,
2206
- createMinimumReleaseAge(time),
2207
- namedRegistries,
2208
- paranoidMode,
2209
- patchedDependencies,
2210
- pinExactVersions,
2211
- provenance,
2212
- publishAccess,
2213
- storeServer,
2214
- strictAllowScripts,
2215
- strictReleaseAge,
2216
- strictStoreIntegrity,
2217
- trustPolicy,
2218
- unsupportedSettings
2219
- ];
2220
- const builtinScope = {
2221
- "advisory-check": "installation",
2222
- "approved-git-repos": "installation",
2223
- "audit-suppression": "installation",
2224
- "block-auto-install": "installation",
2225
- "block-exotic-subdeps": "installation",
2226
- "bun-security-scanner": "installation",
2227
- "checksum-verification": "installation",
2228
- "commit-lockfile": "installation",
2229
- "dependency-overrides": "installation",
2230
- "disable-lifecycle-scripts": "installation",
2231
- "enforce-strict-ssl": "installation",
2232
- "files-field": "manifest",
2233
- "frozen-lockfile": "installation",
2234
- "frozen-store": "installation",
2235
- "hardened-mode": "installation",
2236
- "minimum-release-age": "installation",
2237
- "named-registries": "installation",
2238
- "paranoid-mode": "installation",
2239
- "patched-dependencies": "installation",
2240
- "pin-exact-versions": "installation",
2241
- provenance: "installation",
2242
- "publish-access": "manifest",
2243
- "store-server": "installation",
2244
- "strict-allow-scripts": "installation",
2245
- "strict-release-age": "installation",
2246
- "strict-store-integrity": "installation",
2247
- "trust-policy": "installation",
2248
- "unsupported-settings": "split"
2249
- };
2250
- const scopeOf = (id) => Object.hasOwn(builtinScope, id) ? builtinScope[id] : "custom";
2251
- //#endregion
2252
- //#region src/adapters/exclusions.ts
2253
- const compileExclusions = (patterns) => {
2254
- try {
2255
- const matchers = patterns.map((pattern) => new Minimatch(pattern.replace(/(?:\/\*\*)+$/u, "").replace(/[\\[\]]/gu, "\\$&"), {
2256
- dot: true,
2257
- nocase: false,
2258
- nonegate: true,
2259
- nocomment: true,
2260
- nobrace: true,
2261
- noext: true,
2262
- platform: "linux"
2263
- }));
2264
- return (directory) => matchers.some((matcher) => matcher.match(directory));
2265
- } catch (error) {
2266
- throw new ConfigError(`exclude: ${error instanceof Error ? error.message : String(error)}`);
2267
- }
2268
- };
2269
- //#endregion
2270
- //#region src/core/apply-config.ts
2271
- /** Select active rules and keep user severity choices as explicit runtime data. */
2272
- const applyConfig = (base, config) => {
2273
- const merged = [...base, ...config?.customRules ?? []];
2274
- const known = /* @__PURE__ */ new Set();
2275
- const duplicates = /* @__PURE__ */ new Set();
2276
- for (const rule of merged) {
2277
- if (known.has(rule.id)) duplicates.add(rule.id);
2278
- known.add(rule.id);
2279
- }
2280
- if (duplicates.size > 0) throw new ConfigError(`Duplicate rule ids: ${[...duplicates].map((id) => `'${id}'`).join(", ")}`);
2281
- const unknown = Object.keys(config?.rules ?? {}).filter((id) => !known.has(id));
2282
- if (unknown.length > 0) throw new ConfigError(`Unknown rule ids: ${unknown.map((id) => `'${id}'`).join(", ")}`);
2283
- const activeRules = [];
2284
- const severityOverrides = /* @__PURE__ */ new Map();
2285
- const overrides = config?.rules;
2286
- for (const rule of merged) {
2287
- const override = overrides && Object.hasOwn(overrides, rule.id) ? overrides[rule.id] : void 0;
2288
- if (override === "off") continue;
2289
- activeRules.push(rule);
2290
- if (override !== void 0) severityOverrides.set(rule.id, override);
2291
- }
2292
- return {
2293
- rules: activeRules,
2294
- severityOverrides
2295
- };
2296
- };
2297
- //#endregion
2298
- //#region src/core/detect-pms.ts
2299
- const registerSignals = (signals, pm, owner) => {
2300
- for (const file of signals) {
2301
- const prior = owner.get(file);
2302
- if (typeof prior !== "undefined") throw new TypeError(`detection signal '${file}' is claimed by both '${prior}' and '${pm}'`);
2303
- owner.set(file, pm);
2304
- }
2305
- };
2306
- const buildDetectionSignals = () => {
2307
- const out = /* @__PURE__ */ new Map();
2308
- const owner = /* @__PURE__ */ new Map();
2309
- for (const pm of PMS) {
2310
- const { lockfiles, configs } = PM_SIGNALS[pm];
2311
- const signals = [...lockfiles, ...configs];
2312
- registerSignals(signals, pm, owner);
2313
- out.set(pm, signals);
2314
- }
2315
- return out;
2316
- };
2317
- const DETECTION_SIGNALS = buildDetectionSignals();
2318
- const detectPMs = (ctx) => {
2319
- const found = /* @__PURE__ */ new Set();
2320
- const declared = ctx.packageJson?.packageManager;
2321
- const declaredPM = declared === void 0 ? void 0 : parsePackageManagerField(declared);
2322
- if (declaredPM !== void 0) found.add(declaredPM);
2323
- for (const pm of PMS) {
2324
- const signals = DETECTION_SIGNALS.get(pm);
2325
- if (signals && signals.some((file) => ctx.exists(asRelPath(file)))) found.add(pm);
2326
- }
2327
- return PMS.filter((pm) => found.has(pm));
2328
- };
2329
- //#endregion
2330
- //#region src/core/resolve-pms.ts
2331
- /** Apply the explicit selection or detection, then the configured allow-list. */
2332
- const resolvePMs = (ctx, opts) => {
2333
- const { pmOverride, allowed } = opts;
2334
- const detected = pmOverride ? [pmOverride] : detectPMs(ctx);
2335
- const pms = allowed ? detected.filter((pm) => allowed.includes(pm)) : detected;
2336
- if (pms.length > 0) return pms;
2337
- if (opts.optional && detected.length === 0) return [];
2338
- if (!allowed) throw new UsageError(`No package manager detected. Pass --pm <${PMS.join("|")}> to be explicit.`);
2339
- if (pmOverride) throw new UsageError(`--pm ${pmOverride} is not in siro.config.ts pms (${allowed.join(", ")}). Adjust the config or the flag.`);
2340
- if (detected.length > 0) throw new UsageError(`Detected PMs (${detected.join(", ")}) do not match siro.config.ts pms (${allowed.join(", ")}). Adjust the config or remove the restriction.`);
2341
- throw new UsageError(`No package manager detected, and siro.config.ts restricts pms to ${allowed.join(", ")}. Adjust the config or remove the restriction.`);
2342
- };
2343
- //#endregion
2344
- //#region src/core/decide-severity.ts
2345
- /** Resolve the most specific severity signal for one finding. */
2346
- const decideSeverity = (status, binding, rule, userOverride) => userOverride ?? status.severity ?? binding.severity ?? rule.severity;
2347
- //#endregion
2348
- //#region src/core/render-version-note.ts
2349
- const buildVersionNoteParts = (versionNote) => {
2350
- const parts = [];
2351
- if (typeof versionNote.configAvailableSince !== "undefined") parts.push(`available since ${versionNote.configAvailableSince}`);
2352
- if (typeof versionNote.defaultSafeSince !== "undefined") parts.push(`default safe since ${versionNote.defaultSafeSince}`);
2353
- if (typeof versionNote.note !== "undefined") parts.push(versionNote.note);
2354
- return parts;
2355
- };
2356
- const renderVersionNoteMessage = (message, versionNote) => {
2357
- if (typeof versionNote === "undefined") return message;
2358
- const parts = buildVersionNoteParts(versionNote);
2359
- if (parts.length === 0) return message;
2360
- return `${message} (${parts.join("; ")})`;
2361
- };
2362
- //#endregion
2363
- //#region src/core/evaluate-binding.ts
2364
- /** One response boundary for generic and PM-specific checks. Selection stays with callers. */
2365
- const evaluateBinding = (repository, rule, binding, overrides, target) => {
2366
- const response = binding.check({
2367
- ...repository.ctx,
2368
- readConfig: repository.parseConfig,
2369
- pmVersion: target?.version
2370
- }, repository.parseConfig(binding.file));
2371
- assertSynchronous(response, `Rule '${rule.id}' check`);
2372
- if (!isCheckStatusShape(response)) throw new ConfigError(`Rule '${rule.id}' returned an invalid check result.`);
2373
- return (response.state === "violations" ? response.violations : [response]).flatMap((status) => status.state !== "violation" ? [] : [{
2374
- ruleId: rule.id,
2375
- directory: ".",
2376
- ...target ? { pm: target.pm } : {},
2377
- severity: decideSeverity(status, binding, rule, overrides?.get(rule.id)),
2378
- message: renderVersionNoteMessage(status.message, binding.versionNote),
2379
- file: status.file ?? binding.file?.path,
2380
- docs: binding.docs ?? rule.docs,
2381
- actual: status.actual,
2382
- expected: status.expected,
2383
- remediation: target ? guardRemediationAvailability(target.pm, target.version, status.remediation) : status.remediation
2384
- }]);
2385
- };
2386
- //#endregion
2387
- //#region src/core/run-lint.ts
2388
- const resolveBindingProjectType = (ctx, pm, parseConfig) => {
2389
- if (ctx.projectType !== void 0) return ctx.projectType;
2390
- return pm === "deno" ? resolveDenoProjectType(ctx, parseConfig(CONFIG_FILES.denoJson)) : resolvePackageJsonProjectType(ctx);
2391
- };
2392
- /** Select applicable PM bindings; response handling is shared with manifest checks. */
2393
- const runLint = (opts) => {
2394
- const { repository, pms, ruleSet, severityOverrides } = opts;
2395
- const findings = [];
2396
- for (const rule of ruleSet) for (const pm of pms) {
2397
- const binding = rule.bindings[pm];
2398
- if (!binding || rule.projectTypes && !rule.projectTypes.includes(resolveBindingProjectType(repository.ctx, pm, repository.parseConfig))) continue;
2399
- findings.push(...evaluateBinding(repository, rule, binding, severityOverrides, {
2400
- pm,
2401
- version: opts.pmVersions?.[pm]
2402
- }));
2403
- }
2404
- const summary = {
2405
- error: 0,
2406
- info: 0,
2407
- warn: 0
2408
- };
2409
- for (const finding of findings) summary[finding.severity] += 1;
2410
- return {
2411
- findings,
2412
- summary
2413
- };
2414
- };
2415
- //#endregion
2416
- //#region src/core/contracts/deno-json.ts
2417
- /** Validate consumed publication metadata, not Deno's entire versioned schema. */
2418
- const validateDenoMetadata = (config) => {
2419
- const name = getByPath(config, ["name"]);
2420
- if (name != null && typeof name !== "string") throw new ConfigError("deno.json: name must be a string or null.");
2421
- const publish = getByPath(config, ["publish"]);
2422
- if (publish == null || typeof publish === "boolean") return;
2423
- if (!isPlainRecord(publish)) throw new ConfigError("deno.json: publish must be a boolean, object or null.");
2424
- const include = getByPath(publish, ["include"]);
2425
- if (include != null && (!Array.isArray(include) || !Array.from(include).every((item) => typeof item === "string"))) throw new ConfigError("deno.json: publish.include must be a string array or null.");
2426
- };
2427
- //#endregion
2428
- //#region src/core/parse-config-file.ts
2429
- const EMPTY_FILE = Object.freeze({});
2430
- /** Create a lazy parser for one repository context; callers own its lifetime. */
2431
- const createConfigParser = (codecFor, ctx) => {
2432
- const cache = /* @__PURE__ */ new Map();
2433
- return (file) => {
2434
- if (file === void 0) return EMPTY_FILE;
2435
- const cacheKey = `${file.kind}:${file.path}`;
2436
- const cached = cache.get(cacheKey);
2437
- if (cached !== void 0) return cached;
2438
- const text = ctx.readText(file.path);
2439
- if (text === void 0) {
2440
- cache.set(cacheKey, EMPTY_FILE);
2441
- return EMPTY_FILE;
2442
- }
2443
- const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
2444
- cache.set(cacheKey, parsed);
2445
- return parsed;
2446
- };
2447
- };
2448
- /** Pair the context with a fresh lazy parser; construction performs no reads. */
2449
- const createRepositoryEvaluation = (ctx, codecFor) => ({
2450
- ctx,
2451
- parseConfig: createConfigParser(codecFor, ctx)
2452
- });
2453
- //#endregion
2454
- //#region src/core/discovery.ts
2455
- /** One context per selected directory, shared by publication and installation passes. */
2456
- const discover = (cwd, fs, excluded, projectType, dependencies) => {
2457
- if (typeof fs.readDirectories !== "function") throw new ConfigError("FileSystem.readDirectories is required for package discovery; no native filesystem fallback is used.");
2458
- const { paths, createRepoContext, codecFor } = dependencies;
2459
- const pending = [asRelPath(".")];
2460
- const directories = [];
2461
- while (pending.length) {
2462
- const directory = pending.pop();
2463
- const absolute = paths.resolve(cwd, directory);
2464
- let repository;
2465
- const manifests = [];
2466
- try {
2467
- repository = createRepositoryEvaluation(createRepoContext(absolute, fs, projectType), codecFor);
2468
- const { ctx, parseConfig } = repository;
2469
- if (ctx.packageJson !== void 0) manifests.push("package.json");
2470
- if (ctx.readText(CONFIG_FILES.denoJson.path) !== void 0) {
2471
- validateDenoMetadata(parseConfig(CONFIG_FILES.denoJson));
2472
- manifests.push("deno.json");
2473
- } else if (ctx.exists(asRelPath("deno.jsonc"))) throw new ConfigError("deno.jsonc is not supported; use strict deno.json.");
2474
- } catch (error) {
2475
- if (error instanceof ConfigError && directory !== ".") throw new ConfigError(`${directory}/${error.message}`);
2476
- throw error;
2477
- }
2478
- directories.push({
2479
- directory,
2480
- repository,
2481
- manifests
2482
- });
2483
- const names = fs.readDirectories(absolute);
2484
- if (!Array.isArray(names)) throw new ConfigError(`${directory}: FileSystem.readDirectories must return a dense array.`);
2485
- const children = /* @__PURE__ */ new Set();
2486
- for (let i = 0; i < names.length; i += 1) {
2487
- if (!Object.hasOwn(names, i)) throw new ConfigError(`${directory}: FileSystem.readDirectories must return a dense array.`);
2488
- const child = paths.child(directory, names[i]);
2489
- if (names[i] === ".git" || names[i] === "node_modules" || excluded(child) || children.has(child)) continue;
2490
- children.add(child);
2491
- pending.push(child);
2492
- }
2493
- }
2494
- return directories.sort((a, b) => a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0);
2495
- };
2496
- //#endregion
2497
- //#region src/core/manifest-checks.ts
2498
- const manifestProjectType = (repository, file) => file === "deno.json" ? resolveDenoProjectType(repository.ctx, repository.parseConfig(CONFIG_FILES.denoJson)) : resolvePackageJsonProjectType(repository.ctx);
2499
- const selectBinding = {
2500
- "files-field": (file) => file === "deno.json" ? denoPublishBinding : packageJsonFilesBinding,
2501
- "publish-access": (file, repository, targets) => {
2502
- if (file !== "package.json") return void 0;
2503
- return repository.ctx.packageJson?.publishConfig?.access === "private" && targets.length > 0 ? "pm" : publishAccessBinding;
2504
- }
2505
- };
2506
- const checkManifest = (repository, file, targets, rules, overrides) => {
2507
- const findings = [];
2508
- const projectType = manifestProjectType(repository, file);
2509
- const pms = targets.map((target) => target.pm);
2510
- const pmVersions = Object.fromEntries(targets.map((target) => [target.pm, target.version]));
2511
- for (const rule of rules) {
2512
- if (rule.projectTypes && !rule.projectTypes.includes(projectType)) continue;
2513
- if (rule.id === "unsupported-settings") findings.push(...runLint({
2514
- repository,
2515
- pms,
2516
- pmVersions,
2517
- ruleSet: [{
2518
- ...rule,
2519
- bindings: createUnsupportedSettings((path) => path === file).bindings
2520
- }],
2521
- severityOverrides: overrides
2522
- }).findings);
2523
- else if (Object.hasOwn(selectBinding, rule.id)) {
2524
- const binding = selectBinding[rule.id](file, repository, targets);
2525
- if (binding === "pm") findings.push(...runLint({
2526
- repository,
2527
- pms,
2528
- pmVersions,
2529
- ruleSet: [rule],
2530
- severityOverrides: overrides
2531
- }).findings);
2532
- else if (binding) findings.push(...evaluateBinding(repository, rule, binding, overrides));
2533
- }
2534
- }
2535
- return findings;
2536
- };
2537
- //#endregion
2538
- //#region src/core/rebase-finding.ts
2539
- /** Context-local rule output becomes cwd-relative once, without mutating rule-owned data. */
2540
- const rebaseFinding = (directory, finding) => {
2541
- if (directory === ".") return {
2542
- ...finding,
2543
- directory
2544
- };
2545
- const remediation = finding.remediation;
2546
- const rebaseOperation = (operation) => ({
2547
- ...operation,
2548
- file: {
2549
- ...operation.file,
2550
- path: `${directory}/${operation.file.path}`
2551
- }
2552
- });
2553
- return {
2554
- ...finding,
2555
- directory,
2556
- ...finding.file === void 0 ? {} : { file: `${directory}/${finding.file}` },
2557
- remediation: remediation?.kind === "automatic" ? {
2558
- ...remediation,
2559
- operations: [rebaseOperation(remediation.operations[0]), ...remediation.operations.slice(1).map(rebaseOperation)]
2560
- } : remediation?.kind === "manual" ? {
2561
- ...remediation,
2562
- steps: [`Work in ${directory} for this finding.`, ...remediation.steps]
2563
- } : remediation
2564
- };
2565
- };
2566
- //#endregion
2567
- //#region src/core/lint.ts
2568
- const validateLintOptions = (options, paths) => {
2569
- if (!options || !paths.isAbsolute(options.cwd)) throw new UsageError("cwd must be an absolute filesystem path.");
2570
- if ("workspaces" in options) throw new UsageError("workspaces was removed in 0.6.0. Package discovery is recursive by default; use exclude and installationRoots to specify scope.");
2571
- if ("customRules" in options || "reporters" in options) throw new UsageError("Pass customRules and reporters inside the config option.");
2572
- if (options.pm !== void 0 && !isPM(options.pm)) throw new UsageError(`Unknown package manager: ${String(options.pm)}`);
2573
- if (options.projectType !== void 0 && !isProjectType(options.projectType)) throw new UsageError(`Unknown project type: ${String(options.projectType)}`);
2574
- if (options.pmVersion !== void 0 && (!options.pm || !isStableVersion(options.pmVersion))) throw new UsageError("pmVersion / --pm-version requires pm / --pm and an exact stable version such as 10.16.0.");
2575
- };
2576
- /** Validate inputs and prepare shared contexts, without running rules or child executable configs. */
2577
- const prepareLint = (options, dependencies) => {
2578
- validateLintOptions(options, dependencies.paths);
2579
- const config = options.config === void 0 ? void 0 : parseConfig(options.config);
2580
- const fs = options.fs === void 0 ? dependencies.fileSystem : options.fs;
2581
- const excluded = dependencies.compileExclusions(parseExcludes(options.exclude !== void 0 ? options.exclude : config?.exclude ?? []));
2582
- const installations = parseInstallationRoots(options.installationRoots !== void 0 ? options.installationRoots : config?.installationRoots ?? ["."]);
2583
- const configured = applyConfig(dependencies.rules, config);
2584
- const needsCustomTarget = configured.rules.some((rule) => scopeOf(rule.id) === "custom");
2585
- const discovered = discover(options.cwd, fs, excluded, options.projectType ?? config?.projectType, dependencies);
2586
- for (const entry of installations) if (!discovered.some((item) => item.directory === entry.path)) throw new UsageError(`${entry.path}: installation root must be an existing, selected ordinary directory using its exact enumerated spelling (no symlinks, excluded or hard-skipped paths).`);
2587
- return {
2588
- evaluation: {
2589
- directories: discovered.map((item) => {
2590
- const { ctx } = item.repository;
2591
- const entry = installations.find((root) => root.path === item.directory);
2592
- let pms;
2593
- let versions = declaredPMVersion(ctx.packageJson?.packageManager);
2594
- if (item.directory === ".") {
2595
- pms = resolvePMs(ctx, {
2596
- allowed: config?.pms,
2597
- pmOverride: options.pm,
2598
- optional: !entry && !needsCustomTarget
2599
- });
2600
- versions = {
2601
- ...versions,
2602
- ...config?.pmVersions,
2603
- ...options.pm && options.pmVersion ? { [options.pm]: options.pmVersion } : {}
2604
- };
2605
- } else if (entry) {
2606
- try {
2607
- pms = resolvePMs(ctx, { pmOverride: entry.pm });
2608
- } catch (error) {
2609
- if (error instanceof UsageError) throw new UsageError(`${item.directory}: ${error.message}`);
2610
- throw error;
2611
- }
2612
- if (entry.pm && entry.pmVersion) versions = {
2613
- ...versions,
2614
- [entry.pm]: entry.pmVersion
2615
- };
2616
- } else {
2617
- const declared = ctx.packageJson?.packageManager === void 0 ? void 0 : parsePackageManagerField(ctx.packageJson.packageManager);
2618
- pms = PMS.filter((pm) => pm === declared || pm === "deno" && item.manifests.includes("deno.json"));
2619
- }
2620
- return {
2621
- ...item,
2622
- installation: entry !== void 0,
2623
- targets: pms.map((pm) => ({
2624
- pm,
2625
- ...versions[pm] === void 0 ? {} : { version: versions[pm] }
2626
- }))
2627
- };
2628
- }),
2629
- ruleSet: configured.rules,
2630
- severityOverrides: configured.severityOverrides
2631
- },
2632
- reporters: config?.reporters ?? []
2633
- };
2634
- };
2635
- const runPreparedLint = (evaluation) => {
2636
- const { directories, ruleSet, severityOverrides } = evaluation;
2637
- const findings = [];
2638
- const manifests = [];
2639
- const installationRoots = [];
2640
- const ruleOrder = new Map(ruleSet.map((rule, index) => [rule.id, index]));
2641
- for (const item of directories) {
2642
- const { directory, repository, targets, installation } = item;
2643
- const local = [];
2644
- for (const file of item.manifests) {
2645
- const manifestTargets = targets.filter(({ pm }) => file === "deno.json" ? pm === "deno" : pm !== "deno");
2646
- manifests.push({
2647
- path: directory === "." ? file : `${directory}/${file}`,
2648
- projectType: manifestProjectType(repository, file),
2649
- targets: manifestTargets
2650
- });
2651
- local.push(...checkManifest(repository, file, manifestTargets, ruleSet, severityOverrides));
2652
- }
2653
- if (installation) installationRoots.push({
2654
- directory,
2655
- targets
2656
- });
2657
- const scopedRules = ruleSet.flatMap((rule) => {
2658
- const scope = scopeOf(rule.id);
2659
- if (scope === "installation" && installation || scope === "custom" && directory === ".") return [rule];
2660
- if (scope === "split" && installation) return [{
2661
- ...rule,
2662
- bindings: createUnsupportedSettings((file) => file !== "package.json" && file !== "deno.json").bindings
2663
- }];
2664
- return [];
2665
- });
2666
- try {
2667
- local.push(...runLint({
2668
- repository,
2669
- pms: targets.map(({ pm }) => pm),
2670
- pmVersions: Object.fromEntries(targets.map(({ pm, version }) => [pm, version])),
2671
- ruleSet: scopedRules,
2672
- severityOverrides
2673
- }).findings);
2674
- } catch (error) {
2675
- if (error instanceof ConfigError && directory !== ".") throw new ConfigError(`${directory}/${error.message}`);
2676
- throw error;
2677
- }
2678
- local.sort((a, b) => (ruleOrder.get(a.ruleId) ?? 0) - (ruleOrder.get(b.ruleId) ?? 0));
2679
- findings.push(...local.map((finding) => rebaseFinding(directory, finding)));
2680
- }
2681
- const summary = {
2682
- error: 0,
2683
- warn: 0,
2684
- info: 0
2685
- };
2686
- for (const finding of findings) summary[finding.severity] += 1;
2687
- return {
2688
- findings,
2689
- summary,
2690
- inspection: {
2691
- manifests,
2692
- installationRoots
2693
- }
2694
- };
2695
- };
2696
- const lint$1 = (options, dependencies) => runPreparedLint(prepareLint(options, dependencies).evaluation);
2697
- //#endregion
2698
- //#region src/core/filter.ts
2699
- const EXIT_SUCCESS = 0;
2700
- const EXIT_FAILURE = 1;
2701
- const meetsThreshold = (severity, threshold) => SEVERITY_RANK[severity] >= SEVERITY_RANK[threshold];
2702
- /** Keep only findings at or above `threshold`, recomputing the summary. */
2703
- const filterBySeverity = (result, threshold) => {
2704
- const findings = [];
2705
- const summary = {
2706
- error: 0,
2707
- info: 0,
2708
- warn: 0
2709
- };
2710
- for (const finding of result.findings) if (meetsThreshold(finding.severity, threshold)) {
2711
- findings.push(finding);
2712
- summary[finding.severity] += 1;
2713
- }
2714
- return {
2715
- ...result,
2716
- findings,
2717
- summary
2718
- };
2719
- };
2720
- /**
2721
- * Exit code for a lint run. Non-zero when any finding meets `threshold`
2722
- * (default: only `error` fails the run).
2723
- */
2724
- const exitCodeForLint = (result, threshold = "error") => {
2725
- if (result.findings.some((finding) => meetsThreshold(finding.severity, threshold))) return EXIT_FAILURE;
2726
- return EXIT_SUCCESS;
2727
- };
2728
- //#endregion
2729
- //#region src/core/lint-command.ts
2730
- /** Evaluate and report. Executable config loading belongs to the CLI adapter. */
2731
- const lintCommand$1 = async (options, io, dependencies, reporters) => {
2732
- if (!options) throw new UsageError("Lint options are required.");
2733
- if (options.severity !== void 0 && !isSeverity(options.severity)) throw new UsageError(`Invalid severity: ${String(options.severity)}`);
2734
- const prepared = prepareLint(options, dependencies);
2735
- const registry = reporters.createRegistry(prepared.reporters);
2736
- const selection = options.reporter ?? reporters.defaultName;
2737
- const reporter = typeof selection === "string" ? registry.get(selection) : selection;
2738
- if (!isReporterShape(reporter)) throw new UsageError(`${typeof selection === "string" ? "Unknown" : "Invalid"} reporter: ${String(selection)} (available: ${[...registry.keys()].join(", ")})`);
2739
- const result = runPreparedLint(prepared.evaluation);
2740
- const exitCode = exitCodeForLint(result, options.severity ?? "error");
2741
- const writes = [];
2742
- let outputFailed = false;
2743
- let outputFailure;
2744
- const recordFailure = (error) => {
2745
- if (!outputFailed) {
2746
- outputFailed = true;
2747
- outputFailure = error;
2748
- }
2749
- };
2750
- const track = (write) => (line) => {
2751
- try {
2752
- const written = write(line);
2753
- writes.push(Promise.resolve(written).then(() => {}, recordFailure));
2754
- return written;
2755
- } catch (error) {
2756
- recordFailure(error);
2757
- throw error;
2758
- }
2759
- };
2760
- try {
2761
- await reporter.format(filterBySeverity(result, options.severity ?? "info"), {
2762
- stdout: track((line) => io.stdout(line)),
2763
- stderr: track((line) => io.stderr(line))
2764
- }, { cwd: options.cwd });
2765
- } finally {
2766
- await Promise.all(writes);
2767
- }
2768
- if (outputFailed) throw outputFailure;
2769
- return exitCode;
2770
- };
2771
- //#endregion
2772
- //#region src/core/contracts/package-json.ts
2773
- const PackageJsonSchema = vb.looseObject({
2774
- files: vb.optional(vb.array(vb.string())),
2775
- name: vb.optional(vb.string()),
2776
- packageManager: vb.optional(vb.string()),
2777
- private: vb.optional(vb.boolean()),
2778
- publishConfig: vb.optional(vb.looseObject({
2779
- access: vb.optional(vb.picklist([
2780
- "public",
2781
- "restricted",
2782
- "private"
2783
- ])),
2784
- provenance: vb.optional(vb.boolean())
2785
- })),
2786
- trustedDependencies: vb.optional(vb.array(vb.string()))
2787
- });
2788
- const parsePackageJson = (value) => {
2789
- if (!isPlainRecord(value)) throw new ConfigError("package.json: expected an object at the root.");
2790
- const result = vb.safeParse(PackageJsonSchema, value);
2791
- if (result.success) return result.output;
2792
- const issue = result.issues[0];
2793
- const field = vb.getDotPath(issue);
2794
- throw new ConfigError(`package.json: ${field ? `${field}: ` : ""}${issue.message}`);
2795
- };
2796
- //#endregion
2797
- //#region src/adapters/repo-context.ts
2798
- const tryParseJson = (text) => {
2799
- try {
2800
- return JSON.parse(text);
2801
- } catch (error) {
2802
- let errMsg = String(error);
2803
- if (error instanceof Error) errMsg = error.message;
2804
- throw new ConfigError(`package.json: invalid JSON — ${errMsg}`);
2805
- }
2806
- };
2807
- const readPackageJson = (raw) => {
2808
- return parsePackageJson(tryParseJson(raw.trim()));
2809
- };
2810
- const createRepoContext = (root, fs = nodeFileSystem, projectType) => {
2811
- if (fs === nodeFileSystem) assertDirectory(root);
2812
- const packageFile = resolveIn(root, asRelPath("package.json"));
2813
- const raw = fs.readText(packageFile);
2814
- const texts = new Map([[packageFile, raw]]);
2815
- const readText = (relPath) => {
2816
- const file = resolveIn(root, relPath);
2817
- if (!texts.has(file)) texts.set(file, fs.readText(file));
2818
- return texts.get(file);
2819
- };
2820
- const exists = (relPath) => fs.exists(resolveIn(root, relPath));
2821
- let packageJson = void 0;
2822
- if (typeof raw !== "undefined") packageJson = readPackageJson(raw);
2823
- return {
2824
- exists,
2825
- packageJson,
2826
- projectType,
2827
- readText,
2828
- root
2829
- };
2830
- };
2831
- //#endregion
2832
- //#region src/adapters/codecs/ini.ts
2833
- const INTEGER_PATTERN = /^-?\d+$/u;
2834
- const coerceBoolean = (str) => {
2835
- if (str === "true") return true;
2836
- if (str === "false") return false;
2837
- };
2838
- const coerceInteger = (str) => {
2839
- if (INTEGER_PATTERN.test(str)) return Number(str);
2840
- };
2841
- const coerce = (raw) => {
2842
- if (typeof raw !== "string") return raw;
2843
- return coerceBoolean(raw) ?? coerceInteger(raw) ?? raw;
2844
- };
2845
- //#endregion
2846
- //#region src/adapters/codecs/store.ts
2847
- const CODECS = {
2848
- json: { parse(text) {
2849
- const trimmed = text.trim();
2850
- return toParsedConfig(JSON.parse(trimmed));
2851
- } },
2852
- npmrc: { parse(text) {
2853
- const raw = toParsedConfig(ini.parse(text));
2854
- const config = {};
2855
- for (const [key, value] of Object.entries(raw)) if (Array.isArray(value)) config[key] = value.map((elem) => coerce(elem));
2856
- else if (typeof value === "object" && value !== null) config[key] = toParsedConfig(value);
2857
- else config[key] = coerce(value);
2858
- return config;
2859
- } },
2860
- toml: { parse(text) {
2861
- if (text.trim() === "") return {};
2862
- return toParsedConfig(parse$1(text.startsWith("") ? text.slice(1) : text));
2863
- } },
2864
- yaml: { parse(text) {
2865
- if (text.trim() === "") return {};
2866
- const document = parseDocument(text);
2867
- if (document.errors.length > 0) throw document.errors[0];
2868
- if (document.contents === null) return {};
2869
- return toParsedConfig(document.toJS({ onAnchor: (value, count) => {
2870
- if (count > 1) JSON.stringify(value);
2871
- } }));
2872
- } }
2873
- };
2874
- /** Look up the codec for a parseable kind. Total — every CodecKind has one. */
2875
- const codecFor = (kind) => CODECS[kind];
2876
- //#endregion
2877
- //#region src/adapters/reporters/github.ts
2878
- const COMMAND = {
2879
- error: "error",
2880
- info: "notice",
2881
- warn: "warning"
2882
- };
2883
- const escapeData = (raw) => raw.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
2884
- const escapeProp = (raw) => escapeData(raw).replaceAll(":", "%3A").replaceAll(",", "%2C");
2885
- /** Emit GitHub Actions workflow commands (annotations on PRs). */
2886
- const githubReporter = {
2887
- async format(result, io, context) {
2888
- for (const finding of result.findings) {
2889
- let file = "";
2890
- if (finding.file) file = `file=${escapeProp(path.resolve(context.cwd, finding.file))},`;
2891
- let body = `[${finding.pm ?? "package"}] ${finding.directory}: ${finding.message}`;
2892
- if (finding.docs) body += ` (${finding.docs})`;
2893
- await io.stdout(`::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`);
2894
- }
2895
- },
2896
- name: "github"
2897
- };
2898
- //#endregion
2899
- //#region src/adapters/safe-text.ts
2900
- /** Visible escaping for untrusted human-facing text; never rewrite API/native paths. */
2901
- const safeText = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`).replaceAll("##[", "#\\u0023[").replace(/^(\s*)::/u, "$1\\u003a:");
2902
- /** JSON syntax and decoded values stay identical; CI must not parse legacy commands in it. */
2903
- const safeJsonText = (json) => json.replace(/[\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`).replaceAll("##[", "\\u0023\\u0023[");
2904
- //#endregion
2905
- //#region src/adapters/reporters/json.ts
2906
- /**
2907
- * Versioned machine-readable output — the public contract consumed by
2908
- * external fixers (see docs/json-output.md). Bump `schemaVersion` on any
2909
- * breaking shape change and update that doc in the same commit.
2910
- */
2911
- const SCHEMA_VERSION = 3;
2912
- const jsonReporter = {
2913
- async format(result, io) {
2914
- await io.stdout(safeJsonText(JSON.stringify({
2915
- schemaVersion: SCHEMA_VERSION,
2916
- siroVersion: version,
2917
- findings: result.findings,
2918
- summary: result.summary,
2919
- inspection: result.inspection
2920
- }, void 0, 2)));
2921
- },
2922
- name: "json"
2923
- };
2924
- //#endregion
2925
- //#region src/adapters/reporters/pretty.ts
2926
- const GLYPH = {
2927
- error: "✖ error",
2928
- info: "ℹ info",
2929
- warn: "⚠ warn"
2930
- };
2931
- /**
2932
- * Decide colour at call time, not at import time.
2933
- *
2934
- * picocolors snapshots `isColorSupported` when imported, so a later
2935
- * `process.env.NO_COLOR = '1'` (or a different stdout) is ignored. The
2936
- * reporter is invoked per `siro lint` run from many contexts (TTY, CI
2937
- * with FORCE_COLOR, redirected output, …) — checking each call is the
2938
- * only way NO_COLOR can truly win, as https://no-color.org/ requires.
2939
- */
2940
- const colorSupportedNow = () => {
2941
- const { env } = process;
2942
- if (typeof env.NO_COLOR !== "undefined" && env.NO_COLOR !== "") return false;
2943
- if ("FORCE_COLOR" in env) return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "";
2944
- return pc.isColorSupported;
2945
- };
2946
- const renderFinding = (finding, ctx) => {
2947
- const where = ctx.colors.dim(` (${safeText(finding.file ?? finding.directory)})`);
2948
- ctx.io.stdout(`${ctx.tag[finding.severity](GLYPH[finding.severity])} [${finding.pm ?? "package"}] ${ctx.colors.bold(safeText(finding.ruleId))}${where}`);
2949
- ctx.io.stdout(` ${safeText(finding.message)}`);
2950
- for (const step of finding.remediation?.kind === "manual" ? finding.remediation.steps : []) ctx.io.stdout(` ↳ ${safeText(step)}`);
2951
- if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${safeText(finding.docs)}`));
2952
- };
2953
- const buildRenderCtx = (io) => {
2954
- const colors = pc.createColors(colorSupportedNow());
2955
- return {
2956
- colors,
2957
- io,
2958
- tag: {
2959
- error: colors.red,
2960
- info: colors.cyan,
2961
- warn: colors.yellow
2962
- }
2963
- };
2964
- };
2965
- const prettyReporter = {
2966
- async format(result, io) {
2967
- const lines = [];
2968
- const collect = (line) => {
2969
- lines.push(line);
2970
- };
2971
- const ctx = buildRenderCtx({
2972
- stdout: collect,
2973
- stderr: collect
2974
- });
2975
- collect(`Inspection: ${result.inspection.manifests.length} manifests; installation roots: ${result.inspection.installationRoots.map((root) => safeText(root.directory)).join(", ") || "none"}.`);
2976
- collect("Unknown PM/version targets have no availability assessment; installation scope is explicit.");
2977
- if (result.findings.length === 0) collect(ctx.colors.green("✔ No security best-practice issues found."));
2978
- else {
2979
- for (const finding of result.findings) renderFinding(finding, ctx);
2980
- const { error, warn, info } = result.summary;
2981
- collect("");
2982
- collect(`Summary: ${error} error, ${warn} warn, ${info} info`);
2983
- }
2984
- await io.stdout(lines.join("\n"));
2985
- },
2986
- name: "pretty"
2987
- };
2988
- //#endregion
2989
- //#region src/adapters/reporters/registry.ts
2990
- const BUILTINS = [
2991
- prettyReporter,
2992
- jsonReporter,
2993
- githubReporter
2994
- ];
2995
- const DEFAULT_REPORTER_NAME = prettyReporter.name;
2996
- const JSON_REPORTER_NAME = jsonReporter.name;
2997
- /** Names of every built-in reporter, in stable display order. */
2998
- const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
2999
- /** Later registrations replace earlier reporters with the same name. */
3000
- const createRegistry = (extras = []) => new Map([...BUILTINS, ...extras].map((reporter) => [reporter.name, reporter]));
3001
- const dependencies = {
3002
- rules: createBuiltinRules({
3003
- now: () => Date.now(),
3004
- parse: (value) => Date.parse(value)
3005
- }),
3006
- fileSystem: nodeFileSystem,
3007
- paths: nodePaths,
3008
- createRepoContext,
3009
- codecFor,
3010
- compileExclusions
3011
- };
3012
- /** Public Node API: callers can replace the filesystem without assembling the application. */
3013
- const lint = (options) => lint$1(options, dependencies);
3014
- const lintCommand = (options, io) => lintCommand$1(options, io, dependencies, {
3015
- defaultName: DEFAULT_REPORTER_NAME,
3016
- createRegistry
3017
- });
3018
- //#endregion
3019
- //#region src/adapters/node-io.ts
3020
- const writer = (stream) => {
3021
- let failure;
3022
- const pending = /* @__PURE__ */ new Set();
3023
- const fail = (cause) => {
3024
- failure ??= new Error(`Output failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
3025
- for (const reject of pending) reject(failure);
3026
- pending.clear();
3027
- return failure;
3028
- };
3029
- let observing = false;
3030
- return (line) => {
3031
- if (!observing) {
3032
- observing = true;
3033
- stream.on("error", fail);
3034
- stream.on("close", () => fail(/* @__PURE__ */ new Error("Output stream closed.")));
3035
- }
3036
- return new Promise((resolve, reject) => {
3037
- if (failure || stream.destroyed) {
3038
- reject(failure ?? fail(/* @__PURE__ */ new Error("Output stream is destroyed.")));
3039
- return;
3040
- }
3041
- pending.add(reject);
3042
- try {
3043
- stream.write(`${line}\n`, (error) => {
3044
- pending.delete(reject);
3045
- if (error) reject(fail(error));
3046
- else resolve();
3047
- });
3048
- } catch (error) {
3049
- fail(error);
3050
- }
3051
- });
3052
- };
3053
- };
3054
- /** Node-only construction; keep streams and their errors outside the core. */
3055
- const createNodeIO = (stdout, stderr) => ({
3056
- stdout: writer(stdout),
3057
- stderr: writer(stderr)
3058
- });
3059
- const nodeIO = createNodeIO(process.stdout, process.stderr);
3060
- //#endregion
3061
- export { asRelPath as A, isSupportedNodeVersion as C, nodeFileSystem as D, assertDirectory as E, SiroError as M, UsageError as N, isNodeError as O, isSeverity as S, version as T, PROJECT_TYPES as _, DEFAULT_REPORTER_NAME as a, SEVERITIES as b, jsonReporter as c, overrideBindings as d, requireConfigKey as f, getByPath as g, defineRule as h, BUILTIN_REPORTER_NAMES as i, ConfigError as j, asAbsPath as k, safeText as l, loadConfig as m, lint as n, JSON_REPORTER_NAME as o, CONFIG_FILES as p, lintCommand as r, prettyReporter as s, nodeIO as t, githubReporter as u, isProjectType as v, SUPPORTED_NODE_RANGE as w, isPM as x, PMS as y };
3062
-
3063
- //# sourceMappingURL=node-io-CAszdmko.mjs.map