@pho9ubenaa/siro 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/README.md +55 -92
- package/dist/cli.mjs +24 -18
- package/dist/index.d.mts +87 -54
- package/dist/index.mjs +2 -2
- package/dist/{node-io-BDHg4J2w.mjs → node-io-CAszdmko.mjs} +757 -739
- package/package.json +1 -1
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import satisfies from "semver/functions/satisfies.js";
|
|
5
5
|
import * as vb from "valibot";
|
|
6
6
|
import { lt, parse } from "semver";
|
|
7
7
|
import valid from "semver/functions/valid.js";
|
|
8
|
-
import {
|
|
8
|
+
import { Minimatch } from "minimatch";
|
|
9
9
|
import ini from "ini";
|
|
10
10
|
import { parse as parse$1 } from "smol-toml";
|
|
11
11
|
import { parseDocument } from "yaml";
|
|
12
12
|
import pc from "picocolors";
|
|
13
|
-
//#region src/
|
|
13
|
+
//#region src/core/contracts/errors.ts
|
|
14
14
|
var SiroError = class extends Error {
|
|
15
15
|
exitCode;
|
|
16
16
|
constructor(message, exitCode) {
|
|
@@ -47,7 +47,7 @@ const wrapCodecError = (filePath, fn) => {
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
//#endregion
|
|
50
|
-
//#region src/
|
|
50
|
+
//#region src/core/contracts/paths.ts
|
|
51
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
52
|
const asRelPath = (value) => {
|
|
53
53
|
if (!isRelPath(value)) throw new TypeError("Expected a repository-relative path without parent traversal.");
|
|
@@ -62,8 +62,17 @@ const asAbsPath = (value) => {
|
|
|
62
62
|
};
|
|
63
63
|
const nodePaths = {
|
|
64
64
|
isAbsolute: isAbsPath,
|
|
65
|
-
resolve
|
|
66
|
-
|
|
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
|
+
}
|
|
67
76
|
};
|
|
68
77
|
//#endregion
|
|
69
78
|
//#region src/adapters/node-errors.ts
|
|
@@ -78,24 +87,6 @@ const nodeFileSystem = {
|
|
|
78
87
|
readDirectories(directory) {
|
|
79
88
|
return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
80
89
|
},
|
|
81
|
-
resolveDirectory(parent, name) {
|
|
82
|
-
let target;
|
|
83
|
-
try {
|
|
84
|
-
target = lstatSync(path.join(parent, name), { bigint: true });
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
if (!target.isDirectory()) return void 0;
|
|
90
|
-
const names = readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
91
|
-
if (names.includes(name)) return name;
|
|
92
|
-
const matches = names.filter((candidate) => {
|
|
93
|
-
const entry = lstatSync(path.join(parent, candidate), { bigint: true });
|
|
94
|
-
return entry.isDirectory() && entry.dev === target.dev && entry.ino === target.ino;
|
|
95
|
-
});
|
|
96
|
-
if (target.ino === 0n || matches.length !== 1) throw new ConfigError(`${parent}: cannot identify the resolved workspace directory ${name}.`);
|
|
97
|
-
return matches[0];
|
|
98
|
-
},
|
|
99
90
|
exists(filePath) {
|
|
100
91
|
try {
|
|
101
92
|
assertRegularFile(filePath);
|
|
@@ -121,7 +112,7 @@ const assertDirectory = (root) => {
|
|
|
121
112
|
};
|
|
122
113
|
//#endregion
|
|
123
114
|
//#region package.json
|
|
124
|
-
var version$1 = "0.
|
|
115
|
+
var version$1 = "0.6.0";
|
|
125
116
|
var engines = { "node": "^22.18.0 || ^24.0.0" };
|
|
126
117
|
//#endregion
|
|
127
118
|
//#region src/version.ts
|
|
@@ -131,11 +122,20 @@ const SUPPORTED_NODE_RANGE = engines.node;
|
|
|
131
122
|
//#region src/adapters/node-version.ts
|
|
132
123
|
const isSupportedNodeVersion = (version) => satisfies(version, SUPPORTED_NODE_RANGE);
|
|
133
124
|
//#endregion
|
|
134
|
-
//#region src/
|
|
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
|
|
135
134
|
/**
|
|
136
135
|
* Single source of truth for package managers and severities.
|
|
137
|
-
* Types, runtime sets, and ordering
|
|
138
|
-
*
|
|
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
139
|
*/
|
|
140
140
|
const PMS = [
|
|
141
141
|
"npm",
|
|
@@ -171,24 +171,68 @@ const parsePackageManagerField = (value) => {
|
|
|
171
171
|
if (typeof name !== "undefined" && isPM(name)) return name;
|
|
172
172
|
};
|
|
173
173
|
//#endregion
|
|
174
|
-
//#region src/
|
|
175
|
-
|
|
176
|
-
const
|
|
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
|
+
};
|
|
177
186
|
//#endregion
|
|
178
|
-
//#region src/
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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()];
|
|
184
224
|
};
|
|
185
225
|
//#endregion
|
|
186
|
-
//#region src/
|
|
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
|
|
187
231
|
const isReporterShape = (value) => {
|
|
188
232
|
return isPlainRecord(value) && typeof value.name === "string" && typeof value.format === "function";
|
|
189
233
|
};
|
|
190
234
|
//#endregion
|
|
191
|
-
//#region src/
|
|
235
|
+
//#region src/core/contracts/config-value.ts
|
|
192
236
|
const CODEC_KINDS = [
|
|
193
237
|
"json",
|
|
194
238
|
"npmrc",
|
|
@@ -211,14 +255,14 @@ const toParsedConfig = (value) => {
|
|
|
211
255
|
throw new TypeError("Config root must be a mapping.");
|
|
212
256
|
};
|
|
213
257
|
//#endregion
|
|
214
|
-
//#region src/
|
|
258
|
+
//#region src/core/contracts/config-file-ref.ts
|
|
215
259
|
const CONFIG_FILE_KINDS = new Set(CODEC_KINDS);
|
|
216
260
|
const isConfigFileRefShape = (value) => {
|
|
217
261
|
if (!isPlainRecord(value) || typeof value.kind !== "string" || !isRelPath(value.path)) return false;
|
|
218
262
|
return CONFIG_FILE_KINDS.has(value.kind);
|
|
219
263
|
};
|
|
220
264
|
//#endregion
|
|
221
|
-
//#region src/
|
|
265
|
+
//#region src/core/contracts/rule.ts
|
|
222
266
|
const defineRule = (rule) => rule;
|
|
223
267
|
const isOptionalString = (value) => typeof value === "undefined" || typeof value === "string";
|
|
224
268
|
const isConfigValueShape = (value) => typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
@@ -252,22 +296,26 @@ const isRuleShape = (value) => {
|
|
|
252
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);
|
|
253
297
|
};
|
|
254
298
|
//#endregion
|
|
255
|
-
//#region src/
|
|
256
|
-
/**
|
|
257
|
-
const
|
|
258
|
-
if (typeof value
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
/** Keep name-only detection independent from whether the declaration pins a version. */
|
|
263
|
-
const declaredPMVersion = (declaration) => {
|
|
264
|
-
const [pm, version, ...extra] = declaration?.trim().split("@") ?? [];
|
|
265
|
-
return pm && isPM(pm) && isStableVersion(version) && extra.length === 0 ? { [pm]: version } : {};
|
|
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
|
+
}
|
|
266
306
|
};
|
|
267
307
|
//#endregion
|
|
268
|
-
//#region src/
|
|
308
|
+
//#region src/core/parse-siro-config.ts
|
|
269
309
|
const RuleSettingSchema = vb.union([vb.picklist(SEVERITIES), vb.literal("off")]);
|
|
270
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
|
+
})),
|
|
271
319
|
customRules: vb.optional(vb.array(vb.custom(isRuleShape, "must be a structurally valid rule"))),
|
|
272
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)"))),
|
|
273
321
|
projectType: vb.optional(vb.picklist(PROJECT_TYPES)),
|
|
@@ -298,13 +346,14 @@ const formatIssues = (issues) => issues.map((issue) => {
|
|
|
298
346
|
return issue.message;
|
|
299
347
|
}).join("; ");
|
|
300
348
|
const parseConfig = (candidate, name = "siro.config") => {
|
|
349
|
+
assertSynchronous(candidate, name);
|
|
301
350
|
if (!isPlainRecord(candidate)) throw new ConfigError(`${name} must export a config object (got ${Array.isArray(candidate) ? "an array" : typeof candidate}).`);
|
|
302
351
|
const result = vb.safeParse(ConfigSchema, Object.fromEntries(Object.entries(candidate)));
|
|
303
352
|
if (!result.success) throw new ConfigError(`${name}: ${formatIssues(result.issues)}`);
|
|
304
353
|
return result.output;
|
|
305
354
|
};
|
|
306
355
|
//#endregion
|
|
307
|
-
//#region src/
|
|
356
|
+
//#region src/load-config.ts
|
|
308
357
|
const CONFIG_NAMES = [
|
|
309
358
|
"siro.config.ts",
|
|
310
359
|
"siro.config.mjs",
|
|
@@ -328,7 +377,7 @@ const loadConfig = async (cwd, nodeVersion = process.versions.node) => {
|
|
|
328
377
|
return parseConfig(mod !== null && typeof mod === "object" && "default" in mod ? mod.default : mod, name);
|
|
329
378
|
};
|
|
330
379
|
//#endregion
|
|
331
|
-
//#region src/
|
|
380
|
+
//#region src/core/config-files.ts
|
|
332
381
|
/** Known configuration locations and their parsers. */
|
|
333
382
|
const CONFIG_FILES = {
|
|
334
383
|
aubeWorkspace: {
|
|
@@ -361,7 +410,7 @@ const CONFIG_FILES = {
|
|
|
361
410
|
}
|
|
362
411
|
};
|
|
363
412
|
//#endregion
|
|
364
|
-
//#region src/
|
|
413
|
+
//#region src/core/rules/setting-availability.ts
|
|
365
414
|
const { npmrc: npmrc$7, packageJson: packageJson$2, pnpmWorkspace: pnpmWorkspace$11, yarnrc: yarnrc$10, bunfig: bunfig$6 } = CONFIG_FILES;
|
|
366
415
|
const settingAvailability = [
|
|
367
416
|
{
|
|
@@ -482,7 +531,7 @@ const settingAvailability = [
|
|
|
482
531
|
}
|
|
483
532
|
];
|
|
484
533
|
//#endregion
|
|
485
|
-
//#region src/
|
|
534
|
+
//#region src/core/rules/remediation-availability.ts
|
|
486
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]);
|
|
487
536
|
/** Undefined means the target version or the setting's introduction is unknown. */
|
|
488
537
|
const settingSupportedByTarget = (pm, version, target) => {
|
|
@@ -501,7 +550,7 @@ const guardRemediationAvailability = (pm, version, remediation, targets = remedi
|
|
|
501
550
|
};
|
|
502
551
|
};
|
|
503
552
|
//#endregion
|
|
504
|
-
//#region src/
|
|
553
|
+
//#region src/core/rules/remediation.ts
|
|
505
554
|
/** Scalar writes must not discard an existing container or replace a non-object parent. */
|
|
506
555
|
const proposeChanges = (config, operations) => {
|
|
507
556
|
for (const operation of operations) {
|
|
@@ -525,7 +574,7 @@ const proposeChanges = (config, operations) => {
|
|
|
525
574
|
};
|
|
526
575
|
};
|
|
527
576
|
//#endregion
|
|
528
|
-
//#region src/
|
|
577
|
+
//#region src/core/rules/builders/require-config-key.ts
|
|
529
578
|
const overrideBindings = (rule, overrides) => ({
|
|
530
579
|
...rule,
|
|
531
580
|
bindings: {
|
|
@@ -536,7 +585,7 @@ const overrideBindings = (rule, overrides) => ({
|
|
|
536
585
|
const accepts = (spec, actual) => spec.accept ? spec.accept(actual) : actual === spec.value;
|
|
537
586
|
const checkKeyValue = (spec, config) => {
|
|
538
587
|
const actual = getByPath(config, spec.keyPath);
|
|
539
|
-
const coveredByDefault = actual === void 0 && spec.documentedDefault !== void 0 && spec.
|
|
588
|
+
const coveredByDefault = actual === void 0 && spec.documentedDefault !== void 0 && spec.defaultSafety === "unconditional" && accepts(spec, spec.documentedDefault);
|
|
540
589
|
if (!coveredByDefault && accepts(spec, actual)) return { state: "ok" };
|
|
541
590
|
const severity = coveredByDefault ? spec.defaultSatisfiedSeverity ?? "info" : void 0;
|
|
542
591
|
if (severity === "off") return { state: "ok" };
|
|
@@ -589,7 +638,7 @@ const requireConfigKey = (options) => {
|
|
|
589
638
|
};
|
|
590
639
|
};
|
|
591
640
|
//#endregion
|
|
592
|
-
//#region src/
|
|
641
|
+
//#region src/core/rules/builders/with-aube-paranoid.ts
|
|
593
642
|
const withAubeParanoid = (rule) => {
|
|
594
643
|
const { aube } = rule.bindings;
|
|
595
644
|
if (!aube) return rule;
|
|
@@ -602,13 +651,14 @@ const withAubeParanoid = (rule) => {
|
|
|
602
651
|
} });
|
|
603
652
|
};
|
|
604
653
|
//#endregion
|
|
605
|
-
//#region src/
|
|
654
|
+
//#region src/core/rules/advisory-check.ts
|
|
606
655
|
const { aubeWorkspace: aubeWorkspace$8 } = CONFIG_FILES;
|
|
607
656
|
const advisoryCheck = withAubeParanoid(requireConfigKey({
|
|
608
657
|
bindings: { aube: {
|
|
609
658
|
accept: (value) => value === "on" || value === "required",
|
|
610
659
|
docs: "https://aube.jdx.dev/security.html",
|
|
611
660
|
documentedDefault: "on",
|
|
661
|
+
defaultSafety: "unconditional",
|
|
612
662
|
file: aubeWorkspace$8,
|
|
613
663
|
keyPath: ["advisoryCheck"],
|
|
614
664
|
message: "Set `advisoryCheck: on` (or `required`) in aube-workspace.yaml to check dependencies against known-malicious advisories.",
|
|
@@ -621,11 +671,11 @@ const advisoryCheck = withAubeParanoid(requireConfigKey({
|
|
|
621
671
|
title: "Enable malicious-package advisory checks"
|
|
622
672
|
}));
|
|
623
673
|
//#endregion
|
|
624
|
-
//#region src/
|
|
674
|
+
//#region src/core/rules/config-predicates.ts
|
|
625
675
|
const isNonBlankString = (value) => typeof value === "string" && value.trim().length > 0;
|
|
626
676
|
const isStringList = (value) => Array.isArray(value) && Array.from(value).every(isNonBlankString);
|
|
627
677
|
//#endregion
|
|
628
|
-
//#region src/
|
|
678
|
+
//#region src/core/rules/approved-git-repos.ts
|
|
629
679
|
const { yarnrc: yarnrc$9 } = CONFIG_FILES;
|
|
630
680
|
const message = "Set `approvedGitRepositories: []` in .yarnrc.yml to block all git: protocol dependencies (or list approved repository globs).";
|
|
631
681
|
const approvedGitRepos = defineRule({
|
|
@@ -655,7 +705,7 @@ const approvedGitRepos = defineRule({
|
|
|
655
705
|
title: "Approve git repository dependencies"
|
|
656
706
|
});
|
|
657
707
|
//#endregion
|
|
658
|
-
//#region src/
|
|
708
|
+
//#region src/core/rules/audit-suppression.ts
|
|
659
709
|
const { yarnrc: yarnrc$8 } = CONFIG_FILES;
|
|
660
710
|
const isNonEmptyArray = (value) => Array.isArray(value) && value.length > 0;
|
|
661
711
|
const SUPPRESSION_KEYS = ["npmAuditIgnoreAdvisories", "npmAuditExcludePackages"];
|
|
@@ -688,7 +738,7 @@ const auditSuppression = defineRule({
|
|
|
688
738
|
title: "Review audit suppression entries"
|
|
689
739
|
});
|
|
690
740
|
//#endregion
|
|
691
|
-
//#region src/
|
|
741
|
+
//#region src/core/rules/block-auto-install.ts
|
|
692
742
|
const { bunfig: bunfig$5 } = CONFIG_FILES;
|
|
693
743
|
const blockAutoInstall = requireConfigKey({
|
|
694
744
|
bindings: { bun: {
|
|
@@ -706,7 +756,7 @@ const blockAutoInstall = requireConfigKey({
|
|
|
706
756
|
title: "Block automatic package installation"
|
|
707
757
|
});
|
|
708
758
|
//#endregion
|
|
709
|
-
//#region src/
|
|
759
|
+
//#region src/core/rules/block-exotic-subdeps.ts
|
|
710
760
|
const { aubeWorkspace: aubeWorkspace$7, npmrc: npmrc$6, pnpmWorkspace: pnpmWorkspace$10 } = CONFIG_FILES;
|
|
711
761
|
const isRootOrNone = (value) => value === "root" || value === "none";
|
|
712
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.";
|
|
@@ -743,14 +793,16 @@ const blockExoticSubdeps = overrideBindings(requireConfigKey({
|
|
|
743
793
|
aube: {
|
|
744
794
|
docs: "https://aube.jdx.dev/security.html",
|
|
745
795
|
documentedDefault: true,
|
|
796
|
+
defaultSafety: "unconditional",
|
|
746
797
|
file: aubeWorkspace$7,
|
|
747
798
|
keyPath: ["blockExoticSubdeps"],
|
|
748
799
|
message: "Set `blockExoticSubdeps: true` in aube-workspace.yaml to block git/tarball URLs in transitive dependencies.",
|
|
749
800
|
value: true
|
|
750
801
|
},
|
|
751
802
|
pnpm: {
|
|
752
|
-
docs: "https://pnpm.io/settings#blockexoticsubdeps",
|
|
803
|
+
docs: "https://pnpm.io/settings/dependency-resolution#blockexoticsubdeps",
|
|
753
804
|
documentedDefault: true,
|
|
805
|
+
defaultSafety: "conditional",
|
|
754
806
|
file: pnpmWorkspace$10,
|
|
755
807
|
keyPath: ["blockExoticSubdeps"],
|
|
756
808
|
message: "Set `blockExoticSubdeps: true` in pnpm-workspace.yaml to block git/tarball URLs in transitive dependencies.",
|
|
@@ -762,13 +814,13 @@ const blockExoticSubdeps = overrideBindings(requireConfigKey({
|
|
|
762
814
|
}
|
|
763
815
|
},
|
|
764
816
|
description: "Refuse to install transitive dependencies sourced from git or tarball URLs, which bypass registry integrity checking.",
|
|
765
|
-
docs: "https://pnpm.io/settings#blockexoticsubdeps",
|
|
817
|
+
docs: "https://pnpm.io/settings/dependency-resolution#blockexoticsubdeps",
|
|
766
818
|
id: "block-exotic-subdeps",
|
|
767
819
|
severity: "warn",
|
|
768
820
|
title: "Block exotic subdependencies"
|
|
769
821
|
}), { npm: npmBinding$1 });
|
|
770
822
|
//#endregion
|
|
771
|
-
//#region src/
|
|
823
|
+
//#region src/core/rules/bun-security-scanner.ts
|
|
772
824
|
const { bunfig: bunfig$4 } = CONFIG_FILES;
|
|
773
825
|
const bunSecurityScanner = defineRule({
|
|
774
826
|
bindings: { bun: {
|
|
@@ -807,12 +859,13 @@ const bunSecurityScanner = defineRule({
|
|
|
807
859
|
title: "Enable a bun install-time security scanner"
|
|
808
860
|
});
|
|
809
861
|
//#endregion
|
|
810
|
-
//#region src/
|
|
862
|
+
//#region src/core/rules/checksum-verification.ts
|
|
811
863
|
const { yarnrc: yarnrc$7 } = CONFIG_FILES;
|
|
812
864
|
const checksumVerification = requireConfigKey({
|
|
813
865
|
bindings: { yarn: {
|
|
814
866
|
docs: "https://yarnpkg.com/configuration/yarnrc#checksumBehavior",
|
|
815
867
|
documentedDefault: "throw",
|
|
868
|
+
defaultSafety: "unconditional",
|
|
816
869
|
file: yarnrc$7,
|
|
817
870
|
keyPath: ["checksumBehavior"],
|
|
818
871
|
message: "Set `checksumBehavior: throw` in .yarnrc.yml to reject packages whose checksum does not match the lockfile.",
|
|
@@ -825,7 +878,7 @@ const checksumVerification = requireConfigKey({
|
|
|
825
878
|
title: "Verify package checksums"
|
|
826
879
|
});
|
|
827
880
|
//#endregion
|
|
828
|
-
//#region src/
|
|
881
|
+
//#region src/core/signals.ts
|
|
829
882
|
const PM_SIGNALS = {
|
|
830
883
|
aube: {
|
|
831
884
|
configs: [CONFIG_FILES.aubeWorkspace.path],
|
|
@@ -860,11 +913,11 @@ const PM_SIGNALS = {
|
|
|
860
913
|
}
|
|
861
914
|
};
|
|
862
915
|
//#endregion
|
|
863
|
-
//#region src/
|
|
916
|
+
//#region src/core/rules/commit-lockfile.ts
|
|
864
917
|
const LOCKFILE_DOCS = {
|
|
865
918
|
aube: "https://github.com/aubepkg/aube/blob/main/docs/package-manager/lockfiles.md",
|
|
866
919
|
bun: "https://bun.com/docs/install/lockfile",
|
|
867
|
-
deno: "https://docs.deno.com/runtime/
|
|
920
|
+
deno: "https://docs.deno.com/runtime/reference/deno_json/#lockfile",
|
|
868
921
|
npm: "https://docs.npmjs.com/cli/v12/configuring-npm/package-lock-json",
|
|
869
922
|
pnpm: "https://pnpm.io/git#lockfiles",
|
|
870
923
|
yarn: "https://yarnpkg.com/getting-started/qa#should-lockfiles-be-committed-to-the-repository"
|
|
@@ -873,7 +926,7 @@ const LOCKFILE_VERSION_NOTES = { deno: { configAvailableSince: "deno 1.28.0" } }
|
|
|
873
926
|
const lockfileBinding = (pm) => {
|
|
874
927
|
const { lockfiles, reusesLockfiles } = PM_SIGNALS[pm];
|
|
875
928
|
const [primary] = lockfiles;
|
|
876
|
-
const accepted = [...lockfiles, ...reusesLockfiles ?? []]
|
|
929
|
+
const accepted = [...lockfiles, ...reusesLockfiles ?? []];
|
|
877
930
|
return {
|
|
878
931
|
check(ctx) {
|
|
879
932
|
if (pm === "aube" && ctx.exists(asRelPath("bun.lockb")) && !ctx.exists(asRelPath("bun.lock"))) return {
|
|
@@ -884,7 +937,20 @@ const lockfileBinding = (pm) => {
|
|
|
884
937
|
steps: ["Run `bun install --save-text-lockfile` to generate bun.lock, then commit it."]
|
|
885
938
|
}
|
|
886
939
|
};
|
|
887
|
-
if (
|
|
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" };
|
|
888
954
|
return {
|
|
889
955
|
remediation: {
|
|
890
956
|
kind: "manual",
|
|
@@ -935,13 +1001,13 @@ const commitLockfile = defineRule({
|
|
|
935
1001
|
yarn: lockfileBinding("yarn")
|
|
936
1002
|
},
|
|
937
1003
|
description: "Lockfiles pin the full dependency tree and integrity hashes, enabling reproducible, verifiable installs (e.g. `npm ci`).",
|
|
938
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1004
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#3-include-lockfiles",
|
|
939
1005
|
id: "commit-lockfile",
|
|
940
1006
|
severity: "error",
|
|
941
1007
|
title: "Commit a lockfile"
|
|
942
1008
|
});
|
|
943
1009
|
//#endregion
|
|
944
|
-
//#region src/
|
|
1010
|
+
//#region src/core/rules/dependency-overrides.ts
|
|
945
1011
|
const { aubeWorkspace: aubeWorkspace$6, pnpmWorkspace: pnpmWorkspace$9 } = CONFIG_FILES;
|
|
946
1012
|
const isNonEmptyObject$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
947
1013
|
const checkOverrides = (config, file) => {
|
|
@@ -967,16 +1033,16 @@ const makeBinding = (file, fileName, docs) => ({
|
|
|
967
1033
|
const dependencyOverrides = defineRule({
|
|
968
1034
|
bindings: {
|
|
969
1035
|
aube: makeBinding(aubeWorkspace$6, "aube-workspace.yaml", "https://aube.sh/settings/"),
|
|
970
|
-
pnpm: makeBinding(pnpmWorkspace$9, "pnpm-workspace.yaml", "https://pnpm.io/settings#overrides")
|
|
1036
|
+
pnpm: makeBinding(pnpmWorkspace$9, "pnpm-workspace.yaml", "https://pnpm.io/settings/dependency-resolution#overrides")
|
|
971
1037
|
},
|
|
972
1038
|
description: "Flag dependency overrides that can replace transitive packages with arbitrary versions or forks — a supply-chain injection vector.",
|
|
973
|
-
docs: "https://pnpm.io/settings#overrides",
|
|
1039
|
+
docs: "https://pnpm.io/settings/dependency-resolution#overrides",
|
|
974
1040
|
id: "dependency-overrides",
|
|
975
1041
|
severity: "info",
|
|
976
1042
|
title: "Review dependency overrides"
|
|
977
1043
|
});
|
|
978
1044
|
//#endregion
|
|
979
|
-
//#region src/
|
|
1045
|
+
//#region src/core/rules/disable-lifecycle-scripts.ts
|
|
980
1046
|
const { npmrc: npmrc$5, pnpmWorkspace: pnpmWorkspace$8, yarnrc: yarnrc$6, aubeWorkspace: aubeWorkspace$5, bunfig: bunfig$3 } = CONFIG_FILES;
|
|
981
1047
|
const pnpmBinding$4 = {
|
|
982
1048
|
check(ctx, config) {
|
|
@@ -1050,7 +1116,7 @@ const pnpmBinding$4 = {
|
|
|
1050
1116
|
}])
|
|
1051
1117
|
};
|
|
1052
1118
|
},
|
|
1053
|
-
docs: "https://pnpm.io/settings#strictdepbuilds",
|
|
1119
|
+
docs: "https://pnpm.io/settings/build#strictdepbuilds",
|
|
1054
1120
|
file: pnpmWorkspace$8,
|
|
1055
1121
|
versionNote: {
|
|
1056
1122
|
configAvailableSince: "pnpm 10.3.0",
|
|
@@ -1137,6 +1203,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
|
|
|
1137
1203
|
yarn: {
|
|
1138
1204
|
docs: "https://yarnpkg.com/configuration/yarnrc#enableScripts",
|
|
1139
1205
|
documentedDefault: false,
|
|
1206
|
+
defaultSafety: "conditional",
|
|
1140
1207
|
file: yarnrc$6,
|
|
1141
1208
|
keyPath: ["enableScripts"],
|
|
1142
1209
|
message: "Set `enableScripts: false` in .yarnrc.yml to pin the policy across versions.",
|
|
@@ -1148,7 +1215,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
|
|
|
1148
1215
|
}
|
|
1149
1216
|
},
|
|
1150
1217
|
description: "Malicious postinstall scripts are a primary supply-chain attack vector. Prevent automatic execution of dependency lifecycle scripts.",
|
|
1151
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1218
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#1-disable-lifecycle-scripts",
|
|
1152
1219
|
id: "disable-lifecycle-scripts",
|
|
1153
1220
|
severity: "error",
|
|
1154
1221
|
title: "Disable dependency lifecycle scripts"
|
|
@@ -1158,7 +1225,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
|
|
|
1158
1225
|
pnpm: pnpmBinding$4
|
|
1159
1226
|
}));
|
|
1160
1227
|
//#endregion
|
|
1161
|
-
//#region src/
|
|
1228
|
+
//#region src/core/rules/enforce-strict-ssl.ts
|
|
1162
1229
|
const { npmrc: npmrc$4, yarnrc: yarnrc$5 } = CONFIG_FILES;
|
|
1163
1230
|
const yarnMessage = "Set `enableStrictSsl: true` in .yarnrc.yml to enforce SSL certificate validation for registry connections.";
|
|
1164
1231
|
const yarnBinding = {
|
|
@@ -1197,6 +1264,7 @@ const enforceStrictSsl = overrideBindings(requireConfigKey({
|
|
|
1197
1264
|
bindings: { npm: {
|
|
1198
1265
|
docs: "https://docs.npmjs.com/cli/v11/using-npm/config#strict-ssl",
|
|
1199
1266
|
documentedDefault: true,
|
|
1267
|
+
defaultSafety: "unconditional",
|
|
1200
1268
|
file: npmrc$4,
|
|
1201
1269
|
keyPath: ["strict-ssl"],
|
|
1202
1270
|
message: "Set `strict-ssl=true` in .npmrc to enforce SSL certificate validation.",
|
|
@@ -1209,7 +1277,7 @@ const enforceStrictSsl = overrideBindings(requireConfigKey({
|
|
|
1209
1277
|
title: "Enforce SSL for registry connections"
|
|
1210
1278
|
}), { yarn: yarnBinding });
|
|
1211
1279
|
//#endregion
|
|
1212
|
-
//#region src/
|
|
1280
|
+
//#region src/core/resolve-project-type.ts
|
|
1213
1281
|
const isPublishableName = (value) => typeof value === "string" && value.trim().length > 0;
|
|
1214
1282
|
const resolveProjectType = (selected, inferredPackage) => selected ?? (inferredPackage ? "package" : "application");
|
|
1215
1283
|
const resolvePackageJsonProjectType = (ctx) => {
|
|
@@ -1219,10 +1287,10 @@ const resolvePackageJsonProjectType = (ctx) => {
|
|
|
1219
1287
|
};
|
|
1220
1288
|
const resolveDenoProjectType = (ctx, denoConfig) => resolveProjectType(ctx.projectType, getByPath(denoConfig, ["publish"]) !== false && isPublishableName(getByPath(denoConfig, ["name"])));
|
|
1221
1289
|
//#endregion
|
|
1222
|
-
//#region src/
|
|
1290
|
+
//#region src/core/rules/publishable.ts
|
|
1223
1291
|
const isPublishable = (ctx) => resolvePackageJsonProjectType(ctx) === "package";
|
|
1224
1292
|
//#endregion
|
|
1225
|
-
//#region src/
|
|
1293
|
+
//#region src/core/rules/files-field.ts
|
|
1226
1294
|
const { packageJson: packageJson$1, denoJson: denoJson$3 } = CONFIG_FILES;
|
|
1227
1295
|
const packageJsonFilesBinding = {
|
|
1228
1296
|
check(ctx) {
|
|
@@ -1242,47 +1310,48 @@ const packageJsonFilesBinding = {
|
|
|
1242
1310
|
docs: "https://docs.npmjs.com/cli/v11/configuring-npm/package-json#files",
|
|
1243
1311
|
file: packageJson$1
|
|
1244
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
|
+
};
|
|
1245
1331
|
const filesField = defineRule({
|
|
1246
1332
|
bindings: {
|
|
1247
1333
|
aube: packageJsonFilesBinding,
|
|
1248
1334
|
bun: packageJsonFilesBinding,
|
|
1249
|
-
deno:
|
|
1250
|
-
check(ctx, config) {
|
|
1251
|
-
if (resolveDenoProjectType(ctx, config) !== "package") return { state: "na" };
|
|
1252
|
-
const include = getByPath(config, ["publish", "include"]);
|
|
1253
|
-
if (isStringList(include) && include.length > 0) return { state: "ok" };
|
|
1254
|
-
return {
|
|
1255
|
-
remediation: {
|
|
1256
|
-
kind: "manual",
|
|
1257
|
-
steps: ["Add a non-empty `publish.include` array to deno.json; use `publish.exclude` for additional exclusions."]
|
|
1258
|
-
},
|
|
1259
|
-
actual: include,
|
|
1260
|
-
message: "Add `publish.include` to deno.json to limit what gets published.",
|
|
1261
|
-
state: "violation"
|
|
1262
|
-
};
|
|
1263
|
-
},
|
|
1264
|
-
docs: "https://docs.deno.com/runtime/reference/cli/publish/#how-publishing-works",
|
|
1265
|
-
file: denoJson$3
|
|
1266
|
-
},
|
|
1335
|
+
deno: denoPublishBinding,
|
|
1267
1336
|
npm: packageJsonFilesBinding,
|
|
1268
1337
|
pnpm: packageJsonFilesBinding,
|
|
1269
1338
|
yarn: packageJsonFilesBinding
|
|
1270
1339
|
},
|
|
1271
1340
|
description: "An explicit `files` array in package.json restricts what gets published, preventing accidental inclusion of secrets or local files.",
|
|
1272
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1341
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#12-review-published-files",
|
|
1273
1342
|
id: "files-field",
|
|
1274
1343
|
projectTypes: ["package"],
|
|
1275
1344
|
severity: "info",
|
|
1276
1345
|
title: "Declare a published files allow-list"
|
|
1277
1346
|
});
|
|
1278
1347
|
//#endregion
|
|
1279
|
-
//#region src/
|
|
1348
|
+
//#region src/core/rules/frozen-lockfile.ts
|
|
1280
1349
|
const { pnpmWorkspace: pnpmWorkspace$7, yarnrc: yarnrc$4, bunfig: bunfig$2, denoJson: denoJson$2 } = CONFIG_FILES;
|
|
1281
1350
|
const aubeFrozenCommand = "Use `aube ci` or `aube install --frozen-lockfile` to fail when the lockfile would change.";
|
|
1282
1351
|
const frozenLockfile = overrideBindings(requireConfigKey({
|
|
1283
1352
|
bindings: {
|
|
1284
1353
|
deno: {
|
|
1285
|
-
docs: "https://docs.deno.com/runtime/
|
|
1354
|
+
docs: "https://docs.deno.com/runtime/reference/deno_json/#lockfile",
|
|
1286
1355
|
file: denoJson$2,
|
|
1287
1356
|
keyPath: ["lock", "frozen"],
|
|
1288
1357
|
message: "Set `lock.frozen: true` in deno.json for reproducible, verified installs.",
|
|
@@ -1297,7 +1366,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1297
1366
|
versionNote: { configAvailableSince: "bun 0.6.10" }
|
|
1298
1367
|
},
|
|
1299
1368
|
pnpm: {
|
|
1300
|
-
docs: "https://pnpm.io/
|
|
1369
|
+
docs: "https://pnpm.io/cli/install#--frozen-lockfile",
|
|
1301
1370
|
file: pnpmWorkspace$7,
|
|
1302
1371
|
keyPath: ["frozenLockfile"],
|
|
1303
1372
|
message: "pnpm defaults frozenLockfile to true in CI — set it explicitly in pnpm-workspace.yaml to pin the policy outside CI too.",
|
|
@@ -1306,6 +1375,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1306
1375
|
yarn: {
|
|
1307
1376
|
docs: "https://yarnpkg.com/configuration/yarnrc#enableImmutableInstalls",
|
|
1308
1377
|
documentedDefault: true,
|
|
1378
|
+
defaultSafety: "conditional",
|
|
1309
1379
|
file: yarnrc$4,
|
|
1310
1380
|
keyPath: ["enableImmutableInstalls"],
|
|
1311
1381
|
message: "Set `enableImmutableInstalls: true` in .yarnrc.yml to pin the policy outside CI too.",
|
|
@@ -1317,7 +1387,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1317
1387
|
}
|
|
1318
1388
|
},
|
|
1319
1389
|
description: "Refuse to mutate the lockfile on install so unexpected dependency changes fail loudly.",
|
|
1320
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1390
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#3-include-lockfiles",
|
|
1321
1391
|
id: "frozen-lockfile",
|
|
1322
1392
|
severity: "warn",
|
|
1323
1393
|
title: "Freeze the lockfile"
|
|
@@ -1336,7 +1406,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1336
1406
|
severity: "info"
|
|
1337
1407
|
} });
|
|
1338
1408
|
//#endregion
|
|
1339
|
-
//#region src/
|
|
1409
|
+
//#region src/core/rules/frozen-store.ts
|
|
1340
1410
|
const { pnpmWorkspace: pnpmWorkspace$6 } = CONFIG_FILES;
|
|
1341
1411
|
const frozenStore = defineRule({
|
|
1342
1412
|
bindings: { pnpm: {
|
|
@@ -1367,12 +1437,13 @@ const frozenStore = defineRule({
|
|
|
1367
1437
|
title: "Consider a pre-populated read-only store"
|
|
1368
1438
|
});
|
|
1369
1439
|
//#endregion
|
|
1370
|
-
//#region src/
|
|
1440
|
+
//#region src/core/rules/hardened-mode.ts
|
|
1371
1441
|
const { yarnrc: yarnrc$3 } = CONFIG_FILES;
|
|
1372
1442
|
const hardenedMode = requireConfigKey({
|
|
1373
1443
|
bindings: { yarn: {
|
|
1374
1444
|
docs: "https://yarnpkg.com/configuration/yarnrc#enableHardenedMode",
|
|
1375
1445
|
documentedDefault: true,
|
|
1446
|
+
defaultSafety: "conditional",
|
|
1376
1447
|
file: yarnrc$3,
|
|
1377
1448
|
keyPath: ["enableHardenedMode"],
|
|
1378
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.",
|
|
@@ -1389,7 +1460,7 @@ const hardenedMode = requireConfigKey({
|
|
|
1389
1460
|
title: "Enable hardened mode (yarn)"
|
|
1390
1461
|
});
|
|
1391
1462
|
//#endregion
|
|
1392
|
-
//#region src/
|
|
1463
|
+
//#region src/core/rules/deno-release-age.ts
|
|
1393
1464
|
const DENO_DURATION = /^\+?P(?:\+?\d+[Ww]|(?:\d+[Dd])*(?:T(?:\d+[HhMm]|\d+(?:\.\d+)?[Ss])+)?)$/u;
|
|
1394
1465
|
const DENO_MIN_TIMESTAMP = Date.UTC(-262143, 0, 1);
|
|
1395
1466
|
const DENO_UNIT_SECONDS = {
|
|
@@ -1427,7 +1498,7 @@ const isActiveDenoReleaseAge = (value, now, parse) => {
|
|
|
1427
1498
|
return !Number.isNaN(midnight.valueOf()) && midnight.toISOString().slice(0, 10) === date && timestamp < now;
|
|
1428
1499
|
};
|
|
1429
1500
|
//#endregion
|
|
1430
|
-
//#region src/
|
|
1501
|
+
//#region src/core/rules/minimum-release-age.ts
|
|
1431
1502
|
const HOURS_PER_DAY = 24;
|
|
1432
1503
|
const MINUTES_PER_HOUR = 60;
|
|
1433
1504
|
const SECONDS_PER_MINUTE = 60;
|
|
@@ -1444,7 +1515,7 @@ const isNonDisabledDenoReleaseAge = (value, now, parse) => {
|
|
|
1444
1515
|
if (!isPlainRecord(value)) return isActiveDenoReleaseAge(value, now, parse);
|
|
1445
1516
|
if (Object.keys(value).some((key) => key !== "age" && key !== "exclude")) return false;
|
|
1446
1517
|
if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
|
|
1447
|
-
return value.age
|
|
1518
|
+
return value.age != null && isActiveDenoReleaseAge(value.age, now, parse);
|
|
1448
1519
|
};
|
|
1449
1520
|
const denoAgeUsesFallback = (value) => {
|
|
1450
1521
|
if (value === void 0 || value === null) return true;
|
|
@@ -1453,13 +1524,14 @@ const denoAgeUsesFallback = (value) => {
|
|
|
1453
1524
|
if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
|
|
1454
1525
|
return value.age == null;
|
|
1455
1526
|
};
|
|
1456
|
-
const isPositiveDenoNpmrcDays = (value) => typeof value === "number" && Number.isSafeInteger(value) && value
|
|
1457
|
-
const baseRule$
|
|
1527
|
+
const isPositiveDenoNpmrcDays = (value, now, parse) => typeof value === "number" && Number.isSafeInteger(value) && isActiveDenoReleaseAge(value * MINUTES_PER_DAY, now, parse);
|
|
1528
|
+
const baseRule$2 = requireConfigKey({
|
|
1458
1529
|
bindings: {
|
|
1459
1530
|
aube: {
|
|
1460
1531
|
accept: isPositiveNumber,
|
|
1461
1532
|
docs: "https://aube.sh/settings/",
|
|
1462
1533
|
documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
|
|
1534
|
+
defaultSafety: "unconditional",
|
|
1463
1535
|
file: aubeWorkspace$4,
|
|
1464
1536
|
keyPath: ["minimumReleaseAge"],
|
|
1465
1537
|
message: `Set minimumReleaseAge (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in aube-workspace.yaml.`,
|
|
@@ -1476,8 +1548,9 @@ const baseRule$1 = requireConfigKey({
|
|
|
1476
1548
|
},
|
|
1477
1549
|
pnpm: {
|
|
1478
1550
|
accept: isPositiveNumber,
|
|
1479
|
-
docs: "https://pnpm.io/settings#minimumreleaseage",
|
|
1551
|
+
docs: "https://pnpm.io/settings/dependency-resolution#minimumreleaseage",
|
|
1480
1552
|
documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
|
|
1553
|
+
defaultSafety: "conditional",
|
|
1481
1554
|
file: pnpmWorkspace$5,
|
|
1482
1555
|
keyPath: ["minimumReleaseAge"],
|
|
1483
1556
|
message: `Set minimumReleaseAge (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in pnpm-workspace.yaml.`,
|
|
@@ -1491,6 +1564,7 @@ const baseRule$1 = requireConfigKey({
|
|
|
1491
1564
|
accept: isPositiveYarnDuration,
|
|
1492
1565
|
docs: "https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate",
|
|
1493
1566
|
documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
|
|
1567
|
+
defaultSafety: "conditional",
|
|
1494
1568
|
file: yarnrc$2,
|
|
1495
1569
|
keyPath: ["npmMinimalAgeGate"],
|
|
1496
1570
|
message: `Set npmMinimalAgeGate (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in .yarnrc.yml.`,
|
|
@@ -1502,7 +1576,7 @@ const baseRule$1 = requireConfigKey({
|
|
|
1502
1576
|
}
|
|
1503
1577
|
},
|
|
1504
1578
|
description: "Refuse to install releases newer than a cooldown window so freshly published (possibly compromised) versions are skipped.",
|
|
1505
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#set-
|
|
1579
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#2-set-cooldowns--minimum-release-age",
|
|
1506
1580
|
id: "minimum-release-age",
|
|
1507
1581
|
severity: "warn",
|
|
1508
1582
|
title: "Set a minimum release age"
|
|
@@ -1563,10 +1637,11 @@ const createDenoBinding = (time) => ({
|
|
|
1563
1637
|
},
|
|
1564
1638
|
check(ctx, config) {
|
|
1565
1639
|
const actual = getByPath(config, ["minimumDependencyAge"]);
|
|
1640
|
+
const now = time.now();
|
|
1566
1641
|
if (denoAgeUsesFallback(actual)) {
|
|
1567
1642
|
const npmrcConfig = ctx.readConfig(npmrc$3);
|
|
1568
1643
|
const npmrcAge = getByPath(npmrcConfig, ["min-release-age"]);
|
|
1569
|
-
if (isPositiveDenoNpmrcDays(npmrcAge)) return { state: "ok" };
|
|
1644
|
+
if (isPositiveDenoNpmrcDays(npmrcAge, now, time.parse)) return { state: "ok" };
|
|
1570
1645
|
if (npmrcAge === 0) return {
|
|
1571
1646
|
state: "violation",
|
|
1572
1647
|
actual: npmrcAge,
|
|
@@ -1581,7 +1656,7 @@ const createDenoBinding = (time) => ({
|
|
|
1581
1656
|
}])
|
|
1582
1657
|
};
|
|
1583
1658
|
}
|
|
1584
|
-
if (isNonDisabledDenoReleaseAge(actual,
|
|
1659
|
+
if (isNonDisabledDenoReleaseAge(actual, now, time.parse)) return { state: "ok" };
|
|
1585
1660
|
const objectAge = isPlainRecord(actual);
|
|
1586
1661
|
const invalidObject = objectAge && (Object.keys(actual).some((key) => key !== "age" && key !== "exclude") || actual.exclude !== void 0 && !isStringList(actual.exclude));
|
|
1587
1662
|
return {
|
|
@@ -1601,12 +1676,12 @@ const createDenoBinding = (time) => ({
|
|
|
1601
1676
|
};
|
|
1602
1677
|
}
|
|
1603
1678
|
});
|
|
1604
|
-
const createMinimumReleaseAge = (time) => overrideBindings(baseRule$
|
|
1679
|
+
const createMinimumReleaseAge = (time) => overrideBindings(baseRule$2, {
|
|
1605
1680
|
npm: createNpmBinding(time),
|
|
1606
1681
|
deno: createDenoBinding(time)
|
|
1607
1682
|
});
|
|
1608
1683
|
//#endregion
|
|
1609
|
-
//#region src/
|
|
1684
|
+
//#region src/core/rules/named-registries.ts
|
|
1610
1685
|
const { pnpmWorkspace: pnpmWorkspace$4 } = CONFIG_FILES;
|
|
1611
1686
|
const isNonEmptyObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
1612
1687
|
const namedRegistries = defineRule({
|
|
@@ -1624,17 +1699,17 @@ const namedRegistries = defineRule({
|
|
|
1624
1699
|
state: "violation"
|
|
1625
1700
|
};
|
|
1626
1701
|
},
|
|
1627
|
-
docs: "https://pnpm.io/settings#namedregistries",
|
|
1702
|
+
docs: "https://pnpm.io/settings/dependency-resolution#namedregistries",
|
|
1628
1703
|
file: pnpmWorkspace$4
|
|
1629
1704
|
} },
|
|
1630
1705
|
description: "Flag named registry mappings that redirect package resolution to custom registries, which may enable dependency confusion attacks.",
|
|
1631
|
-
docs: "https://pnpm.io/settings#namedregistries",
|
|
1706
|
+
docs: "https://pnpm.io/settings/dependency-resolution#namedregistries",
|
|
1632
1707
|
id: "named-registries",
|
|
1633
1708
|
severity: "info",
|
|
1634
1709
|
title: "Review named registry mappings"
|
|
1635
1710
|
});
|
|
1636
1711
|
//#endregion
|
|
1637
|
-
//#region src/
|
|
1712
|
+
//#region src/core/rules/paranoid-mode.ts
|
|
1638
1713
|
const { aubeWorkspace: aubeWorkspace$3 } = CONFIG_FILES;
|
|
1639
1714
|
const paranoidMode = requireConfigKey({
|
|
1640
1715
|
bindings: { aube: {
|
|
@@ -1651,7 +1726,7 @@ const paranoidMode = requireConfigKey({
|
|
|
1651
1726
|
title: "Enable paranoid security mode"
|
|
1652
1727
|
});
|
|
1653
1728
|
//#endregion
|
|
1654
|
-
//#region src/
|
|
1729
|
+
//#region src/core/rules/patched-dependencies.ts
|
|
1655
1730
|
const { pnpmWorkspace: pnpmWorkspace$3 } = CONFIG_FILES;
|
|
1656
1731
|
const isNonEmptyObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
1657
1732
|
const patchedDependencies = defineRule({
|
|
@@ -1669,17 +1744,17 @@ const patchedDependencies = defineRule({
|
|
|
1669
1744
|
state: "violation"
|
|
1670
1745
|
};
|
|
1671
1746
|
},
|
|
1672
|
-
docs: "https://pnpm.io/
|
|
1747
|
+
docs: "https://pnpm.io/cli/patch#patcheddependencies",
|
|
1673
1748
|
file: pnpmWorkspace$3
|
|
1674
1749
|
} },
|
|
1675
1750
|
description: "Review local patches separately from the registry artifacts they modify.",
|
|
1676
|
-
docs: "https://pnpm.io/
|
|
1751
|
+
docs: "https://pnpm.io/cli/patch#patcheddependencies",
|
|
1677
1752
|
id: "patched-dependencies",
|
|
1678
1753
|
severity: "info",
|
|
1679
1754
|
title: "Review patched dependencies"
|
|
1680
1755
|
});
|
|
1681
1756
|
//#endregion
|
|
1682
|
-
//#region src/
|
|
1757
|
+
//#region src/core/rules/pin-exact-versions.ts
|
|
1683
1758
|
const { npmrc: npmrc$2, pnpmWorkspace: pnpmWorkspace$2, yarnrc: yarnrc$1, bunfig, denoJson } = CONFIG_FILES;
|
|
1684
1759
|
const REGISTRY_VERSION = /^(?:npm|jsr):(?:@[^/@]+\/)?[^/@]+@(?<version>[^/]*)/u;
|
|
1685
1760
|
const isUnpinnedRegistryImport = (specifier) => {
|
|
@@ -1688,9 +1763,14 @@ const isUnpinnedRegistryImport = (specifier) => {
|
|
|
1688
1763
|
const normalized = version?.startsWith("=") ? version.slice(1) : version;
|
|
1689
1764
|
return normalized === void 0 || valid(normalized) === null;
|
|
1690
1765
|
};
|
|
1691
|
-
const collectUnpinnedImports = (imports) => {
|
|
1766
|
+
const collectUnpinnedImports = (imports, location) => {
|
|
1767
|
+
if (!isPlainRecord(imports)) throw new ConfigError(`deno.json: ${location} must be an import mapping object.`);
|
|
1692
1768
|
const offenders = [];
|
|
1693
|
-
for (const [name, value] of Object.entries(imports))
|
|
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
|
+
}
|
|
1694
1774
|
return offenders;
|
|
1695
1775
|
};
|
|
1696
1776
|
const MAX_SAMPLE_COUNT = 3;
|
|
@@ -1701,23 +1781,26 @@ const formatOffenders = (offenders) => {
|
|
|
1701
1781
|
return {
|
|
1702
1782
|
remediation: {
|
|
1703
1783
|
kind: "manual",
|
|
1704
|
-
steps: ["
|
|
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."]
|
|
1705
1785
|
},
|
|
1706
1786
|
message: `${offenders.length} deno imports are not pinned: ${sample}${more}. Use \`deno add --save-exact\` or pin manually.`,
|
|
1707
1787
|
state: "violation"
|
|
1708
1788
|
};
|
|
1709
1789
|
};
|
|
1710
1790
|
const OK = { state: "ok" };
|
|
1711
|
-
const
|
|
1791
|
+
const unpinnedInlineImports = (config) => {
|
|
1712
1792
|
const imports = getByPath(config, ["imports"]);
|
|
1713
|
-
|
|
1714
|
-
|
|
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;
|
|
1715
1800
|
};
|
|
1716
1801
|
const denoBinding = {
|
|
1717
1802
|
check(_ctx, config) {
|
|
1718
|
-
const
|
|
1719
|
-
if (!imports) return OK;
|
|
1720
|
-
const offenders = collectUnpinnedImports(imports);
|
|
1803
|
+
const offenders = unpinnedInlineImports(config);
|
|
1721
1804
|
if (offenders.length === 0) return OK;
|
|
1722
1805
|
return formatOffenders(offenders);
|
|
1723
1806
|
},
|
|
@@ -1748,7 +1831,7 @@ const npmBinding = {
|
|
|
1748
1831
|
};
|
|
1749
1832
|
const aubeBinding = {
|
|
1750
1833
|
file: npmrc$2,
|
|
1751
|
-
docs: "https://aube.
|
|
1834
|
+
docs: "https://aube.sh/settings/#setting-saveprefix",
|
|
1752
1835
|
check(_ctx, config) {
|
|
1753
1836
|
const prefixes = ["save-prefix", "savePrefix"].filter((key) => Object.hasOwn(config, key)).map((key) => config[key]);
|
|
1754
1837
|
if (prefixes.length > 0 && prefixes.every((value) => value === "")) return OK;
|
|
@@ -1789,8 +1872,8 @@ const pinExactVersions = overrideBindings(requireConfigKey({
|
|
|
1789
1872
|
versionNote: { configAvailableSince: "yarn 2.0.0" }
|
|
1790
1873
|
}
|
|
1791
1874
|
},
|
|
1792
|
-
description: "Semver ranges (^, ~) auto-adopt new releases, including compromised ones. Save exact versions by default.",
|
|
1793
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
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",
|
|
1794
1877
|
id: "pin-exact-versions",
|
|
1795
1878
|
severity: "error",
|
|
1796
1879
|
title: "Pin exact dependency versions"
|
|
@@ -1800,7 +1883,7 @@ const pinExactVersions = overrideBindings(requireConfigKey({
|
|
|
1800
1883
|
npm: npmBinding
|
|
1801
1884
|
});
|
|
1802
1885
|
//#endregion
|
|
1803
|
-
//#region src/
|
|
1886
|
+
//#region src/core/rules/provenance.ts
|
|
1804
1887
|
const { npmrc: npmrc$1, yarnrc } = CONFIG_FILES;
|
|
1805
1888
|
const npmrcProvenance = {
|
|
1806
1889
|
file: npmrc$1,
|
|
@@ -1808,7 +1891,7 @@ const npmrcProvenance = {
|
|
|
1808
1891
|
message: "Set `provenance=true` (and publish from CI) to attest releases.",
|
|
1809
1892
|
value: true
|
|
1810
1893
|
};
|
|
1811
|
-
const
|
|
1894
|
+
const baseRule = requireConfigKey({
|
|
1812
1895
|
applies: isPublishable,
|
|
1813
1896
|
bindings: {
|
|
1814
1897
|
bun: {
|
|
@@ -1834,14 +1917,37 @@ const provenance = requireConfigKey({
|
|
|
1834
1917
|
}
|
|
1835
1918
|
},
|
|
1836
1919
|
description: "Provenance statements (via Sigstore) bind a published artifact to its recorded source and build.",
|
|
1837
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1920
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#11-generate-provenance-statements",
|
|
1838
1921
|
id: "provenance",
|
|
1839
1922
|
projectTypes: ["package"],
|
|
1840
1923
|
severity: "warn",
|
|
1841
1924
|
title: "Publish with provenance"
|
|
1842
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
|
+
} });
|
|
1843
1949
|
//#endregion
|
|
1844
|
-
//#region src/
|
|
1950
|
+
//#region src/core/rules/publish-access.ts
|
|
1845
1951
|
const { packageJson } = CONFIG_FILES;
|
|
1846
1952
|
const publishAccessBinding = {
|
|
1847
1953
|
check(ctx) {
|
|
@@ -1884,7 +1990,7 @@ const publishAccess = defineRule({
|
|
|
1884
1990
|
title: "Declare publish access explicitly"
|
|
1885
1991
|
});
|
|
1886
1992
|
//#endregion
|
|
1887
|
-
//#region src/
|
|
1993
|
+
//#region src/core/rules/store-server.ts
|
|
1888
1994
|
const { pnpmWorkspace: pnpmWorkspace$1 } = CONFIG_FILES;
|
|
1889
1995
|
const storeServer = defineRule({
|
|
1890
1996
|
bindings: { pnpm: {
|
|
@@ -1901,17 +2007,17 @@ const storeServer = defineRule({
|
|
|
1901
2007
|
state: "violation"
|
|
1902
2008
|
};
|
|
1903
2009
|
},
|
|
1904
|
-
docs: "https://pnpm.io/settings#
|
|
2010
|
+
docs: "https://pnpm.io/settings/store#userunningstoreserver",
|
|
1905
2011
|
file: pnpmWorkspace$1
|
|
1906
2012
|
} },
|
|
1907
2013
|
description: "Flag use of an external store server process, which introduces a trust boundary where tampered packages could be served.",
|
|
1908
|
-
docs: "https://pnpm.io/settings#
|
|
2014
|
+
docs: "https://pnpm.io/settings/store#userunningstoreserver",
|
|
1909
2015
|
id: "store-server",
|
|
1910
2016
|
severity: "info",
|
|
1911
2017
|
title: "Review store server usage"
|
|
1912
2018
|
});
|
|
1913
2019
|
//#endregion
|
|
1914
|
-
//#region src/
|
|
2020
|
+
//#region src/core/rules/strict-allow-scripts.ts
|
|
1915
2021
|
const { npmrc } = CONFIG_FILES;
|
|
1916
2022
|
const strictAllowScripts = defineRule({
|
|
1917
2023
|
bindings: { npm: {
|
|
@@ -1953,7 +2059,7 @@ const strictAllowScripts = defineRule({
|
|
|
1953
2059
|
title: "Require explicit script allow-listing"
|
|
1954
2060
|
});
|
|
1955
2061
|
//#endregion
|
|
1956
|
-
//#region src/
|
|
2062
|
+
//#region src/core/rules/strict-release-age.ts
|
|
1957
2063
|
const { aubeWorkspace: aubeWorkspace$2 } = CONFIG_FILES;
|
|
1958
2064
|
const strictReleaseAge = withAubeParanoid(requireConfigKey({
|
|
1959
2065
|
bindings: { aube: {
|
|
@@ -1970,7 +2076,7 @@ const strictReleaseAge = withAubeParanoid(requireConfigKey({
|
|
|
1970
2076
|
title: "Enforce strict release age gate"
|
|
1971
2077
|
}));
|
|
1972
2078
|
//#endregion
|
|
1973
|
-
//#region src/
|
|
2079
|
+
//#region src/core/rules/strict-store-integrity.ts
|
|
1974
2080
|
const { aubeWorkspace: aubeWorkspace$1 } = CONFIG_FILES;
|
|
1975
2081
|
const strictStoreIntegrity = defineRule({
|
|
1976
2082
|
bindings: { aube: {
|
|
@@ -2010,20 +2116,21 @@ const strictStoreIntegrity = defineRule({
|
|
|
2010
2116
|
title: "Require tarball integrity metadata"
|
|
2011
2117
|
});
|
|
2012
2118
|
//#endregion
|
|
2013
|
-
//#region src/
|
|
2119
|
+
//#region src/core/rules/trust-policy.ts
|
|
2014
2120
|
const { aubeWorkspace, pnpmWorkspace } = CONFIG_FILES;
|
|
2015
2121
|
const trustPolicy = withAubeParanoid(requireConfigKey({
|
|
2016
2122
|
bindings: {
|
|
2017
2123
|
aube: {
|
|
2018
2124
|
docs: "https://aube.jdx.dev/security.html",
|
|
2019
2125
|
documentedDefault: "no-downgrade",
|
|
2126
|
+
defaultSafety: "unconditional",
|
|
2020
2127
|
file: aubeWorkspace,
|
|
2021
2128
|
keyPath: ["trustPolicy"],
|
|
2022
2129
|
message: "Set `trustPolicy: no-downgrade` in aube-workspace.yaml to block packages whose trust level has decreased.",
|
|
2023
2130
|
value: "no-downgrade"
|
|
2024
2131
|
},
|
|
2025
2132
|
pnpm: {
|
|
2026
|
-
docs: "https://pnpm.io/settings#trustpolicy",
|
|
2133
|
+
docs: "https://pnpm.io/settings/dependency-resolution#trustpolicy",
|
|
2027
2134
|
file: pnpmWorkspace,
|
|
2028
2135
|
keyPath: ["trustPolicy"],
|
|
2029
2136
|
message: "Set `trustPolicy: no-downgrade` in pnpm-workspace.yaml to block packages whose trust level has decreased.",
|
|
@@ -2032,15 +2139,15 @@ const trustPolicy = withAubeParanoid(requireConfigKey({
|
|
|
2032
2139
|
}
|
|
2033
2140
|
},
|
|
2034
2141
|
description: "Fail installation when a package trust level has decreased compared to previous releases, catching publisher credential downgrades.",
|
|
2035
|
-
docs: "https://pnpm.io/settings#trustpolicy",
|
|
2142
|
+
docs: "https://pnpm.io/settings/dependency-resolution#trustpolicy",
|
|
2036
2143
|
id: "trust-policy",
|
|
2037
2144
|
severity: "warn",
|
|
2038
2145
|
title: "Enforce trust policy on dependency updates"
|
|
2039
2146
|
}));
|
|
2040
2147
|
//#endregion
|
|
2041
|
-
//#region src/
|
|
2042
|
-
const bindingFor = (pm) => {
|
|
2043
|
-
const settings = settingAvailability.filter((setting) => setting.pm === pm);
|
|
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));
|
|
2044
2151
|
return { check(ctx) {
|
|
2045
2152
|
const version = ctx.pmVersion;
|
|
2046
2153
|
if (version === void 0) return { state: "na" };
|
|
@@ -2063,22 +2170,23 @@ const bindingFor = (pm) => {
|
|
|
2063
2170
|
} : { state: "ok" };
|
|
2064
2171
|
} };
|
|
2065
2172
|
};
|
|
2066
|
-
const
|
|
2173
|
+
const createUnsupportedSettings = (includesFile) => defineRule({
|
|
2067
2174
|
id: "unsupported-settings",
|
|
2068
2175
|
title: "Use settings available in the target PM version",
|
|
2069
|
-
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.
|
|
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.",
|
|
2070
2177
|
severity: "error",
|
|
2071
2178
|
docs: "https://github.com/pHo9UBenaA/siro/blob/main/docs/rules.md#unsupported-settings--error",
|
|
2072
2179
|
bindings: {
|
|
2073
|
-
npm: bindingFor("npm"),
|
|
2074
|
-
pnpm: bindingFor("pnpm"),
|
|
2075
|
-
yarn: bindingFor("yarn"),
|
|
2076
|
-
bun: bindingFor("bun"),
|
|
2077
|
-
deno: bindingFor("deno")
|
|
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)
|
|
2078
2185
|
}
|
|
2079
2186
|
});
|
|
2187
|
+
const unsupportedSettings = createUnsupportedSettings(() => true);
|
|
2080
2188
|
//#endregion
|
|
2081
|
-
//#region src/
|
|
2189
|
+
//#region src/core/rules/builtin-rules.ts
|
|
2082
2190
|
const createBuiltinRules = (time) => [
|
|
2083
2191
|
advisoryCheck,
|
|
2084
2192
|
approvedGitRepos,
|
|
@@ -2109,448 +2217,57 @@ const createBuiltinRules = (time) => [
|
|
|
2109
2217
|
trustPolicy,
|
|
2110
2218
|
unsupportedSettings
|
|
2111
2219
|
];
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
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) => {
|
|
2130
2254
|
try {
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
platform: "linux",
|
|
2142
|
-
windowsPathsNoEscape: true,
|
|
2143
|
-
nonegate: true,
|
|
2144
|
-
nocomment: !options.hashComments,
|
|
2145
|
-
nocase: options.caseInsensitive,
|
|
2146
|
-
dot: options.includeDotDirectories ?? false,
|
|
2147
|
-
nobrace: options.syntax === "wildcards",
|
|
2148
|
-
noext: options.syntax === "wildcards" || options.extendedPatterns === false,
|
|
2149
|
-
optimizationLevel: options.syntax === "shell" ? 2 : 1
|
|
2150
|
-
};
|
|
2151
|
-
};
|
|
2152
|
-
/** Compile once so membership, exclusions, and traversal share glob semantics. */
|
|
2153
|
-
const compileWorkspaceGlob = (pattern, options) => {
|
|
2154
|
-
let matcher;
|
|
2155
|
-
try {
|
|
2156
|
-
const settings = engineOptions(options);
|
|
2157
|
-
if (!settings.nobrace) boundedBraceExpand(pattern);
|
|
2158
|
-
matcher = new Minimatch(options.kind === "directory" && options.syntax === "wildcards" ? pattern.replace(/[[\]]/gu, (character) => character === "[" ? "[[]" : "[]]") : pattern, {
|
|
2159
|
-
...settings,
|
|
2160
|
-
braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT
|
|
2161
|
-
});
|
|
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));
|
|
2162
2265
|
} catch (error) {
|
|
2163
|
-
|
|
2164
|
-
throw error;
|
|
2165
|
-
}
|
|
2166
|
-
return {
|
|
2167
|
-
matches: (directory) => matcher.match(directory),
|
|
2168
|
-
canDescend(directory) {
|
|
2169
|
-
const parts = directory.split("/");
|
|
2170
|
-
return matcher.set.some((alternative) => (alternative.includes(GLOBSTAR) || parts.length < alternative.length) && matcher.matchOne(parts, alternative, true));
|
|
2171
|
-
}
|
|
2172
|
-
};
|
|
2173
|
-
};
|
|
2174
|
-
const minimatchGlobs = {
|
|
2175
|
-
expand: expandWorkspaceGlob,
|
|
2176
|
-
compile: compileWorkspaceGlob
|
|
2177
|
-
};
|
|
2178
|
-
//#endregion
|
|
2179
|
-
//#region src/domain/services/parse-config-file.ts
|
|
2180
|
-
const EMPTY_FILE = Object.freeze({});
|
|
2181
|
-
/** Create a fresh parser and cache for one lint command. */
|
|
2182
|
-
const createConfigParser = (codecFor, ctx) => {
|
|
2183
|
-
const cache = /* @__PURE__ */ new Map();
|
|
2184
|
-
return (file) => {
|
|
2185
|
-
if (file === void 0) return EMPTY_FILE;
|
|
2186
|
-
const cacheKey = `${file.kind}:${file.path}`;
|
|
2187
|
-
const cached = cache.get(cacheKey);
|
|
2188
|
-
if (cached !== void 0) return cached;
|
|
2189
|
-
const text = ctx.readText(file.path);
|
|
2190
|
-
if (text === void 0) {
|
|
2191
|
-
cache.set(cacheKey, EMPTY_FILE);
|
|
2192
|
-
return EMPTY_FILE;
|
|
2193
|
-
}
|
|
2194
|
-
const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
|
|
2195
|
-
cache.set(cacheKey, parsed);
|
|
2196
|
-
return parsed;
|
|
2197
|
-
};
|
|
2198
|
-
};
|
|
2199
|
-
//#endregion
|
|
2200
|
-
//#region src/application/workspace-pattern.ts
|
|
2201
|
-
/** Detect the basic * and ? markers shared across workspace dialects. */
|
|
2202
|
-
const hasBasicWorkspaceWildcard = (pattern) => pattern.includes("*") || pattern.includes("?");
|
|
2203
|
-
/** Workspace declarations use POSIX separators regardless of the host platform. */
|
|
2204
|
-
const stripTrailingWorkspaceSlashes = (pattern) => {
|
|
2205
|
-
let end = pattern.length;
|
|
2206
|
-
while (end > 0 && pattern[end - 1] === "/") end -= 1;
|
|
2207
|
-
return pattern.slice(0, end);
|
|
2208
|
-
};
|
|
2209
|
-
//#endregion
|
|
2210
|
-
//#region src/application/workspace-prefix.ts
|
|
2211
|
-
/** Anchor Deno's native literal prefix before applying its glob suffix. */
|
|
2212
|
-
const anchorWorkspacePrefix = (pattern, glob, resolveChild) => {
|
|
2213
|
-
const parts = pattern.split("/");
|
|
2214
|
-
const firstWildcard = parts.findIndex(hasBasicWorkspaceWildcard);
|
|
2215
|
-
const prefix = parts.slice(0, firstWildcard < 0 ? parts.length : firstWildcard);
|
|
2216
|
-
if (prefix.length === 0 || pattern === ".") return glob;
|
|
2217
|
-
const declared = prefix.join("/");
|
|
2218
|
-
let resolved;
|
|
2219
|
-
let attempted = false;
|
|
2220
|
-
const resolve = () => {
|
|
2221
|
-
if (!attempted) {
|
|
2222
|
-
attempted = true;
|
|
2223
|
-
let current = ".";
|
|
2224
|
-
for (const part of prefix) {
|
|
2225
|
-
const child = resolveChild(current, part);
|
|
2226
|
-
if (child === void 0) return void 0;
|
|
2227
|
-
current = current === "." ? child : `${current}/${child}`;
|
|
2228
|
-
}
|
|
2229
|
-
resolved = current;
|
|
2230
|
-
}
|
|
2231
|
-
return resolved;
|
|
2232
|
-
};
|
|
2233
|
-
const rewrite = (directory, actual) => directory === actual ? declared : directory.startsWith(`${actual}/`) ? declared + directory.slice(actual.length) : void 0;
|
|
2234
|
-
return {
|
|
2235
|
-
matches(directory) {
|
|
2236
|
-
if (directory === ".") return false;
|
|
2237
|
-
const actual = resolve();
|
|
2238
|
-
const rewritten = actual === void 0 ? void 0 : rewrite(directory, actual);
|
|
2239
|
-
return rewritten !== void 0 && glob.matches(rewritten);
|
|
2240
|
-
},
|
|
2241
|
-
canDescend(directory) {
|
|
2242
|
-
const actual = resolve();
|
|
2243
|
-
if (actual === void 0) return false;
|
|
2244
|
-
if (directory === "." || actual.startsWith(`${directory}/`)) return true;
|
|
2245
|
-
const rewritten = rewrite(directory, actual);
|
|
2246
|
-
return rewritten !== void 0 && glob.canDescend(rewritten);
|
|
2247
|
-
}
|
|
2248
|
-
};
|
|
2249
|
-
};
|
|
2250
|
-
//#endregion
|
|
2251
|
-
//#region src/application/workspace-glob-policy.ts
|
|
2252
|
-
const workspaceGlobOptions = (caseInsensitive) => ({
|
|
2253
|
-
kind: "directory",
|
|
2254
|
-
syntax: "shell",
|
|
2255
|
-
caseInsensitive
|
|
2256
|
-
});
|
|
2257
|
-
//#endregion
|
|
2258
|
-
//#region src/application/workspace-dialects.ts
|
|
2259
|
-
const containsUnsupportedAubeGlobSyntax = (pattern) => /[[\]{}()]/u.test(pattern);
|
|
2260
|
-
const matchesCrossPathGlob = (pattern, value) => {
|
|
2261
|
-
const patternCharacters = [...pattern];
|
|
2262
|
-
const valueCharacters = [...value];
|
|
2263
|
-
let patternIndex = 0;
|
|
2264
|
-
let valueIndex = 0;
|
|
2265
|
-
let starIndex = -1;
|
|
2266
|
-
let retryIndex = 0;
|
|
2267
|
-
while (valueIndex < valueCharacters.length) {
|
|
2268
|
-
const character = patternCharacters[patternIndex];
|
|
2269
|
-
if (character === "?" || character === valueCharacters[valueIndex]) {
|
|
2270
|
-
patternIndex += 1;
|
|
2271
|
-
valueIndex += 1;
|
|
2272
|
-
} else if (character === "*") {
|
|
2273
|
-
starIndex = patternIndex;
|
|
2274
|
-
retryIndex = valueIndex;
|
|
2275
|
-
patternIndex += 1;
|
|
2276
|
-
} else if (starIndex >= 0) {
|
|
2277
|
-
patternIndex = starIndex + 1;
|
|
2278
|
-
retryIndex += 1;
|
|
2279
|
-
valueIndex = retryIndex;
|
|
2280
|
-
} else return false;
|
|
2281
|
-
}
|
|
2282
|
-
while (patternCharacters[patternIndex] === "*") patternIndex += 1;
|
|
2283
|
-
return patternIndex === patternCharacters.length;
|
|
2284
|
-
};
|
|
2285
|
-
/** The bounded glob subset verified against the recorded Deno/Aube sources. */
|
|
2286
|
-
const compileAdditionalWorkspaceGlob = (pattern, pm, excluded, caseInsensitive, globs) => {
|
|
2287
|
-
const parts = pattern.split("/");
|
|
2288
|
-
if (parts.some((part) => part.includes("**") && part !== "**")) throw new ConfigError(`${pm}: ** must occupy an entire workspace path component.`);
|
|
2289
|
-
if (pm === "aube" && (containsUnsupportedAubeGlobSyntax(pattern) || pattern.includes("**") && (parts.at(-1) !== "**" || hasBasicWorkspaceWildcard(parts.slice(0, -1).join("/"))))) throw new ConfigError("Aube workspace inspection supports literals, * and ?, and a trailing ** after a literal prefix; other glob forms are not yet supported.");
|
|
2290
|
-
const matcher = globs.compile(pattern, {
|
|
2291
|
-
kind: "directory",
|
|
2292
|
-
syntax: "wildcards",
|
|
2293
|
-
includeDotDirectories: pm === "aube",
|
|
2294
|
-
caseInsensitive: pm === "deno" && (hasBasicWorkspaceWildcard(pattern) || caseInsensitive)
|
|
2295
|
-
});
|
|
2296
|
-
if (pm === "deno" && hasBasicWorkspaceWildcard(pattern)) {
|
|
2297
|
-
const files = globs.compile(`${pattern}/package.json`, {
|
|
2298
|
-
kind: "directory",
|
|
2299
|
-
syntax: "wildcards",
|
|
2300
|
-
caseInsensitive: true
|
|
2301
|
-
});
|
|
2302
|
-
return {
|
|
2303
|
-
...matcher,
|
|
2304
|
-
matches: (directory) => files.matches(directory === "." ? "package.json" : `${directory}/package.json`)
|
|
2305
|
-
};
|
|
2306
|
-
}
|
|
2307
|
-
if (pm !== "aube" || !excluded) return matcher;
|
|
2308
|
-
const self = pattern.endsWith("/**") ? pattern.slice(0, -3) : void 0;
|
|
2309
|
-
return {
|
|
2310
|
-
...matcher,
|
|
2311
|
-
matches: (directory) => matchesCrossPathGlob(pattern, directory) || directory === self
|
|
2312
|
-
};
|
|
2313
|
-
};
|
|
2314
|
-
//#endregion
|
|
2315
|
-
//#region src/application/workspaces.ts
|
|
2316
|
-
/** npm cancels an earlier exclusion when a later positive pattern matches it. */
|
|
2317
|
-
const splitNpmPattern = (raw) => {
|
|
2318
|
-
const prefix = /^!+/u.exec(raw)?.[0] ?? "";
|
|
2319
|
-
return {
|
|
2320
|
-
excluded: prefix.length % 2 === 1,
|
|
2321
|
-
pattern: raw.slice(prefix.length)
|
|
2322
|
-
};
|
|
2323
|
-
};
|
|
2324
|
-
const npmPatterns = (patterns, globs) => {
|
|
2325
|
-
const positive = [];
|
|
2326
|
-
let negative = [];
|
|
2327
|
-
for (const raw of patterns) {
|
|
2328
|
-
const parsed = splitNpmPattern(raw);
|
|
2329
|
-
const pattern = parsed.pattern.replace(/^\.?\/+/u, "");
|
|
2330
|
-
if (parsed.excluded) negative.push({
|
|
2331
|
-
pattern,
|
|
2332
|
-
glob: globs.compile(pattern, { kind: "declaration" })
|
|
2333
|
-
});
|
|
2334
|
-
else {
|
|
2335
|
-
for (let index = 0; index < negative.length; index += 1) if (negative[index]?.glob.matches(pattern)) negative.splice(index, 1);
|
|
2336
|
-
positive.push(pattern);
|
|
2337
|
-
}
|
|
2338
|
-
}
|
|
2339
|
-
return [...positive, ...negative.map(({ pattern }) => `!${pattern}`)];
|
|
2340
|
-
};
|
|
2341
|
-
/** Read declaration sources separately so Deno's two manifest sets stay distinct. */
|
|
2342
|
-
const workspaceDefinitions = (ctx, pm, dependencies) => {
|
|
2343
|
-
const { codecFor, paths, globs } = dependencies;
|
|
2344
|
-
const parse = createConfigParser(codecFor, ctx);
|
|
2345
|
-
const validate = (value, source, denoManifests = false) => {
|
|
2346
|
-
if (value === void 0) return {
|
|
2347
|
-
patterns: [],
|
|
2348
|
-
denoManifests
|
|
2349
|
-
};
|
|
2350
|
-
if (!Array.isArray(value) || !Array.from(value).every((item) => typeof item === "string")) throw new ConfigError(`${source}: expected an array of directory patterns.`);
|
|
2351
|
-
for (const pattern of value) {
|
|
2352
|
-
const positive = pm === "npm" ? splitNpmPattern(pattern).pattern : pattern.startsWith("!") ? pattern.slice(1) : pattern;
|
|
2353
|
-
if ((pm === "deno" || pm === "aube" ? [positive] : globs.expand(positive)).some((alternative) => !isRelPath(alternative) || alternative.includes("\\") || alternative.startsWith("!"))) throw new ConfigError(`${source}: use relative directory patterns without parent traversal: ${JSON.stringify(pattern)}.`);
|
|
2354
|
-
if (pm === "deno" && denoManifests && !pattern.startsWith("!") && paths.normalizePattern(positive) === ".") throw new ConfigError(`${source}: a Deno workspace cannot contain itself.`);
|
|
2355
|
-
}
|
|
2356
|
-
return {
|
|
2357
|
-
patterns: pm === "npm" ? npmPatterns(value, globs) : value,
|
|
2358
|
-
denoManifests
|
|
2359
|
-
};
|
|
2360
|
-
};
|
|
2361
|
-
const packageDefinition = () => {
|
|
2362
|
-
let value = ctx.packageJson?.workspaces;
|
|
2363
|
-
if (isPlainRecord(value)) {
|
|
2364
|
-
value = value.packages;
|
|
2365
|
-
if (value === void 0) throw new ConfigError("package.json#workspaces: expected a packages array.");
|
|
2366
|
-
}
|
|
2367
|
-
return validate(value, "package.json#workspaces");
|
|
2368
|
-
};
|
|
2369
|
-
if (pm === "pnpm") return [validate(ctx.exists(CONFIG_FILES.pnpmWorkspace.path) ? parse(CONFIG_FILES.pnpmWorkspace).packages : void 0, "pnpm-workspace.yaml#packages")];
|
|
2370
|
-
if (pm === "aube") {
|
|
2371
|
-
const file = [CONFIG_FILES.aubeWorkspace, CONFIG_FILES.pnpmWorkspace].find((candidate) => ctx.exists(candidate.path));
|
|
2372
|
-
return [file ? validate(parse(file).packages, `${file.path}#packages`) : packageDefinition()];
|
|
2373
|
-
}
|
|
2374
|
-
if (pm === "deno") return [validate(parse(CONFIG_FILES.denoJson).workspace, "deno.json#workspace", true), packageDefinition()];
|
|
2375
|
-
return [packageDefinition()];
|
|
2376
|
-
};
|
|
2377
|
-
/** Traverse only ordinary directories that can fall under a positive pattern. */
|
|
2378
|
-
const workspaceDirectories = (ctx, fs, definition, pm, dependencies) => {
|
|
2379
|
-
const { codecFor, paths, globs, caseInsensitiveGlobs } = dependencies;
|
|
2380
|
-
const defaultWorkspaceGlobOptions = workspaceGlobOptions(caseInsensitiveGlobs);
|
|
2381
|
-
const { patterns } = definition;
|
|
2382
|
-
const skipVendor = pm === "deno" && createConfigParser(codecFor, ctx)(CONFIG_FILES.denoJson).vendor === true;
|
|
2383
|
-
const positive = patterns.filter((pattern) => !pattern.startsWith("!")).map((pattern) => stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern)));
|
|
2384
|
-
const negative = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern.slice(1))));
|
|
2385
|
-
const directoryCache = /* @__PURE__ */ new Map();
|
|
2386
|
-
const readDirectories = (directory) => {
|
|
2387
|
-
const cached = directoryCache.get(directory);
|
|
2388
|
-
if (cached) return cached;
|
|
2389
|
-
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
2390
|
-
const names = fs.readDirectories(paths.resolve(ctx.root, asRelPath(directory)));
|
|
2391
|
-
if (!Array.isArray(names)) throw new ConfigError("FileSystem.readDirectories must return an array of directory names.");
|
|
2392
|
-
for (const name of Array.from(names)) if (!isRelPath(name) || name === "." || name === ".." || /[\\/]/u.test(name)) throw new ConfigError("FileSystem.readDirectories returned an invalid child directory name.");
|
|
2393
|
-
const result = [...new Set(names)].sort();
|
|
2394
|
-
directoryCache.set(directory, result);
|
|
2395
|
-
return result;
|
|
2396
|
-
};
|
|
2397
|
-
const resolveChild = (parent, name) => {
|
|
2398
|
-
if (name === ".git" || name === "node_modules") return void 0;
|
|
2399
|
-
const names = readDirectories(parent);
|
|
2400
|
-
const resolved = names.includes(name) ? name : fs.resolveDirectory?.(paths.resolve(ctx.root, asRelPath(parent)), name);
|
|
2401
|
-
if (resolved !== void 0 && !names.includes(resolved)) throw new ConfigError("FileSystem.resolveDirectory must return an enumerated ordinary child directory name.");
|
|
2402
|
-
return resolved === ".git" || resolved === "node_modules" ? void 0 : resolved;
|
|
2403
|
-
};
|
|
2404
|
-
const compile = (pattern, excluded = false) => {
|
|
2405
|
-
if (pm !== "deno" && pm !== "aube") return globs.compile(pattern, pm === "npm" ? {
|
|
2406
|
-
...defaultWorkspaceGlobOptions,
|
|
2407
|
-
hashComments: true
|
|
2408
|
-
} : defaultWorkspaceGlobOptions);
|
|
2409
|
-
const glob = compileAdditionalWorkspaceGlob(pattern, pm, excluded, caseInsensitiveGlobs, globs);
|
|
2410
|
-
return pm === "deno" && !excluded ? anchorWorkspacePrefix(pattern, glob, resolveChild) : glob;
|
|
2411
|
-
};
|
|
2412
|
-
const included = positive.map((pattern) => compile(pattern));
|
|
2413
|
-
const excluded = negative.map((pattern) => {
|
|
2414
|
-
return {
|
|
2415
|
-
glob: compile(pattern, true),
|
|
2416
|
-
subtree: pm === "deno" || pm === "aube" || pm === "bun" ? void 0 : pattern === "**" ? { matches: () => true } : pattern.endsWith("/**") ? compile(pattern.slice(0, -3), true) : void 0
|
|
2417
|
-
};
|
|
2418
|
-
});
|
|
2419
|
-
const declaredBase = (pattern) => {
|
|
2420
|
-
const parts = pattern.split("/");
|
|
2421
|
-
const wildcard = parts.findIndex(hasBasicWorkspaceWildcard);
|
|
2422
|
-
return parts.slice(0, wildcard < 0 ? parts.length : wildcard).join("/") || ".";
|
|
2423
|
-
};
|
|
2424
|
-
const relatedBases = (left, right) => left === "." || right === "." || left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
2425
|
-
const denoPositive = pm === "deno" ? positive.filter(hasBasicWorkspaceWildcard).map((pattern) => ({
|
|
2426
|
-
base: declaredBase(pattern),
|
|
2427
|
-
scope: anchorWorkspacePrefix(declaredBase(pattern), {
|
|
2428
|
-
matches: () => true,
|
|
2429
|
-
canDescend: () => true
|
|
2430
|
-
}, resolveChild)
|
|
2431
|
-
})) : [];
|
|
2432
|
-
const denoOrdered = pm === "deno" ? patterns.filter((pattern) => hasBasicWorkspaceWildcard(pattern) || pattern.startsWith("!")).map((raw) => {
|
|
2433
|
-
const isExcluded = raw.startsWith("!");
|
|
2434
|
-
const pattern = stripTrailingWorkspaceSlashes(paths.normalizePattern(isExcluded ? raw.slice(1) : raw));
|
|
2435
|
-
const glob = compile(pattern, isExcluded);
|
|
2436
|
-
if (!isExcluded) return {
|
|
2437
|
-
excluded: false,
|
|
2438
|
-
glob
|
|
2439
|
-
};
|
|
2440
|
-
return {
|
|
2441
|
-
excluded: true,
|
|
2442
|
-
glob: {
|
|
2443
|
-
...glob,
|
|
2444
|
-
matches(directory) {
|
|
2445
|
-
if (!hasBasicWorkspaceWildcard(pattern)) return directory === pattern;
|
|
2446
|
-
if (!glob.matches(directory)) return false;
|
|
2447
|
-
const applicable = denoPositive.filter((item) => item.scope.matches(directory)).map((item) => relatedBases(item.base, declaredBase(pattern)));
|
|
2448
|
-
if (applicable.some(Boolean) && applicable.some((value) => !value)) throw new ConfigError("Deno workspace exclusion has ambiguous overlapping bases; use consistent prefix spelling or separate non-overlapping patterns.");
|
|
2449
|
-
return applicable.some(Boolean);
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
};
|
|
2453
|
-
}) : [];
|
|
2454
|
-
const bunPatterns = pm === "bun" ? patterns.map((raw) => {
|
|
2455
|
-
const isExcluded = raw.startsWith("!");
|
|
2456
|
-
const pattern = stripTrailingWorkspaceSlashes(paths.normalizePattern(isExcluded ? raw.slice(1) : raw));
|
|
2457
|
-
const usesGlob = isExcluded || hasBasicWorkspaceWildcard(pattern) || pattern.includes("{") || pattern.includes("[");
|
|
2458
|
-
const glob = globs.compile(pattern, {
|
|
2459
|
-
...defaultWorkspaceGlobOptions,
|
|
2460
|
-
extendedPatterns: false
|
|
2461
|
-
});
|
|
2462
|
-
const subtree = isExcluded && pattern.endsWith("/**") ? globs.compile(pattern.slice(0, -3), {
|
|
2463
|
-
...defaultWorkspaceGlobOptions,
|
|
2464
|
-
extendedPatterns: false
|
|
2465
|
-
}) : void 0;
|
|
2466
|
-
return {
|
|
2467
|
-
isExcluded,
|
|
2468
|
-
usesGlob,
|
|
2469
|
-
glob,
|
|
2470
|
-
matches: (directory) => glob.matches(directory) || glob.matches(`${directory}/`) || subtree?.matches(directory) === true
|
|
2471
|
-
};
|
|
2472
|
-
}) : [];
|
|
2473
|
-
const bunOrdered = bunPatterns.filter(({ usesGlob }) => usesGlob);
|
|
2474
|
-
const bunLiterals = bunPatterns.filter(({ isExcluded, usesGlob }) => !isExcluded && !usesGlob);
|
|
2475
|
-
const bunTraversal = bunPatterns.filter(({ isExcluded }) => !isExcluded).map(({ glob }) => glob);
|
|
2476
|
-
const denoLiteral = pm === "deno" ? positive.filter((pattern) => !hasBasicWorkspaceWildcard(pattern)).map((pattern) => compile(pattern)) : [];
|
|
2477
|
-
if (definition.denoManifests && denoOrdered.findLast(({ glob }) => glob.matches("."))?.excluded === false) throw new ConfigError("deno.json#workspace: a Deno workspace cannot contain itself.");
|
|
2478
|
-
if (positive.length === 0 || positive.every((pattern) => pattern === ".")) return [];
|
|
2479
|
-
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
2480
|
-
const result = [];
|
|
2481
|
-
const pending = ["."];
|
|
2482
|
-
while (pending.length > 0) {
|
|
2483
|
-
const current = pending.pop();
|
|
2484
|
-
if (current === void 0) break;
|
|
2485
|
-
for (const name of readDirectories(current)) {
|
|
2486
|
-
if (name === ".git" || name === "node_modules") continue;
|
|
2487
|
-
const directory = asRelPath(current === "." ? name : `${current}/${name}`);
|
|
2488
|
-
if (pm === "bun" && name === "CMakeFiles" && !bunLiterals.some(({ glob }) => glob.matches(directory) || glob.canDescend(directory))) continue;
|
|
2489
|
-
const inVendor = skipVendor && (directory === "vendor" || directory.startsWith("vendor/"));
|
|
2490
|
-
if (inVendor && !denoLiteral.some((pattern) => pattern.matches(directory) || pattern.canDescend(directory))) continue;
|
|
2491
|
-
const isExcluded = pm !== "deno" && excluded.some(({ glob }) => glob.matches(directory) || glob.matches(`${directory}/`));
|
|
2492
|
-
if (excluded.some(({ subtree }) => subtree?.matches(directory))) continue;
|
|
2493
|
-
const lastDenoMatch = denoOrdered.findLast(({ glob }) => glob.matches(directory));
|
|
2494
|
-
const lastBunMatch = bunOrdered.findLast(({ matches }) => matches(directory));
|
|
2495
|
-
if (pm === "deno" ? denoLiteral.some((pattern) => pattern.matches(directory)) || !inVendor && lastDenoMatch?.excluded === false : pm === "bun" ? bunLiterals.some(({ glob }) => glob.matches(directory)) || lastBunMatch?.isExcluded === false : !isExcluded && included.some((pattern) => pattern.matches(directory))) result.push(directory);
|
|
2496
|
-
if ((pm === "bun" ? bunTraversal : inVendor ? denoLiteral : included).some((pattern) => pattern.canDescend(directory))) pending.push(directory);
|
|
2497
|
-
}
|
|
2266
|
+
throw new ConfigError(`exclude: ${error instanceof Error ? error.message : String(error)}`);
|
|
2498
2267
|
}
|
|
2499
|
-
return result.sort();
|
|
2500
|
-
};
|
|
2501
|
-
//#endregion
|
|
2502
|
-
//#region src/application/workspace-contexts.ts
|
|
2503
|
-
/** Expand each manager's declarations into isolated publication contexts. */
|
|
2504
|
-
const collectWorkspaceMembers = (ctx, fs, pms, projectType, dependencies) => {
|
|
2505
|
-
const { codecFor, createRepoContext, paths } = dependencies;
|
|
2506
|
-
return pms.flatMap((pm) => {
|
|
2507
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2508
|
-
return workspaceDefinitions(ctx, pm, dependencies).flatMap((definition) => workspaceDirectories(ctx, fs, definition, pm, dependencies).flatMap((directory) => {
|
|
2509
|
-
const root = paths.resolve(ctx.root, directory);
|
|
2510
|
-
const manifest = paths.resolve(root, asRelPath("package.json"));
|
|
2511
|
-
const denoManifest = paths.resolve(root, asRelPath("deno.json"));
|
|
2512
|
-
const denoJsonc = paths.resolve(root, asRelPath("deno.jsonc"));
|
|
2513
|
-
const allowed = new Set(pm === "deno" ? [
|
|
2514
|
-
manifest,
|
|
2515
|
-
denoManifest,
|
|
2516
|
-
denoJsonc
|
|
2517
|
-
] : [manifest]);
|
|
2518
|
-
const memberFs = {
|
|
2519
|
-
exists: (file) => allowed.has(file) && fs.exists(file),
|
|
2520
|
-
readText: (file) => allowed.has(file) ? fs.readText(file) : void 0
|
|
2521
|
-
};
|
|
2522
|
-
try {
|
|
2523
|
-
const explicitMember = definition.patterns.some((pattern) => !pattern.startsWith("!") && !hasBasicWorkspaceWildcard(pattern) && stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern)) === directory);
|
|
2524
|
-
if (!definition.denoManifests && !fs.exists(manifest)) {
|
|
2525
|
-
if (pm === "deno" && explicitMember) throw new ConfigError("Declared npm workspace member has no package.json.");
|
|
2526
|
-
return [];
|
|
2527
|
-
}
|
|
2528
|
-
if (pm === "deno" && !fs.exists(denoManifest) && fs.exists(denoJsonc)) throw new ConfigError("deno.jsonc is not supported; use strict deno.json.");
|
|
2529
|
-
const memberCtx = createRepoContext(root, memberFs, projectType);
|
|
2530
|
-
if (!definition.denoManifests && !memberCtx.packageJson) return [];
|
|
2531
|
-
if (!memberCtx.packageJson && !memberCtx.exists(asRelPath("deno.json"))) {
|
|
2532
|
-
if (definition.denoManifests && explicitMember) throw new ConfigError("Declared Deno member has no deno.json or package.json.");
|
|
2533
|
-
return [];
|
|
2534
|
-
}
|
|
2535
|
-
if (pm === "deno") {
|
|
2536
|
-
if (createConfigParser(codecFor, memberCtx)(CONFIG_FILES.denoJson).workspace !== void 0 || memberCtx.packageJson?.workspaces !== void 0) throw new ConfigError("Nested workspace declarations are not supported by Deno.");
|
|
2537
|
-
}
|
|
2538
|
-
if (seen.has(directory)) return [];
|
|
2539
|
-
seen.add(directory);
|
|
2540
|
-
return [{
|
|
2541
|
-
directory,
|
|
2542
|
-
pm,
|
|
2543
|
-
ctx: memberCtx
|
|
2544
|
-
}];
|
|
2545
|
-
} catch (error) {
|
|
2546
|
-
if (error instanceof ConfigError) throw new ConfigError(`${directory}/${error.message}`);
|
|
2547
|
-
throw error;
|
|
2548
|
-
}
|
|
2549
|
-
}));
|
|
2550
|
-
});
|
|
2551
2268
|
};
|
|
2552
2269
|
//#endregion
|
|
2553
|
-
//#region src/
|
|
2270
|
+
//#region src/core/apply-config.ts
|
|
2554
2271
|
/** Select active rules and keep user severity choices as explicit runtime data. */
|
|
2555
2272
|
const applyConfig = (base, config) => {
|
|
2556
2273
|
const merged = [...base, ...config?.customRules ?? []];
|
|
@@ -2578,7 +2295,7 @@ const applyConfig = (base, config) => {
|
|
|
2578
2295
|
};
|
|
2579
2296
|
};
|
|
2580
2297
|
//#endregion
|
|
2581
|
-
//#region src/
|
|
2298
|
+
//#region src/core/detect-pms.ts
|
|
2582
2299
|
const registerSignals = (signals, pm, owner) => {
|
|
2583
2300
|
for (const file of signals) {
|
|
2584
2301
|
const prior = owner.get(file);
|
|
@@ -2610,24 +2327,25 @@ const detectPMs = (ctx) => {
|
|
|
2610
2327
|
return PMS.filter((pm) => found.has(pm));
|
|
2611
2328
|
};
|
|
2612
2329
|
//#endregion
|
|
2613
|
-
//#region src/
|
|
2330
|
+
//#region src/core/resolve-pms.ts
|
|
2614
2331
|
/** Apply the explicit selection or detection, then the configured allow-list. */
|
|
2615
2332
|
const resolvePMs = (ctx, opts) => {
|
|
2616
2333
|
const { pmOverride, allowed } = opts;
|
|
2617
2334
|
const detected = pmOverride ? [pmOverride] : detectPMs(ctx);
|
|
2618
2335
|
const pms = allowed ? detected.filter((pm) => allowed.includes(pm)) : detected;
|
|
2619
2336
|
if (pms.length > 0) return pms;
|
|
2337
|
+
if (opts.optional && detected.length === 0) return [];
|
|
2620
2338
|
if (!allowed) throw new UsageError(`No package manager detected. Pass --pm <${PMS.join("|")}> to be explicit.`);
|
|
2621
2339
|
if (pmOverride) throw new UsageError(`--pm ${pmOverride} is not in siro.config.ts pms (${allowed.join(", ")}). Adjust the config or the flag.`);
|
|
2622
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.`);
|
|
2623
2341
|
throw new UsageError(`No package manager detected, and siro.config.ts restricts pms to ${allowed.join(", ")}. Adjust the config or remove the restriction.`);
|
|
2624
2342
|
};
|
|
2625
2343
|
//#endregion
|
|
2626
|
-
//#region src/
|
|
2344
|
+
//#region src/core/decide-severity.ts
|
|
2627
2345
|
/** Resolve the most specific severity signal for one finding. */
|
|
2628
2346
|
const decideSeverity = (status, binding, rule, userOverride) => userOverride ?? status.severity ?? binding.severity ?? rule.severity;
|
|
2629
2347
|
//#endregion
|
|
2630
|
-
//#region src/
|
|
2348
|
+
//#region src/core/render-version-note.ts
|
|
2631
2349
|
const buildVersionNoteParts = (versionNote) => {
|
|
2632
2350
|
const parts = [];
|
|
2633
2351
|
if (typeof versionNote.configAvailableSince !== "undefined") parts.push(`available since ${versionNote.configAvailableSince}`);
|
|
@@ -2642,130 +2360,342 @@ const renderVersionNoteMessage = (message, versionNote) => {
|
|
|
2642
2360
|
return `${message} (${parts.join("; ")})`;
|
|
2643
2361
|
};
|
|
2644
2362
|
//#endregion
|
|
2645
|
-
//#region src/
|
|
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
|
|
2646
2388
|
const resolveBindingProjectType = (ctx, pm, parseConfig) => {
|
|
2647
2389
|
if (ctx.projectType !== void 0) return ctx.projectType;
|
|
2648
|
-
|
|
2649
|
-
return resolvePackageJsonProjectType(ctx);
|
|
2390
|
+
return pm === "deno" ? resolveDenoProjectType(ctx, parseConfig(CONFIG_FILES.denoJson)) : resolvePackageJsonProjectType(ctx);
|
|
2650
2391
|
};
|
|
2651
|
-
/**
|
|
2392
|
+
/** Select applicable PM bindings; response handling is shared with manifest checks. */
|
|
2652
2393
|
const runLint = (opts) => {
|
|
2653
|
-
const {
|
|
2394
|
+
const { repository, pms, ruleSet, severityOverrides } = opts;
|
|
2654
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
|
+
}
|
|
2655
2404
|
const summary = {
|
|
2656
2405
|
error: 0,
|
|
2657
2406
|
info: 0,
|
|
2658
2407
|
warn: 0
|
|
2659
2408
|
};
|
|
2660
|
-
const
|
|
2661
|
-
for (const rule of ruleSet) for (const pm of pms) {
|
|
2662
|
-
const binding = rule.bindings[pm];
|
|
2663
|
-
if (!binding || rule.projectTypes && !rule.projectTypes.includes(resolveBindingProjectType(ctx, pm, parseConfig))) continue;
|
|
2664
|
-
const ruleContext = {
|
|
2665
|
-
...ctx,
|
|
2666
|
-
readConfig: parseConfig,
|
|
2667
|
-
pmVersion: opts.pmVersions?.[pm]
|
|
2668
|
-
};
|
|
2669
|
-
const response = binding.check(ruleContext, parseConfig(binding.file));
|
|
2670
|
-
if (!isCheckStatusShape(response)) throw new ConfigError(`Rule '${rule.id}' returned an invalid check result.`);
|
|
2671
|
-
const statuses = response.state === "violations" ? response.violations : [response];
|
|
2672
|
-
for (const status of statuses) {
|
|
2673
|
-
if (status.state !== "violation") continue;
|
|
2674
|
-
const finding = {
|
|
2675
|
-
ruleId: rule.id,
|
|
2676
|
-
pm,
|
|
2677
|
-
severity: decideSeverity(status, binding, rule, severityOverrides?.get(rule.id)),
|
|
2678
|
-
message: renderVersionNoteMessage(status.message, binding.versionNote),
|
|
2679
|
-
file: status.file ?? binding.file?.path,
|
|
2680
|
-
docs: binding.docs ?? rule.docs,
|
|
2681
|
-
actual: status.actual,
|
|
2682
|
-
expected: status.expected,
|
|
2683
|
-
remediation: guardRemediationAvailability(pm, ruleContext.pmVersion, status.remediation)
|
|
2684
|
-
};
|
|
2685
|
-
findings.push(finding);
|
|
2686
|
-
summary[finding.severity] += 1;
|
|
2687
|
-
}
|
|
2688
|
-
}
|
|
2409
|
+
for (const finding of findings) summary[finding.severity] += 1;
|
|
2689
2410
|
return {
|
|
2690
2411
|
findings,
|
|
2691
2412
|
summary
|
|
2692
2413
|
};
|
|
2693
2414
|
};
|
|
2694
2415
|
//#endregion
|
|
2695
|
-
//#region src/
|
|
2696
|
-
/**
|
|
2697
|
-
const
|
|
2698
|
-
const
|
|
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) => {
|
|
2699
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.");
|
|
2700
2571
|
if ("customRules" in options || "reporters" in options) throw new UsageError("Pass customRules and reporters inside the config option.");
|
|
2701
2572
|
if (options.pm !== void 0 && !isPM(options.pm)) throw new UsageError(`Unknown package manager: ${String(options.pm)}`);
|
|
2702
2573
|
if (options.projectType !== void 0 && !isProjectType(options.projectType)) throw new UsageError(`Unknown project type: ${String(options.projectType)}`);
|
|
2703
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.");
|
|
2704
|
-
|
|
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);
|
|
2705
2579
|
const config = options.config === void 0 ? void 0 : parseConfig(options.config);
|
|
2706
2580
|
const fs = options.fs === void 0 ? dependencies.fileSystem : options.fs;
|
|
2707
|
-
const
|
|
2708
|
-
const
|
|
2709
|
-
allowed: config?.pms,
|
|
2710
|
-
pmOverride: options.pm
|
|
2711
|
-
});
|
|
2712
|
-
if (pms.includes("deno") && !ctx.exists(asRelPath("deno.json")) && ctx.exists(asRelPath("deno.jsonc"))) throw new ConfigError("deno.jsonc is not supported. siro currently reads strict JSON from deno.json only.");
|
|
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 ?? ["."]);
|
|
2713
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).`);
|
|
2714
2587
|
return {
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
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
|
|
2721
2631
|
},
|
|
2722
|
-
members: options.workspaces ? collectWorkspaceMembers(ctx, fs, pms, options.projectType ?? config?.projectType, dependencies) : [],
|
|
2723
|
-
ruleSet: configured.rules,
|
|
2724
|
-
severityOverrides: configured.severityOverrides,
|
|
2725
|
-
codecFor,
|
|
2726
2632
|
reporters: config?.reporters ?? []
|
|
2727
2633
|
};
|
|
2728
2634
|
};
|
|
2729
|
-
|
|
2730
|
-
const
|
|
2731
|
-
const
|
|
2732
|
-
const
|
|
2733
|
-
const
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
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
|
|
2745
2656
|
});
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
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 [];
|
|
2754
2665
|
});
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
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)));
|
|
2760
2680
|
}
|
|
2681
|
+
const summary = {
|
|
2682
|
+
error: 0,
|
|
2683
|
+
warn: 0,
|
|
2684
|
+
info: 0
|
|
2685
|
+
};
|
|
2686
|
+
for (const finding of findings) summary[finding.severity] += 1;
|
|
2761
2687
|
return {
|
|
2762
2688
|
findings,
|
|
2763
|
-
summary
|
|
2689
|
+
summary,
|
|
2690
|
+
inspection: {
|
|
2691
|
+
manifests,
|
|
2692
|
+
installationRoots
|
|
2693
|
+
}
|
|
2764
2694
|
};
|
|
2765
2695
|
};
|
|
2766
|
-
const lint$1 = (options, dependencies) => runPreparedLint(prepareLint(options, dependencies));
|
|
2696
|
+
const lint$1 = (options, dependencies) => runPreparedLint(prepareLint(options, dependencies).evaluation);
|
|
2767
2697
|
//#endregion
|
|
2768
|
-
//#region src/
|
|
2698
|
+
//#region src/core/filter.ts
|
|
2769
2699
|
const EXIT_SUCCESS = 0;
|
|
2770
2700
|
const EXIT_FAILURE = 1;
|
|
2771
2701
|
const meetsThreshold = (severity, threshold) => SEVERITY_RANK[severity] >= SEVERITY_RANK[threshold];
|
|
@@ -2782,6 +2712,7 @@ const filterBySeverity = (result, threshold) => {
|
|
|
2782
2712
|
summary[finding.severity] += 1;
|
|
2783
2713
|
}
|
|
2784
2714
|
return {
|
|
2715
|
+
...result,
|
|
2785
2716
|
findings,
|
|
2786
2717
|
summary
|
|
2787
2718
|
};
|
|
@@ -2795,7 +2726,7 @@ const exitCodeForLint = (result, threshold = "error") => {
|
|
|
2795
2726
|
return EXIT_SUCCESS;
|
|
2796
2727
|
};
|
|
2797
2728
|
//#endregion
|
|
2798
|
-
//#region src/
|
|
2729
|
+
//#region src/core/lint-command.ts
|
|
2799
2730
|
/** Evaluate and report. Executable config loading belongs to the CLI adapter. */
|
|
2800
2731
|
const lintCommand$1 = async (options, io, dependencies, reporters) => {
|
|
2801
2732
|
if (!options) throw new UsageError("Lint options are required.");
|
|
@@ -2805,23 +2736,53 @@ const lintCommand$1 = async (options, io, dependencies, reporters) => {
|
|
|
2805
2736
|
const selection = options.reporter ?? reporters.defaultName;
|
|
2806
2737
|
const reporter = typeof selection === "string" ? registry.get(selection) : selection;
|
|
2807
2738
|
if (!isReporterShape(reporter)) throw new UsageError(`${typeof selection === "string" ? "Unknown" : "Invalid"} reporter: ${String(selection)} (available: ${[...registry.keys()].join(", ")})`);
|
|
2808
|
-
const result = runPreparedLint(prepared);
|
|
2739
|
+
const result = runPreparedLint(prepared.evaluation);
|
|
2809
2740
|
const exitCode = exitCodeForLint(result, options.severity ?? "error");
|
|
2810
|
-
|
|
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;
|
|
2811
2769
|
return exitCode;
|
|
2812
2770
|
};
|
|
2813
2771
|
//#endregion
|
|
2814
|
-
//#region src/
|
|
2772
|
+
//#region src/core/contracts/package-json.ts
|
|
2815
2773
|
const PackageJsonSchema = vb.looseObject({
|
|
2816
2774
|
files: vb.optional(vb.array(vb.string())),
|
|
2817
2775
|
name: vb.optional(vb.string()),
|
|
2818
2776
|
packageManager: vb.optional(vb.string()),
|
|
2819
2777
|
private: vb.optional(vb.boolean()),
|
|
2820
|
-
publishConfig: vb.optional(vb.looseObject({
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
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
|
+
})),
|
|
2825
2786
|
trustedDependencies: vb.optional(vb.array(vb.string()))
|
|
2826
2787
|
});
|
|
2827
2788
|
const parsePackageJson = (value) => {
|
|
@@ -2848,9 +2809,15 @@ const readPackageJson = (raw) => {
|
|
|
2848
2809
|
};
|
|
2849
2810
|
const createRepoContext = (root, fs = nodeFileSystem, projectType) => {
|
|
2850
2811
|
if (fs === nodeFileSystem) assertDirectory(root);
|
|
2851
|
-
const
|
|
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
|
+
};
|
|
2852
2820
|
const exists = (relPath) => fs.exists(resolveIn(root, relPath));
|
|
2853
|
-
const raw = readText(asRelPath("package.json"));
|
|
2854
2821
|
let packageJson = void 0;
|
|
2855
2822
|
if (typeof raw !== "undefined") packageJson = readPackageJson(raw);
|
|
2856
2823
|
return {
|
|
@@ -2917,33 +2884,40 @@ const escapeData = (raw) => raw.replaceAll("%", "%25").replaceAll("\r", "%0D").r
|
|
|
2917
2884
|
const escapeProp = (raw) => escapeData(raw).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
2918
2885
|
/** Emit GitHub Actions workflow commands (annotations on PRs). */
|
|
2919
2886
|
const githubReporter = {
|
|
2920
|
-
format(result, io) {
|
|
2887
|
+
async format(result, io, context) {
|
|
2921
2888
|
for (const finding of result.findings) {
|
|
2922
2889
|
let file = "";
|
|
2923
|
-
if (finding.file) file = `file=${escapeProp(finding.file)},`;
|
|
2924
|
-
let body = `[${finding.pm}] ${finding.message}`;
|
|
2890
|
+
if (finding.file) file = `file=${escapeProp(path.resolve(context.cwd, finding.file))},`;
|
|
2891
|
+
let body = `[${finding.pm ?? "package"}] ${finding.directory}: ${finding.message}`;
|
|
2925
2892
|
if (finding.docs) body += ` (${finding.docs})`;
|
|
2926
|
-
io.stdout(`::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`);
|
|
2893
|
+
await io.stdout(`::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`);
|
|
2927
2894
|
}
|
|
2928
2895
|
},
|
|
2929
2896
|
name: "github"
|
|
2930
2897
|
};
|
|
2931
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
|
|
2932
2905
|
//#region src/adapters/reporters/json.ts
|
|
2933
2906
|
/**
|
|
2934
2907
|
* Versioned machine-readable output — the public contract consumed by
|
|
2935
2908
|
* external fixers (see docs/json-output.md). Bump `schemaVersion` on any
|
|
2936
2909
|
* breaking shape change and update that doc in the same commit.
|
|
2937
2910
|
*/
|
|
2938
|
-
const SCHEMA_VERSION =
|
|
2911
|
+
const SCHEMA_VERSION = 3;
|
|
2939
2912
|
const jsonReporter = {
|
|
2940
|
-
format(result, io) {
|
|
2941
|
-
io.stdout(JSON.stringify({
|
|
2913
|
+
async format(result, io) {
|
|
2914
|
+
await io.stdout(safeJsonText(JSON.stringify({
|
|
2942
2915
|
schemaVersion: SCHEMA_VERSION,
|
|
2943
2916
|
siroVersion: version,
|
|
2944
2917
|
findings: result.findings,
|
|
2945
|
-
summary: result.summary
|
|
2946
|
-
|
|
2918
|
+
summary: result.summary,
|
|
2919
|
+
inspection: result.inspection
|
|
2920
|
+
}, void 0, 2)));
|
|
2947
2921
|
},
|
|
2948
2922
|
name: "json"
|
|
2949
2923
|
};
|
|
@@ -2970,12 +2944,11 @@ const colorSupportedNow = () => {
|
|
|
2970
2944
|
return pc.isColorSupported;
|
|
2971
2945
|
};
|
|
2972
2946
|
const renderFinding = (finding, ctx) => {
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
ctx.io.stdout(
|
|
2976
|
-
ctx.io.stdout(` ${
|
|
2977
|
-
|
|
2978
|
-
if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${finding.docs}`));
|
|
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)}`));
|
|
2979
2952
|
};
|
|
2980
2953
|
const buildRenderCtx = (io) => {
|
|
2981
2954
|
const colors = pc.createColors(colorSupportedNow());
|
|
@@ -2990,16 +2963,25 @@ const buildRenderCtx = (io) => {
|
|
|
2990
2963
|
};
|
|
2991
2964
|
};
|
|
2992
2965
|
const prettyReporter = {
|
|
2993
|
-
format(result, io) {
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
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`);
|
|
2998
2983
|
}
|
|
2999
|
-
|
|
3000
|
-
const { error, warn, info } = result.summary;
|
|
3001
|
-
io.stdout("");
|
|
3002
|
-
io.stdout(`Summary: ${error} error, ${warn} warn, ${info} info`);
|
|
2984
|
+
await io.stdout(lines.join("\n"));
|
|
3003
2985
|
},
|
|
3004
2986
|
name: "pretty"
|
|
3005
2987
|
};
|
|
@@ -3016,16 +2998,16 @@ const JSON_REPORTER_NAME = jsonReporter.name;
|
|
|
3016
2998
|
const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
|
|
3017
2999
|
/** Later registrations replace earlier reporters with the same name. */
|
|
3018
3000
|
const createRegistry = (extras = []) => new Map([...BUILTINS, ...extras].map((reporter) => [reporter.name, reporter]));
|
|
3019
|
-
//#endregion
|
|
3020
|
-
//#region src/composition/lint.ts
|
|
3021
3001
|
const dependencies = {
|
|
3022
|
-
rules
|
|
3002
|
+
rules: createBuiltinRules({
|
|
3003
|
+
now: () => Date.now(),
|
|
3004
|
+
parse: (value) => Date.parse(value)
|
|
3005
|
+
}),
|
|
3023
3006
|
fileSystem: nodeFileSystem,
|
|
3024
3007
|
paths: nodePaths,
|
|
3025
3008
|
createRepoContext,
|
|
3026
3009
|
codecFor,
|
|
3027
|
-
|
|
3028
|
-
caseInsensitiveGlobs: process.platform === "darwin" || process.platform === "win32"
|
|
3010
|
+
compileExclusions
|
|
3029
3011
|
};
|
|
3030
3012
|
/** Public Node API: callers can replace the filesystem without assembling the application. */
|
|
3031
3013
|
const lint = (options) => lint$1(options, dependencies);
|
|
@@ -3035,11 +3017,47 @@ const lintCommand = (options, io) => lintCommand$1(options, io, dependencies, {
|
|
|
3035
3017
|
});
|
|
3036
3018
|
//#endregion
|
|
3037
3019
|
//#region src/adapters/node-io.ts
|
|
3038
|
-
const
|
|
3039
|
-
|
|
3040
|
-
|
|
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
|
+
};
|
|
3041
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);
|
|
3042
3060
|
//#endregion
|
|
3043
|
-
export {
|
|
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 };
|
|
3044
3062
|
|
|
3045
|
-
//# sourceMappingURL=node-io-
|
|
3063
|
+
//# sourceMappingURL=node-io-CAszdmko.mjs.map
|