@pho9ubenaa/siro 0.5.0 → 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 +81 -0
- package/README.md +55 -87
- package/dist/cli.mjs +24 -18
- package/dist/index.d.mts +112 -65
- package/dist/index.mjs +2 -2
- package/dist/{node-io-CaqobxYp.mjs → node-io-CAszdmko.mjs} +1138 -1048
- 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
|
-
import
|
|
7
|
+
import valid from "semver/functions/valid.js";
|
|
8
|
+
import { Minimatch } from "minimatch";
|
|
8
9
|
import ini from "ini";
|
|
9
10
|
import { parse as parse$1 } from "smol-toml";
|
|
10
11
|
import { parseDocument } from "yaml";
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
//#region src/shared/errors.ts
|
|
12
|
+
import pc from "picocolors";
|
|
13
|
+
//#region src/core/contracts/errors.ts
|
|
14
14
|
var SiroError = class extends Error {
|
|
15
15
|
exitCode;
|
|
16
16
|
constructor(message, exitCode) {
|
|
@@ -47,16 +47,32 @@ const wrapCodecError = (filePath, fn) => {
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
//#endregion
|
|
50
|
-
//#region src/
|
|
50
|
+
//#region src/core/contracts/paths.ts
|
|
51
|
+
const isRelPath = (value) => typeof value === "string" && value.length > 0 && !value.includes("\0") && !/^[\\/]/u.test(value) && !/^[a-z]:/iu.test(value) && !value.split(/[\\/]/u).includes("..");
|
|
52
|
+
const asRelPath = (value) => {
|
|
53
|
+
if (!isRelPath(value)) throw new TypeError("Expected a repository-relative path without parent traversal.");
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/adapters/node-paths.ts
|
|
51
58
|
const isAbsPath = (value) => typeof value === "string" && !value.includes("\0") && path.isAbsolute(value);
|
|
52
|
-
const isRelPath = (value) => typeof value === "string" && value.length > 0 && !value.includes("\0") && !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value) && !/^[a-z]:/iu.test(value) && !value.split(/[\\/]/u).includes("..");
|
|
53
59
|
const asAbsPath = (value) => {
|
|
54
60
|
if (!isAbsPath(value)) throw new TypeError("Expected an absolute filesystem path.");
|
|
55
61
|
return value;
|
|
56
62
|
};
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
const nodePaths = {
|
|
64
|
+
isAbsolute: isAbsPath,
|
|
65
|
+
resolve(root, relative) {
|
|
66
|
+
if (typeof relative !== "string" || relative.includes("\0") || path.isAbsolute(relative) || relative.split(path.sep === "\\" ? /[\\/]/u : /\//u).includes("..")) throw new ConfigError(`Invalid repository-relative path: ${String(relative)}`);
|
|
67
|
+
const resolved = path.join(root, relative);
|
|
68
|
+
const within = path.relative(root, resolved);
|
|
69
|
+
if (within === ".." || within.startsWith(`..${path.sep}`) || path.isAbsolute(within)) throw new ConfigError("Path escapes repository root.");
|
|
70
|
+
return asAbsPath(resolved);
|
|
71
|
+
},
|
|
72
|
+
child(parent, name) {
|
|
73
|
+
if (typeof name !== "string" || !name || name === "." || name === ".." || name.includes("\0") || name.includes("/") || path.sep === "\\" && (name.includes("\\") || name.includes(":"))) throw new ConfigError(`FileSystem.readDirectories returned an invalid child directory name: ${String(name)}`);
|
|
74
|
+
return parent === "." ? name : `${parent}/${name}`;
|
|
75
|
+
}
|
|
60
76
|
};
|
|
61
77
|
//#endregion
|
|
62
78
|
//#region src/adapters/node-errors.ts
|
|
@@ -71,24 +87,6 @@ const nodeFileSystem = {
|
|
|
71
87
|
readDirectories(directory) {
|
|
72
88
|
return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
73
89
|
},
|
|
74
|
-
resolveDirectory(parent, name) {
|
|
75
|
-
let target;
|
|
76
|
-
try {
|
|
77
|
-
target = lstatSync(path.join(parent, name), { bigint: true });
|
|
78
|
-
} catch (error) {
|
|
79
|
-
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
if (!target.isDirectory()) return void 0;
|
|
83
|
-
const names = readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
84
|
-
if (names.includes(name)) return name;
|
|
85
|
-
const matches = names.filter((candidate) => {
|
|
86
|
-
const entry = lstatSync(path.join(parent, candidate), { bigint: true });
|
|
87
|
-
return entry.isDirectory() && entry.dev === target.dev && entry.ino === target.ino;
|
|
88
|
-
});
|
|
89
|
-
if (target.ino === 0n || matches.length !== 1) throw new ConfigError(`${parent}: cannot identify the resolved workspace directory ${name}.`);
|
|
90
|
-
return matches[0];
|
|
91
|
-
},
|
|
92
90
|
exists(filePath) {
|
|
93
91
|
try {
|
|
94
92
|
assertRegularFile(filePath);
|
|
@@ -108,23 +106,36 @@ const nodeFileSystem = {
|
|
|
108
106
|
}
|
|
109
107
|
}
|
|
110
108
|
};
|
|
111
|
-
const resolveIn = (root, relPath) =>
|
|
109
|
+
const resolveIn = (root, relPath) => nodePaths.resolve(root, relPath);
|
|
112
110
|
const assertDirectory = (root) => {
|
|
113
111
|
if (!statSync(root).isDirectory()) throw new UsageError("The lint target must be a directory.");
|
|
114
112
|
};
|
|
115
113
|
//#endregion
|
|
116
114
|
//#region package.json
|
|
117
|
-
var version$1 = "0.
|
|
115
|
+
var version$1 = "0.6.0";
|
|
116
|
+
var engines = { "node": "^22.18.0 || ^24.0.0" };
|
|
118
117
|
//#endregion
|
|
119
|
-
//#region src/
|
|
120
|
-
const
|
|
118
|
+
//#region src/version.ts
|
|
119
|
+
const version = version$1;
|
|
120
|
+
const SUPPORTED_NODE_RANGE = engines.node;
|
|
121
|
+
//#endregion
|
|
122
|
+
//#region src/adapters/node-version.ts
|
|
121
123
|
const isSupportedNodeVersion = (version) => satisfies(version, SUPPORTED_NODE_RANGE);
|
|
122
124
|
//#endregion
|
|
123
|
-
//#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
|
|
124
134
|
/**
|
|
125
135
|
* Single source of truth for package managers and severities.
|
|
126
|
-
* Types, runtime sets, and ordering
|
|
127
|
-
*
|
|
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.
|
|
128
139
|
*/
|
|
129
140
|
const PMS = [
|
|
130
141
|
"npm",
|
|
@@ -160,24 +171,68 @@ const parsePackageManagerField = (value) => {
|
|
|
160
171
|
if (typeof name !== "undefined" && isPM(name)) return name;
|
|
161
172
|
};
|
|
162
173
|
//#endregion
|
|
163
|
-
//#region src/
|
|
164
|
-
|
|
165
|
-
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
|
+
};
|
|
166
186
|
//#endregion
|
|
167
|
-
//#region src/
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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()];
|
|
173
224
|
};
|
|
174
225
|
//#endregion
|
|
175
|
-
//#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
|
|
176
231
|
const isReporterShape = (value) => {
|
|
177
232
|
return isPlainRecord(value) && typeof value.name === "string" && typeof value.format === "function";
|
|
178
233
|
};
|
|
179
234
|
//#endregion
|
|
180
|
-
//#region src/
|
|
235
|
+
//#region src/core/contracts/config-value.ts
|
|
181
236
|
const CODEC_KINDS = [
|
|
182
237
|
"json",
|
|
183
238
|
"npmrc",
|
|
@@ -200,14 +255,16 @@ const toParsedConfig = (value) => {
|
|
|
200
255
|
throw new TypeError("Config root must be a mapping.");
|
|
201
256
|
};
|
|
202
257
|
//#endregion
|
|
203
|
-
//#region src/
|
|
204
|
-
const defineRule = (rule) => rule;
|
|
205
|
-
const isOptionalString = (value) => typeof value === "undefined" || typeof value === "string";
|
|
258
|
+
//#region src/core/contracts/config-file-ref.ts
|
|
206
259
|
const CONFIG_FILE_KINDS = new Set(CODEC_KINDS);
|
|
207
260
|
const isConfigFileRefShape = (value) => {
|
|
208
261
|
if (!isPlainRecord(value) || typeof value.kind !== "string" || !isRelPath(value.path)) return false;
|
|
209
262
|
return CONFIG_FILE_KINDS.has(value.kind);
|
|
210
263
|
};
|
|
264
|
+
//#endregion
|
|
265
|
+
//#region src/core/contracts/rule.ts
|
|
266
|
+
const defineRule = (rule) => rule;
|
|
267
|
+
const isOptionalString = (value) => typeof value === "undefined" || typeof value === "string";
|
|
211
268
|
const isConfigValueShape = (value) => typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
212
269
|
const isDenseStringArray = (value) => Array.isArray(value) && Array.from(value).every((item) => typeof item === "string");
|
|
213
270
|
const isCheckStatusShape = (value) => {
|
|
@@ -239,22 +296,26 @@ const isRuleShape = (value) => {
|
|
|
239
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);
|
|
240
297
|
};
|
|
241
298
|
//#endregion
|
|
242
|
-
//#region src/
|
|
243
|
-
/**
|
|
244
|
-
const
|
|
245
|
-
if (typeof value
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
/** Keep name-only detection independent from whether the declaration pins a version. */
|
|
250
|
-
const declaredPMVersion = (declaration) => {
|
|
251
|
-
const [pm, version, ...extra] = declaration?.trim().split("@") ?? [];
|
|
252
|
-
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
|
+
}
|
|
253
306
|
};
|
|
254
307
|
//#endregion
|
|
255
|
-
//#region src/
|
|
308
|
+
//#region src/core/parse-siro-config.ts
|
|
256
309
|
const RuleSettingSchema = vb.union([vb.picklist(SEVERITIES), vb.literal("off")]);
|
|
257
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
|
+
})),
|
|
258
319
|
customRules: vb.optional(vb.array(vb.custom(isRuleShape, "must be a structurally valid rule"))),
|
|
259
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)"))),
|
|
260
321
|
projectType: vb.optional(vb.picklist(PROJECT_TYPES)),
|
|
@@ -285,13 +346,14 @@ const formatIssues = (issues) => issues.map((issue) => {
|
|
|
285
346
|
return issue.message;
|
|
286
347
|
}).join("; ");
|
|
287
348
|
const parseConfig = (candidate, name = "siro.config") => {
|
|
349
|
+
assertSynchronous(candidate, name);
|
|
288
350
|
if (!isPlainRecord(candidate)) throw new ConfigError(`${name} must export a config object (got ${Array.isArray(candidate) ? "an array" : typeof candidate}).`);
|
|
289
351
|
const result = vb.safeParse(ConfigSchema, Object.fromEntries(Object.entries(candidate)));
|
|
290
352
|
if (!result.success) throw new ConfigError(`${name}: ${formatIssues(result.issues)}`);
|
|
291
353
|
return result.output;
|
|
292
354
|
};
|
|
293
355
|
//#endregion
|
|
294
|
-
//#region src/
|
|
356
|
+
//#region src/load-config.ts
|
|
295
357
|
const CONFIG_NAMES = [
|
|
296
358
|
"siro.config.ts",
|
|
297
359
|
"siro.config.mjs",
|
|
@@ -315,150 +377,7 @@ const loadConfig = async (cwd, nodeVersion = process.versions.node) => {
|
|
|
315
377
|
return parseConfig(mod !== null && typeof mod === "object" && "default" in mod ? mod.default : mod, name);
|
|
316
378
|
};
|
|
317
379
|
//#endregion
|
|
318
|
-
//#region src/
|
|
319
|
-
const EXIT_SUCCESS = 0;
|
|
320
|
-
const EXIT_FAILURE = 1;
|
|
321
|
-
const meetsThreshold = (severity, threshold) => SEVERITY_RANK[severity] >= SEVERITY_RANK[threshold];
|
|
322
|
-
/** Keep only findings at or above `threshold`, recomputing the summary. */
|
|
323
|
-
const filterBySeverity = (result, threshold) => {
|
|
324
|
-
const findings = [];
|
|
325
|
-
const summary = {
|
|
326
|
-
error: 0,
|
|
327
|
-
info: 0,
|
|
328
|
-
warn: 0
|
|
329
|
-
};
|
|
330
|
-
for (const finding of result.findings) if (meetsThreshold(finding.severity, threshold)) {
|
|
331
|
-
findings.push(finding);
|
|
332
|
-
summary[finding.severity] += 1;
|
|
333
|
-
}
|
|
334
|
-
return {
|
|
335
|
-
findings,
|
|
336
|
-
summary
|
|
337
|
-
};
|
|
338
|
-
};
|
|
339
|
-
/**
|
|
340
|
-
* Exit code for a lint run. Non-zero when any finding meets `threshold`
|
|
341
|
-
* (default: only `error` fails the run).
|
|
342
|
-
*/
|
|
343
|
-
const exitCodeForLint = (result, threshold = "error") => {
|
|
344
|
-
if (result.findings.some((finding) => meetsThreshold(finding.severity, threshold))) return EXIT_FAILURE;
|
|
345
|
-
return EXIT_SUCCESS;
|
|
346
|
-
};
|
|
347
|
-
//#endregion
|
|
348
|
-
//#region src/adapters/reporters/github.ts
|
|
349
|
-
const COMMAND = {
|
|
350
|
-
error: "error",
|
|
351
|
-
info: "notice",
|
|
352
|
-
warn: "warning"
|
|
353
|
-
};
|
|
354
|
-
const escapeData = (raw) => raw.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
355
|
-
const escapeProp = (raw) => escapeData(raw).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
356
|
-
/** Emit GitHub Actions workflow commands (annotations on PRs). */
|
|
357
|
-
const githubReporter = {
|
|
358
|
-
format(result, io) {
|
|
359
|
-
for (const finding of result.findings) {
|
|
360
|
-
let file = "";
|
|
361
|
-
if (finding.file) file = `file=${escapeProp(finding.file)},`;
|
|
362
|
-
let body = `[${finding.pm}] ${finding.message}`;
|
|
363
|
-
if (finding.docs) body += ` (${finding.docs})`;
|
|
364
|
-
io.stdout(`::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`);
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
name: "github"
|
|
368
|
-
};
|
|
369
|
-
//#endregion
|
|
370
|
-
//#region src/version.ts
|
|
371
|
-
const version = version$1;
|
|
372
|
-
//#endregion
|
|
373
|
-
//#region src/adapters/reporters/json.ts
|
|
374
|
-
/**
|
|
375
|
-
* Versioned machine-readable output — the public contract consumed by
|
|
376
|
-
* external fixers (see docs/json-output.md). Bump `schemaVersion` on any
|
|
377
|
-
* breaking shape change and update that doc in the same commit.
|
|
378
|
-
*/
|
|
379
|
-
const SCHEMA_VERSION = 2;
|
|
380
|
-
const jsonReporter = {
|
|
381
|
-
format(result, io) {
|
|
382
|
-
io.stdout(JSON.stringify({
|
|
383
|
-
schemaVersion: SCHEMA_VERSION,
|
|
384
|
-
siroVersion: version,
|
|
385
|
-
findings: result.findings,
|
|
386
|
-
summary: result.summary
|
|
387
|
-
}, void 0, 2));
|
|
388
|
-
},
|
|
389
|
-
name: "json"
|
|
390
|
-
};
|
|
391
|
-
//#endregion
|
|
392
|
-
//#region src/adapters/reporters/pretty.ts
|
|
393
|
-
const GLYPH = {
|
|
394
|
-
error: "✖ error",
|
|
395
|
-
info: "ℹ info",
|
|
396
|
-
warn: "⚠ warn"
|
|
397
|
-
};
|
|
398
|
-
/**
|
|
399
|
-
* Decide colour at call time, not at import time.
|
|
400
|
-
*
|
|
401
|
-
* picocolors snapshots `isColorSupported` when imported, so a later
|
|
402
|
-
* `process.env.NO_COLOR = '1'` (or a different stdout) is ignored. The
|
|
403
|
-
* reporter is invoked per `siro lint` run from many contexts (TTY, CI
|
|
404
|
-
* with FORCE_COLOR, redirected output, …) — checking each call is the
|
|
405
|
-
* only way NO_COLOR can truly win, as https://no-color.org/ requires.
|
|
406
|
-
*/
|
|
407
|
-
const colorSupportedNow = () => {
|
|
408
|
-
const { env } = process;
|
|
409
|
-
if (typeof env.NO_COLOR !== "undefined" && env.NO_COLOR !== "") return false;
|
|
410
|
-
if ("FORCE_COLOR" in env) return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "";
|
|
411
|
-
return pc.isColorSupported;
|
|
412
|
-
};
|
|
413
|
-
const renderFinding = (finding, ctx) => {
|
|
414
|
-
let where = "";
|
|
415
|
-
if (finding.file) where = ctx.colors.dim(` (${finding.file})`);
|
|
416
|
-
ctx.io.stdout(`${ctx.tag[finding.severity](GLYPH[finding.severity])} [${finding.pm}] ${ctx.colors.bold(finding.ruleId)}${where}`);
|
|
417
|
-
ctx.io.stdout(` ${finding.message}`);
|
|
418
|
-
for (const step of finding.remediation?.kind === "manual" ? finding.remediation.steps : []) ctx.io.stdout(` ↳ ${step}`);
|
|
419
|
-
if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${finding.docs}`));
|
|
420
|
-
};
|
|
421
|
-
const buildRenderCtx = (io) => {
|
|
422
|
-
const colors = pc.createColors(colorSupportedNow());
|
|
423
|
-
return {
|
|
424
|
-
colors,
|
|
425
|
-
io,
|
|
426
|
-
tag: {
|
|
427
|
-
error: colors.red,
|
|
428
|
-
info: colors.cyan,
|
|
429
|
-
warn: colors.yellow
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
};
|
|
433
|
-
const prettyReporter = {
|
|
434
|
-
format(result, io) {
|
|
435
|
-
const ctx = buildRenderCtx(io);
|
|
436
|
-
if (result.findings.length === 0) {
|
|
437
|
-
io.stdout(ctx.colors.green("✔ No security best-practice issues found."));
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
for (const finding of result.findings) renderFinding(finding, ctx);
|
|
441
|
-
const { error, warn, info } = result.summary;
|
|
442
|
-
io.stdout("");
|
|
443
|
-
io.stdout(`Summary: ${error} error, ${warn} warn, ${info} info`);
|
|
444
|
-
},
|
|
445
|
-
name: "pretty"
|
|
446
|
-
};
|
|
447
|
-
//#endregion
|
|
448
|
-
//#region src/adapters/reporters/registry.ts
|
|
449
|
-
const BUILTINS = [
|
|
450
|
-
prettyReporter,
|
|
451
|
-
jsonReporter,
|
|
452
|
-
githubReporter
|
|
453
|
-
];
|
|
454
|
-
const DEFAULT_REPORTER_NAME = prettyReporter.name;
|
|
455
|
-
const JSON_REPORTER_NAME = jsonReporter.name;
|
|
456
|
-
/** Names of every built-in reporter, in stable display order. */
|
|
457
|
-
const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
|
|
458
|
-
/** Later registrations replace earlier reporters with the same name. */
|
|
459
|
-
const createRegistry = (extras = []) => new Map([...BUILTINS, ...extras].map((reporter) => [reporter.name, reporter]));
|
|
460
|
-
//#endregion
|
|
461
|
-
//#region src/domain/entities/config-files.ts
|
|
380
|
+
//#region src/core/config-files.ts
|
|
462
381
|
/** Known configuration locations and their parsers. */
|
|
463
382
|
const CONFIG_FILES = {
|
|
464
383
|
aubeWorkspace: {
|
|
@@ -491,509 +410,7 @@ const CONFIG_FILES = {
|
|
|
491
410
|
}
|
|
492
411
|
};
|
|
493
412
|
//#endregion
|
|
494
|
-
//#region src/
|
|
495
|
-
const EMPTY_FILE = Object.freeze({});
|
|
496
|
-
/** Create a fresh parser and cache for one lint command. */
|
|
497
|
-
const createConfigParser = (codecFor, ctx) => {
|
|
498
|
-
const cache = /* @__PURE__ */ new Map();
|
|
499
|
-
return (file) => {
|
|
500
|
-
if (file === void 0) return EMPTY_FILE;
|
|
501
|
-
const cacheKey = `${file.kind}:${file.path}`;
|
|
502
|
-
const cached = cache.get(cacheKey);
|
|
503
|
-
if (cached !== void 0) return cached;
|
|
504
|
-
const text = ctx.readText(file.path);
|
|
505
|
-
if (text === void 0) {
|
|
506
|
-
cache.set(cacheKey, EMPTY_FILE);
|
|
507
|
-
return EMPTY_FILE;
|
|
508
|
-
}
|
|
509
|
-
const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
|
|
510
|
-
cache.set(cacheKey, parsed);
|
|
511
|
-
return parsed;
|
|
512
|
-
};
|
|
513
|
-
};
|
|
514
|
-
//#endregion
|
|
515
|
-
//#region src/adapters/codecs/ini.ts
|
|
516
|
-
const INTEGER_PATTERN = /^-?\d+$/u;
|
|
517
|
-
const coerceBoolean = (str) => {
|
|
518
|
-
if (str === "true") return true;
|
|
519
|
-
if (str === "false") return false;
|
|
520
|
-
};
|
|
521
|
-
const coerceInteger = (str) => {
|
|
522
|
-
if (INTEGER_PATTERN.test(str)) return Number(str);
|
|
523
|
-
};
|
|
524
|
-
const coerce = (raw) => {
|
|
525
|
-
if (typeof raw !== "string") return raw;
|
|
526
|
-
return coerceBoolean(raw) ?? coerceInteger(raw) ?? raw;
|
|
527
|
-
};
|
|
528
|
-
//#endregion
|
|
529
|
-
//#region src/adapters/codecs/store.ts
|
|
530
|
-
const CODECS = {
|
|
531
|
-
json: { parse(text) {
|
|
532
|
-
const trimmed = text.trim();
|
|
533
|
-
return toParsedConfig(JSON.parse(trimmed));
|
|
534
|
-
} },
|
|
535
|
-
npmrc: { parse(text) {
|
|
536
|
-
const raw = toParsedConfig(ini.parse(text));
|
|
537
|
-
const config = {};
|
|
538
|
-
for (const [key, value] of Object.entries(raw)) if (Array.isArray(value)) config[key] = value.map((elem) => coerce(elem));
|
|
539
|
-
else if (typeof value === "object" && value !== null) config[key] = toParsedConfig(value);
|
|
540
|
-
else config[key] = coerce(value);
|
|
541
|
-
return config;
|
|
542
|
-
} },
|
|
543
|
-
toml: { parse(text) {
|
|
544
|
-
if (text.trim() === "") return {};
|
|
545
|
-
return toParsedConfig(parse$1(text.replace(/^\uFEFF/u, "")));
|
|
546
|
-
} },
|
|
547
|
-
yaml: { parse(text) {
|
|
548
|
-
if (text.trim() === "") return {};
|
|
549
|
-
const document = parseDocument(text);
|
|
550
|
-
if (document.errors.length > 0) throw document.errors[0];
|
|
551
|
-
if (document.contents === null) return {};
|
|
552
|
-
return toParsedConfig(document.toJS({ onAnchor: (value, count) => {
|
|
553
|
-
if (count > 1) JSON.stringify(value);
|
|
554
|
-
} }));
|
|
555
|
-
} }
|
|
556
|
-
};
|
|
557
|
-
/** Look up the codec for a parseable kind. Total — every CodecKind has one. */
|
|
558
|
-
const codecFor = (kind) => CODECS[kind];
|
|
559
|
-
//#endregion
|
|
560
|
-
//#region src/domain/schemas/package-json.ts
|
|
561
|
-
const PackageJsonSchema = vb.looseObject({
|
|
562
|
-
files: vb.optional(vb.array(vb.string())),
|
|
563
|
-
name: vb.optional(vb.string()),
|
|
564
|
-
packageManager: vb.optional(vb.string()),
|
|
565
|
-
private: vb.optional(vb.boolean()),
|
|
566
|
-
publishConfig: vb.optional(vb.looseObject({ access: vb.optional(vb.picklist([
|
|
567
|
-
"public",
|
|
568
|
-
"restricted",
|
|
569
|
-
"private"
|
|
570
|
-
])) })),
|
|
571
|
-
trustedDependencies: vb.optional(vb.array(vb.string()))
|
|
572
|
-
});
|
|
573
|
-
const parsePackageJson = (value) => {
|
|
574
|
-
if (!isPlainRecord(value)) throw new ConfigError("package.json: expected an object at the root.");
|
|
575
|
-
const result = vb.safeParse(PackageJsonSchema, value);
|
|
576
|
-
if (result.success) return result.output;
|
|
577
|
-
const issue = result.issues[0];
|
|
578
|
-
const field = vb.getDotPath(issue);
|
|
579
|
-
throw new ConfigError(`package.json: ${field ? `${field}: ` : ""}${issue.message}`);
|
|
580
|
-
};
|
|
581
|
-
//#endregion
|
|
582
|
-
//#region src/adapters/repo-context.ts
|
|
583
|
-
const tryParseJson = (text) => {
|
|
584
|
-
try {
|
|
585
|
-
return JSON.parse(text);
|
|
586
|
-
} catch (error) {
|
|
587
|
-
let errMsg = String(error);
|
|
588
|
-
if (error instanceof Error) errMsg = error.message;
|
|
589
|
-
throw new ConfigError(`package.json: invalid JSON — ${errMsg}`);
|
|
590
|
-
}
|
|
591
|
-
};
|
|
592
|
-
const readPackageJson = (raw) => {
|
|
593
|
-
return parsePackageJson(tryParseJson(raw.trim()));
|
|
594
|
-
};
|
|
595
|
-
const createRepoContext = (root, fs = nodeFileSystem, projectType) => {
|
|
596
|
-
if (fs === nodeFileSystem) assertDirectory(root);
|
|
597
|
-
const readText = (relPath) => fs.readText(resolveIn(root, relPath));
|
|
598
|
-
const exists = (relPath) => fs.exists(resolveIn(root, relPath));
|
|
599
|
-
const raw = readText(asRelPath("package.json"));
|
|
600
|
-
let packageJson = void 0;
|
|
601
|
-
if (typeof raw !== "undefined") packageJson = readPackageJson(raw);
|
|
602
|
-
return {
|
|
603
|
-
exists,
|
|
604
|
-
packageJson,
|
|
605
|
-
projectType,
|
|
606
|
-
readText,
|
|
607
|
-
root
|
|
608
|
-
};
|
|
609
|
-
};
|
|
610
|
-
//#endregion
|
|
611
|
-
//#region src/application/workspace-prefix.ts
|
|
612
|
-
/** Anchor Deno's native literal prefix before applying its glob suffix. */
|
|
613
|
-
const anchorWorkspacePrefix = (pattern, glob, resolveChild) => {
|
|
614
|
-
const parts = pattern.split("/");
|
|
615
|
-
const firstWildcard = parts.findIndex((part) => /[*?]/u.test(part));
|
|
616
|
-
const prefix = parts.slice(0, firstWildcard < 0 ? parts.length : firstWildcard);
|
|
617
|
-
if (prefix.length === 0 || pattern === ".") return glob;
|
|
618
|
-
const declared = prefix.join("/");
|
|
619
|
-
let resolved;
|
|
620
|
-
let attempted = false;
|
|
621
|
-
const resolve = () => {
|
|
622
|
-
if (!attempted) {
|
|
623
|
-
attempted = true;
|
|
624
|
-
let current = ".";
|
|
625
|
-
for (const part of prefix) {
|
|
626
|
-
const child = resolveChild(current, part);
|
|
627
|
-
if (child === void 0) return void 0;
|
|
628
|
-
current = current === "." ? child : `${current}/${child}`;
|
|
629
|
-
}
|
|
630
|
-
resolved = current;
|
|
631
|
-
}
|
|
632
|
-
return resolved;
|
|
633
|
-
};
|
|
634
|
-
const rewrite = (directory, actual) => directory === actual ? declared : directory.startsWith(`${actual}/`) ? declared + directory.slice(actual.length) : void 0;
|
|
635
|
-
return {
|
|
636
|
-
matches(directory) {
|
|
637
|
-
if (directory === ".") return false;
|
|
638
|
-
const actual = resolve();
|
|
639
|
-
const rewritten = actual === void 0 ? void 0 : rewrite(directory, actual);
|
|
640
|
-
return rewritten !== void 0 && glob.matches(rewritten);
|
|
641
|
-
},
|
|
642
|
-
canDescend(directory) {
|
|
643
|
-
const actual = resolve();
|
|
644
|
-
if (actual === void 0) return false;
|
|
645
|
-
if (directory === "." || actual.startsWith(`${directory}/`)) return true;
|
|
646
|
-
const rewritten = rewrite(directory, actual);
|
|
647
|
-
return rewritten !== void 0 && glob.canDescend(rewritten);
|
|
648
|
-
}
|
|
649
|
-
};
|
|
650
|
-
};
|
|
651
|
-
//#endregion
|
|
652
|
-
//#region src/application/workspace-globs.ts
|
|
653
|
-
const MAX_WORKSPACE_GLOB_ALTERNATIVES = 8192;
|
|
654
|
-
const BRACE_EXPANSION_PROBE_LIMIT = 8193;
|
|
655
|
-
const defaultWorkspaceGlobOptions = {
|
|
656
|
-
platform: "linux",
|
|
657
|
-
nocase: process.platform === "darwin" || process.platform === "win32",
|
|
658
|
-
windowsPathsNoEscape: true,
|
|
659
|
-
nonegate: true,
|
|
660
|
-
nocomment: true,
|
|
661
|
-
optimizationLevel: 2
|
|
662
|
-
};
|
|
663
|
-
const boundedBraceExpand = (pattern) => {
|
|
664
|
-
const alternatives = braceExpand(pattern, { braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT });
|
|
665
|
-
if (alternatives.length > MAX_WORKSPACE_GLOB_ALTERNATIVES) throw new ConfigError(`Workspace pattern expands beyond ${MAX_WORKSPACE_GLOB_ALTERNATIVES} alternatives.`);
|
|
666
|
-
return alternatives;
|
|
667
|
-
};
|
|
668
|
-
/** Expand with minimatch's own bounded brace semantics. */
|
|
669
|
-
const expandWorkspaceGlob = (pattern) => {
|
|
670
|
-
try {
|
|
671
|
-
return boundedBraceExpand(pattern);
|
|
672
|
-
} catch (error) {
|
|
673
|
-
if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
|
|
674
|
-
throw error;
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
/** Compile once so membership, exclusions, and traversal share glob semantics. */
|
|
678
|
-
const compileWorkspaceGlob = (pattern, options = defaultWorkspaceGlobOptions) => {
|
|
679
|
-
let matcher;
|
|
680
|
-
try {
|
|
681
|
-
if (!options.nobrace) boundedBraceExpand(pattern);
|
|
682
|
-
matcher = new Minimatch(pattern, {
|
|
683
|
-
...options,
|
|
684
|
-
braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT
|
|
685
|
-
});
|
|
686
|
-
} catch (error) {
|
|
687
|
-
if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
|
|
688
|
-
throw error;
|
|
689
|
-
}
|
|
690
|
-
return {
|
|
691
|
-
matches: (directory) => matcher.match(directory),
|
|
692
|
-
canDescend(directory) {
|
|
693
|
-
const parts = directory.split("/");
|
|
694
|
-
return matcher.set.some((alternative) => (alternative.includes(GLOBSTAR) || parts.length < alternative.length) && matcher.matchOne(parts, alternative, true));
|
|
695
|
-
}
|
|
696
|
-
};
|
|
697
|
-
};
|
|
698
|
-
//#endregion
|
|
699
|
-
//#region src/application/workspace-dialects.ts
|
|
700
|
-
const matchesCrossPathGlob = (pattern, value) => {
|
|
701
|
-
const patternCharacters = [...pattern];
|
|
702
|
-
const valueCharacters = [...value];
|
|
703
|
-
let patternIndex = 0;
|
|
704
|
-
let valueIndex = 0;
|
|
705
|
-
let starIndex = -1;
|
|
706
|
-
let retryIndex = 0;
|
|
707
|
-
while (valueIndex < valueCharacters.length) {
|
|
708
|
-
const character = patternCharacters[patternIndex];
|
|
709
|
-
if (character === "?" || character === valueCharacters[valueIndex]) {
|
|
710
|
-
patternIndex += 1;
|
|
711
|
-
valueIndex += 1;
|
|
712
|
-
} else if (character === "*") {
|
|
713
|
-
starIndex = patternIndex;
|
|
714
|
-
retryIndex = valueIndex;
|
|
715
|
-
patternIndex += 1;
|
|
716
|
-
} else if (starIndex >= 0) {
|
|
717
|
-
patternIndex = starIndex + 1;
|
|
718
|
-
retryIndex += 1;
|
|
719
|
-
valueIndex = retryIndex;
|
|
720
|
-
} else return false;
|
|
721
|
-
}
|
|
722
|
-
while (patternCharacters[patternIndex] === "*") patternIndex += 1;
|
|
723
|
-
return patternIndex === patternCharacters.length;
|
|
724
|
-
};
|
|
725
|
-
/** The bounded glob subset verified against the recorded Deno/Aube sources. */
|
|
726
|
-
const compileAdditionalWorkspaceGlob = (pattern, pm, excluded) => {
|
|
727
|
-
const parts = pattern.split("/");
|
|
728
|
-
if (parts.some((part) => part.includes("**") && part !== "**")) throw new ConfigError(`${pm}: ** must occupy an entire workspace path component.`);
|
|
729
|
-
if (pm === "aube" && (/[[\]{}()]/u.test(pattern) || pattern.includes("**") && (parts.at(-1) !== "**" || /[*?]/u.test(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.");
|
|
730
|
-
const escaped = pm === "deno" ? pattern.replace(/[[\]]/gu, (character) => character === "[" ? "[[]" : "[]]") : pattern;
|
|
731
|
-
const matcher = compileWorkspaceGlob(escaped, {
|
|
732
|
-
platform: "linux",
|
|
733
|
-
windowsPathsNoEscape: true,
|
|
734
|
-
nonegate: true,
|
|
735
|
-
nocomment: true,
|
|
736
|
-
nobrace: true,
|
|
737
|
-
noext: true,
|
|
738
|
-
dot: pm === "aube",
|
|
739
|
-
nocase: pm === "deno" && (/[*?]/u.test(pattern) || process.platform === "darwin" || process.platform === "win32")
|
|
740
|
-
});
|
|
741
|
-
if (pm === "deno" && /[*?]/u.test(pattern)) {
|
|
742
|
-
const files = compileWorkspaceGlob(`${escaped}/package.json`, {
|
|
743
|
-
platform: "linux",
|
|
744
|
-
windowsPathsNoEscape: true,
|
|
745
|
-
nonegate: true,
|
|
746
|
-
nocomment: true,
|
|
747
|
-
nobrace: true,
|
|
748
|
-
noext: true,
|
|
749
|
-
nocase: true
|
|
750
|
-
});
|
|
751
|
-
return {
|
|
752
|
-
...matcher,
|
|
753
|
-
matches: (directory) => files.matches(directory === "." ? "package.json" : `${directory}/package.json`)
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
if (pm !== "aube" || !excluded) return matcher;
|
|
757
|
-
const self = pattern.endsWith("/**") ? pattern.slice(0, -3) : void 0;
|
|
758
|
-
return {
|
|
759
|
-
...matcher,
|
|
760
|
-
matches: (directory) => matchesCrossPathGlob(pattern, directory) || directory === self
|
|
761
|
-
};
|
|
762
|
-
};
|
|
763
|
-
//#endregion
|
|
764
|
-
//#region src/application/workspaces.ts
|
|
765
|
-
/** npm cancels an earlier exclusion when a later positive pattern matches it. */
|
|
766
|
-
const splitNpmPattern = (raw) => {
|
|
767
|
-
const prefix = /^!+/u.exec(raw)?.[0] ?? "";
|
|
768
|
-
return {
|
|
769
|
-
excluded: prefix.length % 2 === 1,
|
|
770
|
-
pattern: raw.slice(prefix.length)
|
|
771
|
-
};
|
|
772
|
-
};
|
|
773
|
-
const npmPatterns = (patterns) => {
|
|
774
|
-
const positive = [];
|
|
775
|
-
let negative = [];
|
|
776
|
-
for (const raw of patterns) {
|
|
777
|
-
const parsed = splitNpmPattern(raw);
|
|
778
|
-
const pattern = parsed.pattern.replace(/^\.?\/+/u, "");
|
|
779
|
-
if (parsed.excluded) negative.push({
|
|
780
|
-
pattern,
|
|
781
|
-
glob: compileWorkspaceGlob(pattern, {})
|
|
782
|
-
});
|
|
783
|
-
else {
|
|
784
|
-
for (let index = 0; index < negative.length; index += 1) if (negative[index]?.glob.matches(pattern)) negative.splice(index, 1);
|
|
785
|
-
positive.push(pattern);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
return [...positive, ...negative.map(({ pattern }) => `!${pattern}`)];
|
|
789
|
-
};
|
|
790
|
-
/** Read declaration sources separately so Deno's two manifest sets stay distinct. */
|
|
791
|
-
const workspaceDefinitions = (ctx, pm) => {
|
|
792
|
-
const parse = createConfigParser(codecFor, ctx);
|
|
793
|
-
const validate = (value, source, denoManifests = false) => {
|
|
794
|
-
if (value === void 0) return {
|
|
795
|
-
patterns: [],
|
|
796
|
-
denoManifests
|
|
797
|
-
};
|
|
798
|
-
if (!Array.isArray(value) || !Array.from(value).every((item) => typeof item === "string")) throw new ConfigError(`${source}: expected an array of directory patterns.`);
|
|
799
|
-
for (const pattern of value) {
|
|
800
|
-
const positive = pm === "npm" ? splitNpmPattern(pattern).pattern : pattern.startsWith("!") ? pattern.slice(1) : pattern;
|
|
801
|
-
if ((pm === "deno" || pm === "aube" ? [positive] : expandWorkspaceGlob(positive)).some((alternative) => !isRelPath(alternative) || alternative.includes("\\") || alternative.startsWith("!"))) throw new ConfigError(`${source}: use relative directory patterns without parent traversal: ${JSON.stringify(pattern)}.`);
|
|
802
|
-
if (pm === "deno" && denoManifests && !pattern.startsWith("!") && path.posix.normalize(positive) === ".") throw new ConfigError(`${source}: a Deno workspace cannot contain itself.`);
|
|
803
|
-
}
|
|
804
|
-
return {
|
|
805
|
-
patterns: pm === "npm" ? npmPatterns(value) : value,
|
|
806
|
-
denoManifests
|
|
807
|
-
};
|
|
808
|
-
};
|
|
809
|
-
const packageDefinition = () => {
|
|
810
|
-
let value = ctx.packageJson?.workspaces;
|
|
811
|
-
if (isPlainRecord(value)) {
|
|
812
|
-
value = value.packages;
|
|
813
|
-
if (value === void 0) throw new ConfigError("package.json#workspaces: expected a packages array.");
|
|
814
|
-
}
|
|
815
|
-
return validate(value, "package.json#workspaces");
|
|
816
|
-
};
|
|
817
|
-
if (pm === "pnpm") return [validate(ctx.exists(CONFIG_FILES.pnpmWorkspace.path) ? parse(CONFIG_FILES.pnpmWorkspace).packages : void 0, "pnpm-workspace.yaml#packages")];
|
|
818
|
-
if (pm === "aube") {
|
|
819
|
-
const file = [CONFIG_FILES.aubeWorkspace, CONFIG_FILES.pnpmWorkspace].find((candidate) => ctx.exists(candidate.path));
|
|
820
|
-
return [file ? validate(parse(file).packages, `${file.path}#packages`) : packageDefinition()];
|
|
821
|
-
}
|
|
822
|
-
if (pm === "deno") return [validate(parse(CONFIG_FILES.denoJson).workspace, "deno.json#workspace", true), packageDefinition()];
|
|
823
|
-
return [packageDefinition()];
|
|
824
|
-
};
|
|
825
|
-
/** Traverse only ordinary directories that can fall under a positive pattern. */
|
|
826
|
-
const workspaceDirectories = (ctx, fs, definition, pm) => {
|
|
827
|
-
const { patterns } = definition;
|
|
828
|
-
const skipVendor = pm === "deno" && createConfigParser(codecFor, ctx)(CONFIG_FILES.denoJson).vendor === true;
|
|
829
|
-
const positive = patterns.filter((pattern) => !pattern.startsWith("!")).map((pattern) => path.posix.normalize(pattern).replace(/\/+$/u, ""));
|
|
830
|
-
const negative = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => path.posix.normalize(pattern.slice(1)).replace(/\/+$/u, ""));
|
|
831
|
-
const directoryCache = /* @__PURE__ */ new Map();
|
|
832
|
-
const readDirectories = (directory) => {
|
|
833
|
-
const cached = directoryCache.get(directory);
|
|
834
|
-
if (cached) return cached;
|
|
835
|
-
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
836
|
-
const names = fs.readDirectories(resolveIn(ctx.root, asRelPath(directory)));
|
|
837
|
-
if (!Array.isArray(names)) throw new ConfigError("FileSystem.readDirectories must return an array of directory names.");
|
|
838
|
-
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.");
|
|
839
|
-
const result = [...new Set(names)].sort();
|
|
840
|
-
directoryCache.set(directory, result);
|
|
841
|
-
return result;
|
|
842
|
-
};
|
|
843
|
-
const resolveChild = (parent, name) => {
|
|
844
|
-
if (name === ".git" || name === "node_modules") return void 0;
|
|
845
|
-
const names = readDirectories(parent);
|
|
846
|
-
const resolved = names.includes(name) ? name : fs.resolveDirectory?.(resolveIn(ctx.root, asRelPath(parent)), name);
|
|
847
|
-
if (resolved !== void 0 && !names.includes(resolved)) throw new ConfigError("FileSystem.resolveDirectory must return an enumerated ordinary child directory name.");
|
|
848
|
-
return resolved === ".git" || resolved === "node_modules" ? void 0 : resolved;
|
|
849
|
-
};
|
|
850
|
-
const compile = (pattern, excluded = false) => {
|
|
851
|
-
if (pm !== "deno" && pm !== "aube") return compileWorkspaceGlob(pattern, pm === "npm" ? {
|
|
852
|
-
...defaultWorkspaceGlobOptions,
|
|
853
|
-
nocomment: false
|
|
854
|
-
} : defaultWorkspaceGlobOptions);
|
|
855
|
-
const glob = compileAdditionalWorkspaceGlob(pattern, pm, excluded);
|
|
856
|
-
return pm === "deno" && !excluded ? anchorWorkspacePrefix(pattern, glob, resolveChild) : glob;
|
|
857
|
-
};
|
|
858
|
-
const included = positive.map((pattern) => compile(pattern));
|
|
859
|
-
const excluded = negative.map((pattern) => {
|
|
860
|
-
return {
|
|
861
|
-
glob: compile(pattern, true),
|
|
862
|
-
subtree: pm === "deno" || pm === "aube" || pm === "bun" ? void 0 : pattern === "**" ? { matches: () => true } : pattern.endsWith("/**") ? compile(pattern.slice(0, -3), true) : void 0
|
|
863
|
-
};
|
|
864
|
-
});
|
|
865
|
-
const declaredBase = (pattern) => {
|
|
866
|
-
const parts = pattern.split("/");
|
|
867
|
-
const wildcard = parts.findIndex((part) => /[*?]/u.test(part));
|
|
868
|
-
return parts.slice(0, wildcard < 0 ? parts.length : wildcard).join("/") || ".";
|
|
869
|
-
};
|
|
870
|
-
const relatedBases = (left, right) => left === "." || right === "." || left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
871
|
-
const denoPositive = pm === "deno" ? positive.filter((pattern) => /[*?]/u.test(pattern)).map((pattern) => ({
|
|
872
|
-
base: declaredBase(pattern),
|
|
873
|
-
scope: anchorWorkspacePrefix(declaredBase(pattern), {
|
|
874
|
-
matches: () => true,
|
|
875
|
-
canDescend: () => true
|
|
876
|
-
}, resolveChild)
|
|
877
|
-
})) : [];
|
|
878
|
-
const denoOrdered = pm === "deno" ? patterns.filter((pattern) => /[*?]/u.test(pattern) || pattern.startsWith("!")).map((raw) => {
|
|
879
|
-
const isExcluded = raw.startsWith("!");
|
|
880
|
-
const pattern = path.posix.normalize(raw.replace(/^!/u, "")).replace(/\/+$/u, "");
|
|
881
|
-
const glob = compile(pattern, isExcluded);
|
|
882
|
-
if (!isExcluded) return {
|
|
883
|
-
excluded: false,
|
|
884
|
-
glob
|
|
885
|
-
};
|
|
886
|
-
return {
|
|
887
|
-
excluded: true,
|
|
888
|
-
glob: {
|
|
889
|
-
...glob,
|
|
890
|
-
matches(directory) {
|
|
891
|
-
if (!/[*?]/u.test(pattern)) return directory === pattern;
|
|
892
|
-
if (!glob.matches(directory)) return false;
|
|
893
|
-
const applicable = denoPositive.filter((item) => item.scope.matches(directory)).map((item) => relatedBases(item.base, declaredBase(pattern)));
|
|
894
|
-
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.");
|
|
895
|
-
return applicable.some(Boolean);
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
}) : [];
|
|
900
|
-
const bunPatterns = pm === "bun" ? patterns.map((raw) => {
|
|
901
|
-
const isExcluded = raw.startsWith("!");
|
|
902
|
-
const pattern = path.posix.normalize(raw.replace(/^!/u, "")).replace(/\/+$/u, "");
|
|
903
|
-
const usesGlob = isExcluded || pattern.includes("*") || pattern.includes("?") || pattern.includes("{") || pattern.includes("[");
|
|
904
|
-
const glob = compileWorkspaceGlob(pattern, {
|
|
905
|
-
...defaultWorkspaceGlobOptions,
|
|
906
|
-
noext: true
|
|
907
|
-
});
|
|
908
|
-
const subtree = isExcluded && pattern.endsWith("/**") ? compileWorkspaceGlob(pattern.slice(0, -3), {
|
|
909
|
-
...defaultWorkspaceGlobOptions,
|
|
910
|
-
noext: true
|
|
911
|
-
}) : void 0;
|
|
912
|
-
return {
|
|
913
|
-
isExcluded,
|
|
914
|
-
usesGlob,
|
|
915
|
-
glob,
|
|
916
|
-
matches: (directory) => glob.matches(directory) || glob.matches(`${directory}/`) || subtree?.matches(directory) === true
|
|
917
|
-
};
|
|
918
|
-
}) : [];
|
|
919
|
-
const bunOrdered = bunPatterns.filter(({ usesGlob }) => usesGlob);
|
|
920
|
-
const bunLiterals = bunPatterns.filter(({ isExcluded, usesGlob }) => !isExcluded && !usesGlob);
|
|
921
|
-
const bunTraversal = bunPatterns.filter(({ isExcluded }) => !isExcluded).map(({ glob }) => glob);
|
|
922
|
-
const denoLiteral = pm === "deno" ? positive.filter((pattern) => !/[*?]/u.test(pattern)).map((pattern) => compile(pattern)) : [];
|
|
923
|
-
if (definition.denoManifests && denoOrdered.findLast(({ glob }) => glob.matches("."))?.excluded === false) throw new ConfigError("deno.json#workspace: a Deno workspace cannot contain itself.");
|
|
924
|
-
if (positive.length === 0 || positive.every((pattern) => pattern === ".")) return [];
|
|
925
|
-
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
926
|
-
const result = [];
|
|
927
|
-
const pending = ["."];
|
|
928
|
-
while (pending.length > 0) {
|
|
929
|
-
const current = pending.pop();
|
|
930
|
-
if (current === void 0) break;
|
|
931
|
-
for (const name of readDirectories(current)) {
|
|
932
|
-
if (name === ".git" || name === "node_modules") continue;
|
|
933
|
-
const directory = asRelPath(current === "." ? name : `${current}/${name}`);
|
|
934
|
-
if (pm === "bun" && name === "CMakeFiles" && !bunLiterals.some(({ glob }) => glob.matches(directory) || glob.canDescend(directory))) continue;
|
|
935
|
-
const inVendor = skipVendor && (directory === "vendor" || directory.startsWith("vendor/"));
|
|
936
|
-
if (inVendor && !denoLiteral.some((pattern) => pattern.matches(directory) || pattern.canDescend(directory))) continue;
|
|
937
|
-
const isExcluded = pm !== "deno" && excluded.some(({ glob }) => glob.matches(directory) || glob.matches(`${directory}/`));
|
|
938
|
-
if (excluded.some(({ subtree }) => subtree?.matches(directory))) continue;
|
|
939
|
-
const lastDenoMatch = denoOrdered.findLast(({ glob }) => glob.matches(directory));
|
|
940
|
-
const lastBunMatch = bunOrdered.findLast(({ matches }) => matches(directory));
|
|
941
|
-
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);
|
|
942
|
-
if ((pm === "bun" ? bunTraversal : inVendor ? denoLiteral : included).some((pattern) => pattern.canDescend(directory))) pending.push(directory);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
return result.sort();
|
|
946
|
-
};
|
|
947
|
-
//#endregion
|
|
948
|
-
//#region src/application/workspace-contexts.ts
|
|
949
|
-
/** Expand each manager's declarations into isolated publication contexts. */
|
|
950
|
-
const collectWorkspaceMembers = (ctx, fs, pms, projectType) => pms.flatMap((pm) => {
|
|
951
|
-
const seen = /* @__PURE__ */ new Set();
|
|
952
|
-
return workspaceDefinitions(ctx, pm).flatMap((definition) => workspaceDirectories(ctx, fs, definition, pm).flatMap((directory) => {
|
|
953
|
-
const root = resolveIn(ctx.root, directory);
|
|
954
|
-
const manifest = resolveIn(root, asRelPath("package.json"));
|
|
955
|
-
const denoManifest = resolveIn(root, asRelPath("deno.json"));
|
|
956
|
-
const denoJsonc = resolveIn(root, asRelPath("deno.jsonc"));
|
|
957
|
-
const allowed = new Set(pm === "deno" ? [
|
|
958
|
-
manifest,
|
|
959
|
-
denoManifest,
|
|
960
|
-
denoJsonc
|
|
961
|
-
] : [manifest]);
|
|
962
|
-
const memberFs = {
|
|
963
|
-
exists: (file) => allowed.has(file) && fs.exists(file),
|
|
964
|
-
readText: (file) => allowed.has(file) ? fs.readText(file) : void 0
|
|
965
|
-
};
|
|
966
|
-
try {
|
|
967
|
-
const explicitMember = definition.patterns.some((pattern) => !pattern.startsWith("!") && !/[*?]/u.test(pattern) && path.posix.normalize(pattern).replace(/\/+$/u, "") === directory);
|
|
968
|
-
if (!definition.denoManifests && !fs.exists(manifest)) {
|
|
969
|
-
if (pm === "deno" && explicitMember) throw new ConfigError("Declared npm workspace member has no package.json.");
|
|
970
|
-
return [];
|
|
971
|
-
}
|
|
972
|
-
if (pm === "deno" && !fs.exists(denoManifest) && fs.exists(denoJsonc)) throw new ConfigError("deno.jsonc is not supported; use strict deno.json.");
|
|
973
|
-
const memberCtx = createRepoContext(root, memberFs, projectType);
|
|
974
|
-
if (!definition.denoManifests && !memberCtx.packageJson) return [];
|
|
975
|
-
if (!memberCtx.packageJson && !memberCtx.exists(asRelPath("deno.json"))) {
|
|
976
|
-
if (definition.denoManifests && explicitMember) throw new ConfigError("Declared Deno member has no deno.json or package.json.");
|
|
977
|
-
return [];
|
|
978
|
-
}
|
|
979
|
-
if (pm === "deno") {
|
|
980
|
-
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.");
|
|
981
|
-
}
|
|
982
|
-
if (seen.has(directory)) return [];
|
|
983
|
-
seen.add(directory);
|
|
984
|
-
return [{
|
|
985
|
-
directory,
|
|
986
|
-
pm,
|
|
987
|
-
ctx: memberCtx
|
|
988
|
-
}];
|
|
989
|
-
} catch (error) {
|
|
990
|
-
if (error instanceof ConfigError) throw new ConfigError(`${directory}/${error.message}`);
|
|
991
|
-
throw error;
|
|
992
|
-
}
|
|
993
|
-
}));
|
|
994
|
-
});
|
|
995
|
-
//#endregion
|
|
996
|
-
//#region src/domain/setting-availability.ts
|
|
413
|
+
//#region src/core/rules/setting-availability.ts
|
|
997
414
|
const { npmrc: npmrc$7, packageJson: packageJson$2, pnpmWorkspace: pnpmWorkspace$11, yarnrc: yarnrc$10, bunfig: bunfig$6 } = CONFIG_FILES;
|
|
998
415
|
const settingAvailability = [
|
|
999
416
|
{
|
|
@@ -1114,7 +531,7 @@ const settingAvailability = [
|
|
|
1114
531
|
}
|
|
1115
532
|
];
|
|
1116
533
|
//#endregion
|
|
1117
|
-
//#region src/
|
|
534
|
+
//#region src/core/rules/remediation-availability.ts
|
|
1118
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]);
|
|
1119
536
|
/** Undefined means the target version or the setting's introduction is unknown. */
|
|
1120
537
|
const settingSupportedByTarget = (pm, version, target) => {
|
|
@@ -1133,7 +550,7 @@ const guardRemediationAvailability = (pm, version, remediation, targets = remedi
|
|
|
1133
550
|
};
|
|
1134
551
|
};
|
|
1135
552
|
//#endregion
|
|
1136
|
-
//#region src/
|
|
553
|
+
//#region src/core/rules/remediation.ts
|
|
1137
554
|
/** Scalar writes must not discard an existing container or replace a non-object parent. */
|
|
1138
555
|
const proposeChanges = (config, operations) => {
|
|
1139
556
|
for (const operation of operations) {
|
|
@@ -1157,7 +574,7 @@ const proposeChanges = (config, operations) => {
|
|
|
1157
574
|
};
|
|
1158
575
|
};
|
|
1159
576
|
//#endregion
|
|
1160
|
-
//#region src/
|
|
577
|
+
//#region src/core/rules/builders/require-config-key.ts
|
|
1161
578
|
const overrideBindings = (rule, overrides) => ({
|
|
1162
579
|
...rule,
|
|
1163
580
|
bindings: {
|
|
@@ -1168,7 +585,7 @@ const overrideBindings = (rule, overrides) => ({
|
|
|
1168
585
|
const accepts = (spec, actual) => spec.accept ? spec.accept(actual) : actual === spec.value;
|
|
1169
586
|
const checkKeyValue = (spec, config) => {
|
|
1170
587
|
const actual = getByPath(config, spec.keyPath);
|
|
1171
|
-
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);
|
|
1172
589
|
if (!coveredByDefault && accepts(spec, actual)) return { state: "ok" };
|
|
1173
590
|
const severity = coveredByDefault ? spec.defaultSatisfiedSeverity ?? "info" : void 0;
|
|
1174
591
|
if (severity === "off") return { state: "ok" };
|
|
@@ -1221,7 +638,7 @@ const requireConfigKey = (options) => {
|
|
|
1221
638
|
};
|
|
1222
639
|
};
|
|
1223
640
|
//#endregion
|
|
1224
|
-
//#region src/
|
|
641
|
+
//#region src/core/rules/builders/with-aube-paranoid.ts
|
|
1225
642
|
const withAubeParanoid = (rule) => {
|
|
1226
643
|
const { aube } = rule.bindings;
|
|
1227
644
|
if (!aube) return rule;
|
|
@@ -1234,13 +651,14 @@ const withAubeParanoid = (rule) => {
|
|
|
1234
651
|
} });
|
|
1235
652
|
};
|
|
1236
653
|
//#endregion
|
|
1237
|
-
//#region src/
|
|
654
|
+
//#region src/core/rules/advisory-check.ts
|
|
1238
655
|
const { aubeWorkspace: aubeWorkspace$8 } = CONFIG_FILES;
|
|
1239
656
|
const advisoryCheck = withAubeParanoid(requireConfigKey({
|
|
1240
657
|
bindings: { aube: {
|
|
1241
658
|
accept: (value) => value === "on" || value === "required",
|
|
1242
659
|
docs: "https://aube.jdx.dev/security.html",
|
|
1243
660
|
documentedDefault: "on",
|
|
661
|
+
defaultSafety: "unconditional",
|
|
1244
662
|
file: aubeWorkspace$8,
|
|
1245
663
|
keyPath: ["advisoryCheck"],
|
|
1246
664
|
message: "Set `advisoryCheck: on` (or `required`) in aube-workspace.yaml to check dependencies against known-malicious advisories.",
|
|
@@ -1253,11 +671,11 @@ const advisoryCheck = withAubeParanoid(requireConfigKey({
|
|
|
1253
671
|
title: "Enable malicious-package advisory checks"
|
|
1254
672
|
}));
|
|
1255
673
|
//#endregion
|
|
1256
|
-
//#region src/
|
|
674
|
+
//#region src/core/rules/config-predicates.ts
|
|
1257
675
|
const isNonBlankString = (value) => typeof value === "string" && value.trim().length > 0;
|
|
1258
676
|
const isStringList = (value) => Array.isArray(value) && Array.from(value).every(isNonBlankString);
|
|
1259
677
|
//#endregion
|
|
1260
|
-
//#region src/
|
|
678
|
+
//#region src/core/rules/approved-git-repos.ts
|
|
1261
679
|
const { yarnrc: yarnrc$9 } = CONFIG_FILES;
|
|
1262
680
|
const message = "Set `approvedGitRepositories: []` in .yarnrc.yml to block all git: protocol dependencies (or list approved repository globs).";
|
|
1263
681
|
const approvedGitRepos = defineRule({
|
|
@@ -1287,7 +705,7 @@ const approvedGitRepos = defineRule({
|
|
|
1287
705
|
title: "Approve git repository dependencies"
|
|
1288
706
|
});
|
|
1289
707
|
//#endregion
|
|
1290
|
-
//#region src/
|
|
708
|
+
//#region src/core/rules/audit-suppression.ts
|
|
1291
709
|
const { yarnrc: yarnrc$8 } = CONFIG_FILES;
|
|
1292
710
|
const isNonEmptyArray = (value) => Array.isArray(value) && value.length > 0;
|
|
1293
711
|
const SUPPRESSION_KEYS = ["npmAuditIgnoreAdvisories", "npmAuditExcludePackages"];
|
|
@@ -1320,7 +738,7 @@ const auditSuppression = defineRule({
|
|
|
1320
738
|
title: "Review audit suppression entries"
|
|
1321
739
|
});
|
|
1322
740
|
//#endregion
|
|
1323
|
-
//#region src/
|
|
741
|
+
//#region src/core/rules/block-auto-install.ts
|
|
1324
742
|
const { bunfig: bunfig$5 } = CONFIG_FILES;
|
|
1325
743
|
const blockAutoInstall = requireConfigKey({
|
|
1326
744
|
bindings: { bun: {
|
|
@@ -1338,11 +756,11 @@ const blockAutoInstall = requireConfigKey({
|
|
|
1338
756
|
title: "Block automatic package installation"
|
|
1339
757
|
});
|
|
1340
758
|
//#endregion
|
|
1341
|
-
//#region src/
|
|
759
|
+
//#region src/core/rules/block-exotic-subdeps.ts
|
|
1342
760
|
const { aubeWorkspace: aubeWorkspace$7, npmrc: npmrc$6, pnpmWorkspace: pnpmWorkspace$10 } = CONFIG_FILES;
|
|
1343
761
|
const isRootOrNone = (value) => value === "root" || value === "none";
|
|
1344
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.";
|
|
1345
|
-
const npmBinding$
|
|
763
|
+
const npmBinding$1 = {
|
|
1346
764
|
check(_ctx, config) {
|
|
1347
765
|
const git = getByPath(config, ["allow-git"]);
|
|
1348
766
|
const remote = getByPath(config, ["allow-remote"]);
|
|
@@ -1375,14 +793,16 @@ const blockExoticSubdeps = overrideBindings(requireConfigKey({
|
|
|
1375
793
|
aube: {
|
|
1376
794
|
docs: "https://aube.jdx.dev/security.html",
|
|
1377
795
|
documentedDefault: true,
|
|
796
|
+
defaultSafety: "unconditional",
|
|
1378
797
|
file: aubeWorkspace$7,
|
|
1379
798
|
keyPath: ["blockExoticSubdeps"],
|
|
1380
799
|
message: "Set `blockExoticSubdeps: true` in aube-workspace.yaml to block git/tarball URLs in transitive dependencies.",
|
|
1381
800
|
value: true
|
|
1382
801
|
},
|
|
1383
802
|
pnpm: {
|
|
1384
|
-
docs: "https://pnpm.io/settings#blockexoticsubdeps",
|
|
803
|
+
docs: "https://pnpm.io/settings/dependency-resolution#blockexoticsubdeps",
|
|
1385
804
|
documentedDefault: true,
|
|
805
|
+
defaultSafety: "conditional",
|
|
1386
806
|
file: pnpmWorkspace$10,
|
|
1387
807
|
keyPath: ["blockExoticSubdeps"],
|
|
1388
808
|
message: "Set `blockExoticSubdeps: true` in pnpm-workspace.yaml to block git/tarball URLs in transitive dependencies.",
|
|
@@ -1394,13 +814,13 @@ const blockExoticSubdeps = overrideBindings(requireConfigKey({
|
|
|
1394
814
|
}
|
|
1395
815
|
},
|
|
1396
816
|
description: "Refuse to install transitive dependencies sourced from git or tarball URLs, which bypass registry integrity checking.",
|
|
1397
|
-
docs: "https://pnpm.io/settings#blockexoticsubdeps",
|
|
817
|
+
docs: "https://pnpm.io/settings/dependency-resolution#blockexoticsubdeps",
|
|
1398
818
|
id: "block-exotic-subdeps",
|
|
1399
819
|
severity: "warn",
|
|
1400
820
|
title: "Block exotic subdependencies"
|
|
1401
|
-
}), { npm: npmBinding$
|
|
821
|
+
}), { npm: npmBinding$1 });
|
|
1402
822
|
//#endregion
|
|
1403
|
-
//#region src/
|
|
823
|
+
//#region src/core/rules/bun-security-scanner.ts
|
|
1404
824
|
const { bunfig: bunfig$4 } = CONFIG_FILES;
|
|
1405
825
|
const bunSecurityScanner = defineRule({
|
|
1406
826
|
bindings: { bun: {
|
|
@@ -1439,12 +859,13 @@ const bunSecurityScanner = defineRule({
|
|
|
1439
859
|
title: "Enable a bun install-time security scanner"
|
|
1440
860
|
});
|
|
1441
861
|
//#endregion
|
|
1442
|
-
//#region src/
|
|
862
|
+
//#region src/core/rules/checksum-verification.ts
|
|
1443
863
|
const { yarnrc: yarnrc$7 } = CONFIG_FILES;
|
|
1444
864
|
const checksumVerification = requireConfigKey({
|
|
1445
865
|
bindings: { yarn: {
|
|
1446
866
|
docs: "https://yarnpkg.com/configuration/yarnrc#checksumBehavior",
|
|
1447
867
|
documentedDefault: "throw",
|
|
868
|
+
defaultSafety: "unconditional",
|
|
1448
869
|
file: yarnrc$7,
|
|
1449
870
|
keyPath: ["checksumBehavior"],
|
|
1450
871
|
message: "Set `checksumBehavior: throw` in .yarnrc.yml to reject packages whose checksum does not match the lockfile.",
|
|
@@ -1457,7 +878,7 @@ const checksumVerification = requireConfigKey({
|
|
|
1457
878
|
title: "Verify package checksums"
|
|
1458
879
|
});
|
|
1459
880
|
//#endregion
|
|
1460
|
-
//#region src/
|
|
881
|
+
//#region src/core/signals.ts
|
|
1461
882
|
const PM_SIGNALS = {
|
|
1462
883
|
aube: {
|
|
1463
884
|
configs: [CONFIG_FILES.aubeWorkspace.path],
|
|
@@ -1492,11 +913,11 @@ const PM_SIGNALS = {
|
|
|
1492
913
|
}
|
|
1493
914
|
};
|
|
1494
915
|
//#endregion
|
|
1495
|
-
//#region src/
|
|
916
|
+
//#region src/core/rules/commit-lockfile.ts
|
|
1496
917
|
const LOCKFILE_DOCS = {
|
|
1497
918
|
aube: "https://github.com/aubepkg/aube/blob/main/docs/package-manager/lockfiles.md",
|
|
1498
919
|
bun: "https://bun.com/docs/install/lockfile",
|
|
1499
|
-
deno: "https://docs.deno.com/runtime/
|
|
920
|
+
deno: "https://docs.deno.com/runtime/reference/deno_json/#lockfile",
|
|
1500
921
|
npm: "https://docs.npmjs.com/cli/v12/configuring-npm/package-lock-json",
|
|
1501
922
|
pnpm: "https://pnpm.io/git#lockfiles",
|
|
1502
923
|
yarn: "https://yarnpkg.com/getting-started/qa#should-lockfiles-be-committed-to-the-repository"
|
|
@@ -1505,7 +926,7 @@ const LOCKFILE_VERSION_NOTES = { deno: { configAvailableSince: "deno 1.28.0" } }
|
|
|
1505
926
|
const lockfileBinding = (pm) => {
|
|
1506
927
|
const { lockfiles, reusesLockfiles } = PM_SIGNALS[pm];
|
|
1507
928
|
const [primary] = lockfiles;
|
|
1508
|
-
const accepted = [...lockfiles, ...reusesLockfiles ?? []]
|
|
929
|
+
const accepted = [...lockfiles, ...reusesLockfiles ?? []];
|
|
1509
930
|
return {
|
|
1510
931
|
check(ctx) {
|
|
1511
932
|
if (pm === "aube" && ctx.exists(asRelPath("bun.lockb")) && !ctx.exists(asRelPath("bun.lock"))) return {
|
|
@@ -1516,7 +937,20 @@ const lockfileBinding = (pm) => {
|
|
|
1516
937
|
steps: ["Run `bun install --save-text-lockfile` to generate bun.lock, then commit it."]
|
|
1517
938
|
}
|
|
1518
939
|
};
|
|
1519
|
-
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" };
|
|
1520
954
|
return {
|
|
1521
955
|
remediation: {
|
|
1522
956
|
kind: "manual",
|
|
@@ -1530,7 +964,7 @@ const lockfileBinding = (pm) => {
|
|
|
1530
964
|
versionNote: LOCKFILE_VERSION_NOTES[pm]
|
|
1531
965
|
};
|
|
1532
966
|
};
|
|
1533
|
-
const denoBinding$
|
|
967
|
+
const denoBinding$1 = {
|
|
1534
968
|
file: CONFIG_FILES.denoJson,
|
|
1535
969
|
docs: LOCKFILE_DOCS.deno,
|
|
1536
970
|
versionNote: LOCKFILE_VERSION_NOTES.deno,
|
|
@@ -1561,19 +995,19 @@ const commitLockfile = defineRule({
|
|
|
1561
995
|
bindings: {
|
|
1562
996
|
aube: lockfileBinding("aube"),
|
|
1563
997
|
bun: lockfileBinding("bun"),
|
|
1564
|
-
deno: denoBinding$
|
|
998
|
+
deno: denoBinding$1,
|
|
1565
999
|
npm: lockfileBinding("npm"),
|
|
1566
1000
|
pnpm: lockfileBinding("pnpm"),
|
|
1567
1001
|
yarn: lockfileBinding("yarn")
|
|
1568
1002
|
},
|
|
1569
1003
|
description: "Lockfiles pin the full dependency tree and integrity hashes, enabling reproducible, verifiable installs (e.g. `npm ci`).",
|
|
1570
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1004
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#3-include-lockfiles",
|
|
1571
1005
|
id: "commit-lockfile",
|
|
1572
1006
|
severity: "error",
|
|
1573
1007
|
title: "Commit a lockfile"
|
|
1574
1008
|
});
|
|
1575
1009
|
//#endregion
|
|
1576
|
-
//#region src/
|
|
1010
|
+
//#region src/core/rules/dependency-overrides.ts
|
|
1577
1011
|
const { aubeWorkspace: aubeWorkspace$6, pnpmWorkspace: pnpmWorkspace$9 } = CONFIG_FILES;
|
|
1578
1012
|
const isNonEmptyObject$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
1579
1013
|
const checkOverrides = (config, file) => {
|
|
@@ -1599,16 +1033,16 @@ const makeBinding = (file, fileName, docs) => ({
|
|
|
1599
1033
|
const dependencyOverrides = defineRule({
|
|
1600
1034
|
bindings: {
|
|
1601
1035
|
aube: makeBinding(aubeWorkspace$6, "aube-workspace.yaml", "https://aube.sh/settings/"),
|
|
1602
|
-
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")
|
|
1603
1037
|
},
|
|
1604
1038
|
description: "Flag dependency overrides that can replace transitive packages with arbitrary versions or forks — a supply-chain injection vector.",
|
|
1605
|
-
docs: "https://pnpm.io/settings#overrides",
|
|
1039
|
+
docs: "https://pnpm.io/settings/dependency-resolution#overrides",
|
|
1606
1040
|
id: "dependency-overrides",
|
|
1607
1041
|
severity: "info",
|
|
1608
1042
|
title: "Review dependency overrides"
|
|
1609
1043
|
});
|
|
1610
1044
|
//#endregion
|
|
1611
|
-
//#region src/
|
|
1045
|
+
//#region src/core/rules/disable-lifecycle-scripts.ts
|
|
1612
1046
|
const { npmrc: npmrc$5, pnpmWorkspace: pnpmWorkspace$8, yarnrc: yarnrc$6, aubeWorkspace: aubeWorkspace$5, bunfig: bunfig$3 } = CONFIG_FILES;
|
|
1613
1047
|
const pnpmBinding$4 = {
|
|
1614
1048
|
check(ctx, config) {
|
|
@@ -1682,7 +1116,7 @@ const pnpmBinding$4 = {
|
|
|
1682
1116
|
}])
|
|
1683
1117
|
};
|
|
1684
1118
|
},
|
|
1685
|
-
docs: "https://pnpm.io/settings#strictdepbuilds",
|
|
1119
|
+
docs: "https://pnpm.io/settings/build#strictdepbuilds",
|
|
1686
1120
|
file: pnpmWorkspace$8,
|
|
1687
1121
|
versionNote: {
|
|
1688
1122
|
configAvailableSince: "pnpm 10.3.0",
|
|
@@ -1769,6 +1203,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
|
|
|
1769
1203
|
yarn: {
|
|
1770
1204
|
docs: "https://yarnpkg.com/configuration/yarnrc#enableScripts",
|
|
1771
1205
|
documentedDefault: false,
|
|
1206
|
+
defaultSafety: "conditional",
|
|
1772
1207
|
file: yarnrc$6,
|
|
1773
1208
|
keyPath: ["enableScripts"],
|
|
1774
1209
|
message: "Set `enableScripts: false` in .yarnrc.yml to pin the policy across versions.",
|
|
@@ -1780,7 +1215,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
|
|
|
1780
1215
|
}
|
|
1781
1216
|
},
|
|
1782
1217
|
description: "Malicious postinstall scripts are a primary supply-chain attack vector. Prevent automatic execution of dependency lifecycle scripts.",
|
|
1783
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1218
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#1-disable-lifecycle-scripts",
|
|
1784
1219
|
id: "disable-lifecycle-scripts",
|
|
1785
1220
|
severity: "error",
|
|
1786
1221
|
title: "Disable dependency lifecycle scripts"
|
|
@@ -1790,7 +1225,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
|
|
|
1790
1225
|
pnpm: pnpmBinding$4
|
|
1791
1226
|
}));
|
|
1792
1227
|
//#endregion
|
|
1793
|
-
//#region src/
|
|
1228
|
+
//#region src/core/rules/enforce-strict-ssl.ts
|
|
1794
1229
|
const { npmrc: npmrc$4, yarnrc: yarnrc$5 } = CONFIG_FILES;
|
|
1795
1230
|
const yarnMessage = "Set `enableStrictSsl: true` in .yarnrc.yml to enforce SSL certificate validation for registry connections.";
|
|
1796
1231
|
const yarnBinding = {
|
|
@@ -1829,6 +1264,7 @@ const enforceStrictSsl = overrideBindings(requireConfigKey({
|
|
|
1829
1264
|
bindings: { npm: {
|
|
1830
1265
|
docs: "https://docs.npmjs.com/cli/v11/using-npm/config#strict-ssl",
|
|
1831
1266
|
documentedDefault: true,
|
|
1267
|
+
defaultSafety: "unconditional",
|
|
1832
1268
|
file: npmrc$4,
|
|
1833
1269
|
keyPath: ["strict-ssl"],
|
|
1834
1270
|
message: "Set `strict-ssl=true` in .npmrc to enforce SSL certificate validation.",
|
|
@@ -1841,7 +1277,7 @@ const enforceStrictSsl = overrideBindings(requireConfigKey({
|
|
|
1841
1277
|
title: "Enforce SSL for registry connections"
|
|
1842
1278
|
}), { yarn: yarnBinding });
|
|
1843
1279
|
//#endregion
|
|
1844
|
-
//#region src/
|
|
1280
|
+
//#region src/core/resolve-project-type.ts
|
|
1845
1281
|
const isPublishableName = (value) => typeof value === "string" && value.trim().length > 0;
|
|
1846
1282
|
const resolveProjectType = (selected, inferredPackage) => selected ?? (inferredPackage ? "package" : "application");
|
|
1847
1283
|
const resolvePackageJsonProjectType = (ctx) => {
|
|
@@ -1851,10 +1287,10 @@ const resolvePackageJsonProjectType = (ctx) => {
|
|
|
1851
1287
|
};
|
|
1852
1288
|
const resolveDenoProjectType = (ctx, denoConfig) => resolveProjectType(ctx.projectType, getByPath(denoConfig, ["publish"]) !== false && isPublishableName(getByPath(denoConfig, ["name"])));
|
|
1853
1289
|
//#endregion
|
|
1854
|
-
//#region src/
|
|
1290
|
+
//#region src/core/rules/publishable.ts
|
|
1855
1291
|
const isPublishable = (ctx) => resolvePackageJsonProjectType(ctx) === "package";
|
|
1856
1292
|
//#endregion
|
|
1857
|
-
//#region src/
|
|
1293
|
+
//#region src/core/rules/files-field.ts
|
|
1858
1294
|
const { packageJson: packageJson$1, denoJson: denoJson$3 } = CONFIG_FILES;
|
|
1859
1295
|
const packageJsonFilesBinding = {
|
|
1860
1296
|
check(ctx) {
|
|
@@ -1874,47 +1310,48 @@ const packageJsonFilesBinding = {
|
|
|
1874
1310
|
docs: "https://docs.npmjs.com/cli/v11/configuring-npm/package-json#files",
|
|
1875
1311
|
file: packageJson$1
|
|
1876
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
|
+
};
|
|
1877
1331
|
const filesField = defineRule({
|
|
1878
1332
|
bindings: {
|
|
1879
1333
|
aube: packageJsonFilesBinding,
|
|
1880
1334
|
bun: packageJsonFilesBinding,
|
|
1881
|
-
deno:
|
|
1882
|
-
check(ctx, config) {
|
|
1883
|
-
if (resolveDenoProjectType(ctx, config) !== "package") return { state: "na" };
|
|
1884
|
-
const include = getByPath(config, ["publish", "include"]);
|
|
1885
|
-
if (isStringList(include) && include.length > 0) return { state: "ok" };
|
|
1886
|
-
return {
|
|
1887
|
-
remediation: {
|
|
1888
|
-
kind: "manual",
|
|
1889
|
-
steps: ["Add a non-empty `publish.include` array to deno.json; use `publish.exclude` for additional exclusions."]
|
|
1890
|
-
},
|
|
1891
|
-
actual: include,
|
|
1892
|
-
message: "Add `publish.include` to deno.json to limit what gets published.",
|
|
1893
|
-
state: "violation"
|
|
1894
|
-
};
|
|
1895
|
-
},
|
|
1896
|
-
docs: "https://docs.deno.com/runtime/reference/cli/publish/#how-publishing-works",
|
|
1897
|
-
file: denoJson$3
|
|
1898
|
-
},
|
|
1335
|
+
deno: denoPublishBinding,
|
|
1899
1336
|
npm: packageJsonFilesBinding,
|
|
1900
1337
|
pnpm: packageJsonFilesBinding,
|
|
1901
1338
|
yarn: packageJsonFilesBinding
|
|
1902
1339
|
},
|
|
1903
1340
|
description: "An explicit `files` array in package.json restricts what gets published, preventing accidental inclusion of secrets or local files.",
|
|
1904
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1341
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#12-review-published-files",
|
|
1905
1342
|
id: "files-field",
|
|
1906
1343
|
projectTypes: ["package"],
|
|
1907
1344
|
severity: "info",
|
|
1908
1345
|
title: "Declare a published files allow-list"
|
|
1909
1346
|
});
|
|
1910
1347
|
//#endregion
|
|
1911
|
-
//#region src/
|
|
1348
|
+
//#region src/core/rules/frozen-lockfile.ts
|
|
1912
1349
|
const { pnpmWorkspace: pnpmWorkspace$7, yarnrc: yarnrc$4, bunfig: bunfig$2, denoJson: denoJson$2 } = CONFIG_FILES;
|
|
1913
1350
|
const aubeFrozenCommand = "Use `aube ci` or `aube install --frozen-lockfile` to fail when the lockfile would change.";
|
|
1914
1351
|
const frozenLockfile = overrideBindings(requireConfigKey({
|
|
1915
1352
|
bindings: {
|
|
1916
1353
|
deno: {
|
|
1917
|
-
docs: "https://docs.deno.com/runtime/
|
|
1354
|
+
docs: "https://docs.deno.com/runtime/reference/deno_json/#lockfile",
|
|
1918
1355
|
file: denoJson$2,
|
|
1919
1356
|
keyPath: ["lock", "frozen"],
|
|
1920
1357
|
message: "Set `lock.frozen: true` in deno.json for reproducible, verified installs.",
|
|
@@ -1929,7 +1366,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1929
1366
|
versionNote: { configAvailableSince: "bun 0.6.10" }
|
|
1930
1367
|
},
|
|
1931
1368
|
pnpm: {
|
|
1932
|
-
docs: "https://pnpm.io/
|
|
1369
|
+
docs: "https://pnpm.io/cli/install#--frozen-lockfile",
|
|
1933
1370
|
file: pnpmWorkspace$7,
|
|
1934
1371
|
keyPath: ["frozenLockfile"],
|
|
1935
1372
|
message: "pnpm defaults frozenLockfile to true in CI — set it explicitly in pnpm-workspace.yaml to pin the policy outside CI too.",
|
|
@@ -1938,6 +1375,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1938
1375
|
yarn: {
|
|
1939
1376
|
docs: "https://yarnpkg.com/configuration/yarnrc#enableImmutableInstalls",
|
|
1940
1377
|
documentedDefault: true,
|
|
1378
|
+
defaultSafety: "conditional",
|
|
1941
1379
|
file: yarnrc$4,
|
|
1942
1380
|
keyPath: ["enableImmutableInstalls"],
|
|
1943
1381
|
message: "Set `enableImmutableInstalls: true` in .yarnrc.yml to pin the policy outside CI too.",
|
|
@@ -1949,7 +1387,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1949
1387
|
}
|
|
1950
1388
|
},
|
|
1951
1389
|
description: "Refuse to mutate the lockfile on install so unexpected dependency changes fail loudly.",
|
|
1952
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1390
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#3-include-lockfiles",
|
|
1953
1391
|
id: "frozen-lockfile",
|
|
1954
1392
|
severity: "warn",
|
|
1955
1393
|
title: "Freeze the lockfile"
|
|
@@ -1968,7 +1406,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1968
1406
|
severity: "info"
|
|
1969
1407
|
} });
|
|
1970
1408
|
//#endregion
|
|
1971
|
-
//#region src/
|
|
1409
|
+
//#region src/core/rules/frozen-store.ts
|
|
1972
1410
|
const { pnpmWorkspace: pnpmWorkspace$6 } = CONFIG_FILES;
|
|
1973
1411
|
const frozenStore = defineRule({
|
|
1974
1412
|
bindings: { pnpm: {
|
|
@@ -1999,12 +1437,13 @@ const frozenStore = defineRule({
|
|
|
1999
1437
|
title: "Consider a pre-populated read-only store"
|
|
2000
1438
|
});
|
|
2001
1439
|
//#endregion
|
|
2002
|
-
//#region src/
|
|
1440
|
+
//#region src/core/rules/hardened-mode.ts
|
|
2003
1441
|
const { yarnrc: yarnrc$3 } = CONFIG_FILES;
|
|
2004
1442
|
const hardenedMode = requireConfigKey({
|
|
2005
1443
|
bindings: { yarn: {
|
|
2006
1444
|
docs: "https://yarnpkg.com/configuration/yarnrc#enableHardenedMode",
|
|
2007
1445
|
documentedDefault: true,
|
|
1446
|
+
defaultSafety: "conditional",
|
|
2008
1447
|
file: yarnrc$3,
|
|
2009
1448
|
keyPath: ["enableHardenedMode"],
|
|
2010
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.",
|
|
@@ -2021,19 +1460,9 @@ const hardenedMode = requireConfigKey({
|
|
|
2021
1460
|
title: "Enable hardened mode (yarn)"
|
|
2022
1461
|
});
|
|
2023
1462
|
//#endregion
|
|
2024
|
-
//#region src/
|
|
2025
|
-
const HOURS_PER_DAY = 24;
|
|
2026
|
-
const MINUTES_PER_HOUR = 60;
|
|
2027
|
-
const SECONDS_PER_MINUTE = 60;
|
|
2028
|
-
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
|
|
2029
|
-
const SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE;
|
|
2030
|
-
const RECOMMENDED_RELEASE_AGE_DAYS = 3;
|
|
2031
|
-
const RECOMMENDED_RELEASE_AGE_MINUTES = RECOMMENDED_RELEASE_AGE_DAYS * MINUTES_PER_DAY;
|
|
2032
|
-
const RECOMMENDED_RELEASE_AGE_SECONDS = RECOMMENDED_RELEASE_AGE_DAYS * SECONDS_PER_DAY;
|
|
2033
|
-
const DOCUMENTED_DEFAULT_MINUTES = MINUTES_PER_DAY;
|
|
2034
|
-
const { npmrc: npmrc$3, pnpmWorkspace: pnpmWorkspace$5, yarnrc: yarnrc$2, bunfig: bunfig$1, denoJson: denoJson$1, aubeWorkspace: aubeWorkspace$4 } = CONFIG_FILES;
|
|
1463
|
+
//#region src/core/rules/deno-release-age.ts
|
|
2035
1464
|
const DENO_DURATION = /^\+?P(?:\+?\d+[Ww]|(?:\d+[Dd])*(?:T(?:\d+[HhMm]|\d+(?:\.\d+)?[Ss])+)?)$/u;
|
|
2036
|
-
const DENO_MIN_TIMESTAMP = Date.
|
|
1465
|
+
const DENO_MIN_TIMESTAMP = Date.UTC(-262143, 0, 1);
|
|
2037
1466
|
const DENO_UNIT_SECONDS = {
|
|
2038
1467
|
w: 604800,
|
|
2039
1468
|
d: 86400,
|
|
@@ -2044,33 +1473,49 @@ const DENO_UNIT_SECONDS = {
|
|
|
2044
1473
|
const DENO_DATE = /^\d{4}-\d{2}-\d{2}$/u;
|
|
2045
1474
|
const DENO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}[Tt ](?:[01]\d|2[0-3]):[0-5]\d:(?:[0-5]\d|60)(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/u;
|
|
2046
1475
|
const DENO_OFFSET_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d(?::(?:[0-5]\d|60))?[+-](?:[01]\d|2[0-3]):?[0-5]\d$/u;
|
|
2047
|
-
const
|
|
2048
|
-
const
|
|
2049
|
-
const isPositiveDenoSeconds = (seconds) => seconds > 0 && Number.isFinite(seconds) &&
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
let seconds = 0;
|
|
2056
|
-
for (const [, integer, fraction, unit] of value.matchAll(/(\d+)(?:\.(\d+))?([WDHMS])/giu)) {
|
|
2057
|
-
const amount = Number(integer) + Number(`0.${(fraction ?? "").slice(0, 9) || "0"}`);
|
|
2058
|
-
seconds += amount * (DENO_UNIT_SECONDS[unit?.toLowerCase() ?? ""] ?? 0);
|
|
2059
|
-
}
|
|
2060
|
-
return isPositiveDenoSeconds(seconds);
|
|
1476
|
+
const DENO_DURATION_TOKEN = /(\d+)(?:\.(\d+))?([WDHMS])/giu;
|
|
1477
|
+
const LEAP_SECOND = /:60(?=\.|Z|z|[+-])/u;
|
|
1478
|
+
const isPositiveDenoSeconds = (seconds, now) => seconds > 0 && Number.isFinite(seconds) && now - seconds * 1e3 >= DENO_MIN_TIMESTAMP;
|
|
1479
|
+
const durationSeconds = (value) => {
|
|
1480
|
+
let seconds = 0;
|
|
1481
|
+
for (const [, integer, fraction, unit] of value.matchAll(DENO_DURATION_TOKEN)) {
|
|
1482
|
+
const amount = Number(integer) + Number(`0.${(fraction ?? "").slice(0, 9) || "0"}`);
|
|
1483
|
+
seconds += amount * (DENO_UNIT_SECONDS[unit?.toLowerCase() ?? ""] ?? 0);
|
|
2061
1484
|
}
|
|
1485
|
+
return seconds;
|
|
1486
|
+
};
|
|
1487
|
+
/** Match Deno's minimumDependencyAge value grammar and active-cutoff semantics. */
|
|
1488
|
+
const isActiveDenoReleaseAge = (value, now, parse) => {
|
|
1489
|
+
if (typeof value === "number") return Number.isSafeInteger(value) && isPositiveDenoSeconds(value * 60, now);
|
|
1490
|
+
if (typeof value !== "string") return false;
|
|
1491
|
+
if (/^\d+$/u.test(value)) return Number.isSafeInteger(Number(value)) && isPositiveDenoSeconds(Number(value) * 60, now);
|
|
1492
|
+
if (DENO_DURATION.test(value)) return isPositiveDenoSeconds(durationSeconds(value), now);
|
|
2062
1493
|
if (!DENO_DATE.test(value) && !DENO_TIMESTAMP.test(value) && !DENO_OFFSET_TIMESTAMP.test(value)) return false;
|
|
2063
1494
|
const date = value.slice(0, 10);
|
|
2064
1495
|
const midnight = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
|
|
2065
|
-
const leapSecond =
|
|
2066
|
-
const timestamp =
|
|
2067
|
-
return !Number.isNaN(midnight.valueOf()) && midnight.toISOString().slice(0, 10) === date && timestamp <
|
|
1496
|
+
const leapSecond = LEAP_SECOND.test(value);
|
|
1497
|
+
const timestamp = parse(value.replace(LEAP_SECOND, ":59")) + (leapSecond ? 1e3 : 0);
|
|
1498
|
+
return !Number.isNaN(midnight.valueOf()) && midnight.toISOString().slice(0, 10) === date && timestamp < now;
|
|
2068
1499
|
};
|
|
2069
|
-
|
|
2070
|
-
|
|
1500
|
+
//#endregion
|
|
1501
|
+
//#region src/core/rules/minimum-release-age.ts
|
|
1502
|
+
const HOURS_PER_DAY = 24;
|
|
1503
|
+
const MINUTES_PER_HOUR = 60;
|
|
1504
|
+
const SECONDS_PER_MINUTE = 60;
|
|
1505
|
+
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
|
|
1506
|
+
const SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE;
|
|
1507
|
+
const RECOMMENDED_RELEASE_AGE_DAYS = 3;
|
|
1508
|
+
const RECOMMENDED_RELEASE_AGE_MINUTES = RECOMMENDED_RELEASE_AGE_DAYS * MINUTES_PER_DAY;
|
|
1509
|
+
const RECOMMENDED_RELEASE_AGE_SECONDS = RECOMMENDED_RELEASE_AGE_DAYS * SECONDS_PER_DAY;
|
|
1510
|
+
const DOCUMENTED_DEFAULT_MINUTES = MINUTES_PER_DAY;
|
|
1511
|
+
const { npmrc: npmrc$3, pnpmWorkspace: pnpmWorkspace$5, yarnrc: yarnrc$2, bunfig: bunfig$1, denoJson: denoJson$1, aubeWorkspace: aubeWorkspace$4 } = CONFIG_FILES;
|
|
1512
|
+
const isPositiveNumber = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
1513
|
+
const isPositiveYarnDuration = (value) => isPositiveNumber(value) || typeof value === "string" && /^\d*\.?\d+(?:ms|s|m|h|d|w)?$/u.test(value) && Number.isFinite(Number.parseFloat(value)) && Number.parseFloat(value) > 0;
|
|
1514
|
+
const isNonDisabledDenoReleaseAge = (value, now, parse) => {
|
|
1515
|
+
if (!isPlainRecord(value)) return isActiveDenoReleaseAge(value, now, parse);
|
|
2071
1516
|
if (Object.keys(value).some((key) => key !== "age" && key !== "exclude")) return false;
|
|
2072
1517
|
if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
|
|
2073
|
-
return value.age
|
|
1518
|
+
return value.age != null && isActiveDenoReleaseAge(value.age, now, parse);
|
|
2074
1519
|
};
|
|
2075
1520
|
const denoAgeUsesFallback = (value) => {
|
|
2076
1521
|
if (value === void 0 || value === null) return true;
|
|
@@ -2079,13 +1524,14 @@ const denoAgeUsesFallback = (value) => {
|
|
|
2079
1524
|
if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
|
|
2080
1525
|
return value.age == null;
|
|
2081
1526
|
};
|
|
2082
|
-
const isPositiveDenoNpmrcDays = (value) => typeof value === "number" && Number.isSafeInteger(value) && value
|
|
2083
|
-
const
|
|
1527
|
+
const isPositiveDenoNpmrcDays = (value, now, parse) => typeof value === "number" && Number.isSafeInteger(value) && isActiveDenoReleaseAge(value * MINUTES_PER_DAY, now, parse);
|
|
1528
|
+
const baseRule$2 = requireConfigKey({
|
|
2084
1529
|
bindings: {
|
|
2085
1530
|
aube: {
|
|
2086
1531
|
accept: isPositiveNumber,
|
|
2087
1532
|
docs: "https://aube.sh/settings/",
|
|
2088
1533
|
documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
|
|
1534
|
+
defaultSafety: "unconditional",
|
|
2089
1535
|
file: aubeWorkspace$4,
|
|
2090
1536
|
keyPath: ["minimumReleaseAge"],
|
|
2091
1537
|
message: `Set minimumReleaseAge (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in aube-workspace.yaml.`,
|
|
@@ -2102,8 +1548,9 @@ const minimumReleaseAge = overrideBindings(requireConfigKey({
|
|
|
2102
1548
|
},
|
|
2103
1549
|
pnpm: {
|
|
2104
1550
|
accept: isPositiveNumber,
|
|
2105
|
-
docs: "https://pnpm.io/settings#minimumreleaseage",
|
|
1551
|
+
docs: "https://pnpm.io/settings/dependency-resolution#minimumreleaseage",
|
|
2106
1552
|
documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
|
|
1553
|
+
defaultSafety: "conditional",
|
|
2107
1554
|
file: pnpmWorkspace$5,
|
|
2108
1555
|
keyPath: ["minimumReleaseAge"],
|
|
2109
1556
|
message: `Set minimumReleaseAge (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in pnpm-workspace.yaml.`,
|
|
@@ -2117,6 +1564,7 @@ const minimumReleaseAge = overrideBindings(requireConfigKey({
|
|
|
2117
1564
|
accept: isPositiveYarnDuration,
|
|
2118
1565
|
docs: "https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate",
|
|
2119
1566
|
documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
|
|
1567
|
+
defaultSafety: "conditional",
|
|
2120
1568
|
file: yarnrc$2,
|
|
2121
1569
|
keyPath: ["npmMinimalAgeGate"],
|
|
2122
1570
|
message: `Set npmMinimalAgeGate (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in .yarnrc.yml.`,
|
|
@@ -2128,108 +1576,112 @@ const minimumReleaseAge = overrideBindings(requireConfigKey({
|
|
|
2128
1576
|
}
|
|
2129
1577
|
},
|
|
2130
1578
|
description: "Refuse to install releases newer than a cooldown window so freshly published (possibly compromised) versions are skipped.",
|
|
2131
|
-
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",
|
|
2132
1580
|
id: "minimum-release-age",
|
|
2133
1581
|
severity: "warn",
|
|
2134
1582
|
title: "Set a minimum release age"
|
|
2135
|
-
})
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
return {
|
|
2146
|
-
state: "violation",
|
|
2147
|
-
actual,
|
|
2148
|
-
expected: "a date in the past",
|
|
2149
|
-
message: "Use a valid past before cutoff, or remove before and set min-release-age.",
|
|
2150
|
-
remediation: {
|
|
2151
|
-
kind: "manual",
|
|
2152
|
-
steps: ["In .npmrc, set before to a valid past date. A future or disabled before overrides min-release-age in this file.", ...guardRemediationAvailability("npm", ctx.pmVersion, {
|
|
2153
|
-
kind: "manual",
|
|
2154
|
-
steps: [`Alternatively, remove before and set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days.`]
|
|
2155
|
-
}, [{
|
|
2156
|
-
file: npmrc$3,
|
|
2157
|
-
keyPath: ["min-release-age"]
|
|
2158
|
-
}])?.steps ?? []]
|
|
2159
|
-
}
|
|
2160
|
-
};
|
|
2161
|
-
}
|
|
2162
|
-
const actual = getByPath(config, ["min-release-age"]);
|
|
2163
|
-
const age = typeof actual === "number" || typeof actual === "string" ? Number(actual) : NaN;
|
|
2164
|
-
const cutoff = (/* @__PURE__ */ new Date(now - SECONDS_PER_DAY * 1e3 * age)).valueOf();
|
|
2165
|
-
if (age > 0 && Number.isFinite(cutoff) && cutoff < now) return { state: "ok" };
|
|
1583
|
+
});
|
|
1584
|
+
const createNpmBinding = (time) => ({
|
|
1585
|
+
file: npmrc$3,
|
|
1586
|
+
docs: "https://docs.npmjs.com/cli/v12/using-npm/config#min-release-age",
|
|
1587
|
+
versionNote: { note: "min-release-age available since npm 11.10.0" },
|
|
1588
|
+
check(ctx, config) {
|
|
1589
|
+
const now = time.now();
|
|
1590
|
+
if (Object.hasOwn(config, "before")) {
|
|
1591
|
+
const actual = config.before;
|
|
1592
|
+
if ((typeof actual === "string" || typeof actual === "number") && time.parse(String(actual)) < now) return { state: "ok" };
|
|
2166
1593
|
return {
|
|
2167
1594
|
state: "violation",
|
|
2168
1595
|
actual,
|
|
2169
|
-
expected:
|
|
2170
|
-
message:
|
|
2171
|
-
remediation:
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
1596
|
+
expected: "a date in the past",
|
|
1597
|
+
message: "Use a valid past before cutoff, or remove before and set min-release-age.",
|
|
1598
|
+
remediation: {
|
|
1599
|
+
kind: "manual",
|
|
1600
|
+
steps: ["In .npmrc, set before to a valid past date. A future or disabled before overrides min-release-age in this file.", ...guardRemediationAvailability("npm", ctx.pmVersion, {
|
|
1601
|
+
kind: "manual",
|
|
1602
|
+
steps: [`Alternatively, remove before and set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days.`]
|
|
1603
|
+
}, [{
|
|
1604
|
+
file: npmrc$3,
|
|
1605
|
+
keyPath: ["min-release-age"]
|
|
1606
|
+
}])?.steps ?? []]
|
|
1607
|
+
}
|
|
2180
1608
|
};
|
|
2181
1609
|
}
|
|
1610
|
+
const actual = getByPath(config, ["min-release-age"]);
|
|
1611
|
+
const age = typeof actual === "number" || typeof actual === "string" ? Number(actual) : NaN;
|
|
1612
|
+
const cutoff = (/* @__PURE__ */ new Date(now - SECONDS_PER_DAY * 1e3 * age)).valueOf();
|
|
1613
|
+
if (age > 0 && Number.isFinite(cutoff) && cutoff < now) return { state: "ok" };
|
|
1614
|
+
return {
|
|
1615
|
+
state: "violation",
|
|
1616
|
+
actual,
|
|
1617
|
+
expected: RECOMMENDED_RELEASE_AGE_DAYS,
|
|
1618
|
+
message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days to quarantine brand-new releases.`,
|
|
1619
|
+
remediation: guardRemediationAvailability("npm", ctx.pmVersion, proposeChanges(config, [{
|
|
1620
|
+
file: npmrc$3,
|
|
1621
|
+
op: "setKey",
|
|
1622
|
+
keyPath: ["min-release-age"],
|
|
1623
|
+
value: RECOMMENDED_RELEASE_AGE_DAYS
|
|
1624
|
+
}]), [{
|
|
1625
|
+
file: npmrc$3,
|
|
1626
|
+
keyPath: ["min-release-age"]
|
|
1627
|
+
}])
|
|
1628
|
+
};
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
const createDenoBinding = (time) => ({
|
|
1632
|
+
file: denoJson$1,
|
|
1633
|
+
docs: "https://docs.deno.com/runtime/reference/deno_json/",
|
|
1634
|
+
versionNote: {
|
|
1635
|
+
defaultSafeSince: "deno 2.9.0 (1440 minutes)",
|
|
1636
|
+
note: "object age may be omitted; project .npmrc fallback available since deno 2.8.1"
|
|
2182
1637
|
},
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
const actual = getByPath(config, ["minimumDependencyAge"]);
|
|
2192
|
-
if (denoAgeUsesFallback(actual)) {
|
|
2193
|
-
const npmrcConfig = ctx.readConfig(npmrc$3);
|
|
2194
|
-
const npmrcAge = getByPath(npmrcConfig, ["min-release-age"]);
|
|
2195
|
-
if (isPositiveDenoNpmrcDays(npmrcAge)) return { state: "ok" };
|
|
2196
|
-
if (npmrcAge === 0) return {
|
|
2197
|
-
state: "violation",
|
|
2198
|
-
actual: npmrcAge,
|
|
2199
|
-
expected: RECOMMENDED_RELEASE_AGE_DAYS,
|
|
2200
|
-
file: npmrc$3.path,
|
|
2201
|
-
message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days in .npmrc, or set minimumDependencyAge in deno.json.`,
|
|
2202
|
-
remediation: proposeChanges(npmrcConfig, [{
|
|
2203
|
-
file: npmrc$3,
|
|
2204
|
-
op: "setKey",
|
|
2205
|
-
keyPath: ["min-release-age"],
|
|
2206
|
-
value: RECOMMENDED_RELEASE_AGE_DAYS
|
|
2207
|
-
}])
|
|
2208
|
-
};
|
|
2209
|
-
}
|
|
2210
|
-
if (isNonDisabledDenoDuration(actual)) return { state: "ok" };
|
|
2211
|
-
const objectAge = isPlainRecord(actual);
|
|
2212
|
-
const invalidObject = objectAge && (Object.keys(actual).some((key) => key !== "age" && key !== "exclude") || actual.exclude !== void 0 && !isStringList(actual.exclude));
|
|
2213
|
-
return {
|
|
1638
|
+
check(ctx, config) {
|
|
1639
|
+
const actual = getByPath(config, ["minimumDependencyAge"]);
|
|
1640
|
+
const now = time.now();
|
|
1641
|
+
if (denoAgeUsesFallback(actual)) {
|
|
1642
|
+
const npmrcConfig = ctx.readConfig(npmrc$3);
|
|
1643
|
+
const npmrcAge = getByPath(npmrcConfig, ["min-release-age"]);
|
|
1644
|
+
if (isPositiveDenoNpmrcDays(npmrcAge, now, time.parse)) return { state: "ok" };
|
|
1645
|
+
if (npmrcAge === 0) return {
|
|
2214
1646
|
state: "violation",
|
|
2215
|
-
actual,
|
|
2216
|
-
expected:
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
} : proposeChanges(config, [{
|
|
2222
|
-
file: denoJson$1,
|
|
1647
|
+
actual: npmrcAge,
|
|
1648
|
+
expected: RECOMMENDED_RELEASE_AGE_DAYS,
|
|
1649
|
+
file: npmrc$3.path,
|
|
1650
|
+
message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days in .npmrc, or set minimumDependencyAge in deno.json.`,
|
|
1651
|
+
remediation: proposeChanges(npmrcConfig, [{
|
|
1652
|
+
file: npmrc$3,
|
|
2223
1653
|
op: "setKey",
|
|
2224
|
-
keyPath:
|
|
2225
|
-
value:
|
|
1654
|
+
keyPath: ["min-release-age"],
|
|
1655
|
+
value: RECOMMENDED_RELEASE_AGE_DAYS
|
|
2226
1656
|
}])
|
|
2227
1657
|
};
|
|
2228
1658
|
}
|
|
1659
|
+
if (isNonDisabledDenoReleaseAge(actual, now, time.parse)) return { state: "ok" };
|
|
1660
|
+
const objectAge = isPlainRecord(actual);
|
|
1661
|
+
const invalidObject = objectAge && (Object.keys(actual).some((key) => key !== "age" && key !== "exclude") || actual.exclude !== void 0 && !isStringList(actual.exclude));
|
|
1662
|
+
return {
|
|
1663
|
+
state: "violation",
|
|
1664
|
+
actual,
|
|
1665
|
+
expected: "P3D",
|
|
1666
|
+
message: `Set minimumDependencyAge (e.g. "P3D" for a ${RECOMMENDED_RELEASE_AGE_DAYS}-day cooldown) in deno.json.`,
|
|
1667
|
+
remediation: invalidObject ? {
|
|
1668
|
+
kind: "manual",
|
|
1669
|
+
steps: ["Use only age and exclude in minimumDependencyAge. Make exclude a list of package names and set age to a positive supported duration."]
|
|
1670
|
+
} : proposeChanges(config, [{
|
|
1671
|
+
file: denoJson$1,
|
|
1672
|
+
op: "setKey",
|
|
1673
|
+
keyPath: objectAge ? ["minimumDependencyAge", "age"] : ["minimumDependencyAge"],
|
|
1674
|
+
value: "P3D"
|
|
1675
|
+
}])
|
|
1676
|
+
};
|
|
2229
1677
|
}
|
|
2230
1678
|
});
|
|
1679
|
+
const createMinimumReleaseAge = (time) => overrideBindings(baseRule$2, {
|
|
1680
|
+
npm: createNpmBinding(time),
|
|
1681
|
+
deno: createDenoBinding(time)
|
|
1682
|
+
});
|
|
2231
1683
|
//#endregion
|
|
2232
|
-
//#region src/
|
|
1684
|
+
//#region src/core/rules/named-registries.ts
|
|
2233
1685
|
const { pnpmWorkspace: pnpmWorkspace$4 } = CONFIG_FILES;
|
|
2234
1686
|
const isNonEmptyObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
2235
1687
|
const namedRegistries = defineRule({
|
|
@@ -2247,17 +1699,17 @@ const namedRegistries = defineRule({
|
|
|
2247
1699
|
state: "violation"
|
|
2248
1700
|
};
|
|
2249
1701
|
},
|
|
2250
|
-
docs: "https://pnpm.io/settings#namedregistries",
|
|
1702
|
+
docs: "https://pnpm.io/settings/dependency-resolution#namedregistries",
|
|
2251
1703
|
file: pnpmWorkspace$4
|
|
2252
1704
|
} },
|
|
2253
1705
|
description: "Flag named registry mappings that redirect package resolution to custom registries, which may enable dependency confusion attacks.",
|
|
2254
|
-
docs: "https://pnpm.io/settings#namedregistries",
|
|
1706
|
+
docs: "https://pnpm.io/settings/dependency-resolution#namedregistries",
|
|
2255
1707
|
id: "named-registries",
|
|
2256
1708
|
severity: "info",
|
|
2257
1709
|
title: "Review named registry mappings"
|
|
2258
1710
|
});
|
|
2259
1711
|
//#endregion
|
|
2260
|
-
//#region src/
|
|
1712
|
+
//#region src/core/rules/paranoid-mode.ts
|
|
2261
1713
|
const { aubeWorkspace: aubeWorkspace$3 } = CONFIG_FILES;
|
|
2262
1714
|
const paranoidMode = requireConfigKey({
|
|
2263
1715
|
bindings: { aube: {
|
|
@@ -2274,7 +1726,7 @@ const paranoidMode = requireConfigKey({
|
|
|
2274
1726
|
title: "Enable paranoid security mode"
|
|
2275
1727
|
});
|
|
2276
1728
|
//#endregion
|
|
2277
|
-
//#region src/
|
|
1729
|
+
//#region src/core/rules/patched-dependencies.ts
|
|
2278
1730
|
const { pnpmWorkspace: pnpmWorkspace$3 } = CONFIG_FILES;
|
|
2279
1731
|
const isNonEmptyObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
2280
1732
|
const patchedDependencies = defineRule({
|
|
@@ -2292,27 +1744,33 @@ const patchedDependencies = defineRule({
|
|
|
2292
1744
|
state: "violation"
|
|
2293
1745
|
};
|
|
2294
1746
|
},
|
|
2295
|
-
docs: "https://pnpm.io/
|
|
1747
|
+
docs: "https://pnpm.io/cli/patch#patcheddependencies",
|
|
2296
1748
|
file: pnpmWorkspace$3
|
|
2297
1749
|
} },
|
|
2298
1750
|
description: "Review local patches separately from the registry artifacts they modify.",
|
|
2299
|
-
docs: "https://pnpm.io/
|
|
1751
|
+
docs: "https://pnpm.io/cli/patch#patcheddependencies",
|
|
2300
1752
|
id: "patched-dependencies",
|
|
2301
1753
|
severity: "info",
|
|
2302
1754
|
title: "Review patched dependencies"
|
|
2303
1755
|
});
|
|
2304
1756
|
//#endregion
|
|
2305
|
-
//#region src/
|
|
1757
|
+
//#region src/core/rules/pin-exact-versions.ts
|
|
2306
1758
|
const { npmrc: npmrc$2, pnpmWorkspace: pnpmWorkspace$2, yarnrc: yarnrc$1, bunfig, denoJson } = CONFIG_FILES;
|
|
2307
1759
|
const REGISTRY_VERSION = /^(?:npm|jsr):(?:@[^/@]+\/)?[^/@]+@(?<version>[^/]*)/u;
|
|
2308
1760
|
const isUnpinnedRegistryImport = (specifier) => {
|
|
2309
1761
|
if (!specifier.startsWith("npm:") && !specifier.startsWith("jsr:")) return false;
|
|
2310
1762
|
const version = REGISTRY_VERSION.exec(specifier)?.groups?.version;
|
|
2311
|
-
|
|
1763
|
+
const normalized = version?.startsWith("=") ? version.slice(1) : version;
|
|
1764
|
+
return normalized === void 0 || valid(normalized) === null;
|
|
2312
1765
|
};
|
|
2313
|
-
const collectUnpinnedImports = (imports) => {
|
|
1766
|
+
const collectUnpinnedImports = (imports, location) => {
|
|
1767
|
+
if (!isPlainRecord(imports)) throw new ConfigError(`deno.json: ${location} must be an import mapping object.`);
|
|
2314
1768
|
const offenders = [];
|
|
2315
|
-
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
|
+
}
|
|
2316
1774
|
return offenders;
|
|
2317
1775
|
};
|
|
2318
1776
|
const MAX_SAMPLE_COUNT = 3;
|
|
@@ -2323,23 +1781,26 @@ const formatOffenders = (offenders) => {
|
|
|
2323
1781
|
return {
|
|
2324
1782
|
remediation: {
|
|
2325
1783
|
kind: "manual",
|
|
2326
|
-
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."]
|
|
2327
1785
|
},
|
|
2328
1786
|
message: `${offenders.length} deno imports are not pinned: ${sample}${more}. Use \`deno add --save-exact\` or pin manually.`,
|
|
2329
1787
|
state: "violation"
|
|
2330
1788
|
};
|
|
2331
1789
|
};
|
|
2332
1790
|
const OK = { state: "ok" };
|
|
2333
|
-
const
|
|
1791
|
+
const unpinnedInlineImports = (config) => {
|
|
2334
1792
|
const imports = getByPath(config, ["imports"]);
|
|
2335
|
-
|
|
2336
|
-
|
|
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;
|
|
2337
1800
|
};
|
|
2338
1801
|
const denoBinding = {
|
|
2339
1802
|
check(_ctx, config) {
|
|
2340
|
-
const
|
|
2341
|
-
if (!imports) return OK;
|
|
2342
|
-
const offenders = collectUnpinnedImports(imports);
|
|
1803
|
+
const offenders = unpinnedInlineImports(config);
|
|
2343
1804
|
if (offenders.length === 0) return OK;
|
|
2344
1805
|
return formatOffenders(offenders);
|
|
2345
1806
|
},
|
|
@@ -2370,7 +1831,7 @@ const npmBinding = {
|
|
|
2370
1831
|
};
|
|
2371
1832
|
const aubeBinding = {
|
|
2372
1833
|
file: npmrc$2,
|
|
2373
|
-
docs: "https://aube.
|
|
1834
|
+
docs: "https://aube.sh/settings/#setting-saveprefix",
|
|
2374
1835
|
check(_ctx, config) {
|
|
2375
1836
|
const prefixes = ["save-prefix", "savePrefix"].filter((key) => Object.hasOwn(config, key)).map((key) => config[key]);
|
|
2376
1837
|
if (prefixes.length > 0 && prefixes.every((value) => value === "")) return OK;
|
|
@@ -2411,8 +1872,8 @@ const pinExactVersions = overrideBindings(requireConfigKey({
|
|
|
2411
1872
|
versionNote: { configAvailableSince: "yarn 2.0.0" }
|
|
2412
1873
|
}
|
|
2413
1874
|
},
|
|
2414
|
-
description: "Semver ranges (^, ~) auto-adopt new releases, including compromised ones. Save exact versions by default.",
|
|
2415
|
-
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",
|
|
2416
1877
|
id: "pin-exact-versions",
|
|
2417
1878
|
severity: "error",
|
|
2418
1879
|
title: "Pin exact dependency versions"
|
|
@@ -2422,7 +1883,7 @@ const pinExactVersions = overrideBindings(requireConfigKey({
|
|
|
2422
1883
|
npm: npmBinding
|
|
2423
1884
|
});
|
|
2424
1885
|
//#endregion
|
|
2425
|
-
//#region src/
|
|
1886
|
+
//#region src/core/rules/provenance.ts
|
|
2426
1887
|
const { npmrc: npmrc$1, yarnrc } = CONFIG_FILES;
|
|
2427
1888
|
const npmrcProvenance = {
|
|
2428
1889
|
file: npmrc$1,
|
|
@@ -2430,7 +1891,7 @@ const npmrcProvenance = {
|
|
|
2430
1891
|
message: "Set `provenance=true` (and publish from CI) to attest releases.",
|
|
2431
1892
|
value: true
|
|
2432
1893
|
};
|
|
2433
|
-
const
|
|
1894
|
+
const baseRule = requireConfigKey({
|
|
2434
1895
|
applies: isPublishable,
|
|
2435
1896
|
bindings: {
|
|
2436
1897
|
bun: {
|
|
@@ -2456,14 +1917,37 @@ const provenance = requireConfigKey({
|
|
|
2456
1917
|
}
|
|
2457
1918
|
},
|
|
2458
1919
|
description: "Provenance statements (via Sigstore) bind a published artifact to its recorded source and build.",
|
|
2459
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1920
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#11-generate-provenance-statements",
|
|
2460
1921
|
id: "provenance",
|
|
2461
1922
|
projectTypes: ["package"],
|
|
2462
1923
|
severity: "warn",
|
|
2463
1924
|
title: "Publish with provenance"
|
|
2464
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
|
+
} });
|
|
2465
1949
|
//#endregion
|
|
2466
|
-
//#region src/
|
|
1950
|
+
//#region src/core/rules/publish-access.ts
|
|
2467
1951
|
const { packageJson } = CONFIG_FILES;
|
|
2468
1952
|
const publishAccessBinding = {
|
|
2469
1953
|
check(ctx) {
|
|
@@ -2506,7 +1990,7 @@ const publishAccess = defineRule({
|
|
|
2506
1990
|
title: "Declare publish access explicitly"
|
|
2507
1991
|
});
|
|
2508
1992
|
//#endregion
|
|
2509
|
-
//#region src/
|
|
1993
|
+
//#region src/core/rules/store-server.ts
|
|
2510
1994
|
const { pnpmWorkspace: pnpmWorkspace$1 } = CONFIG_FILES;
|
|
2511
1995
|
const storeServer = defineRule({
|
|
2512
1996
|
bindings: { pnpm: {
|
|
@@ -2523,17 +2007,17 @@ const storeServer = defineRule({
|
|
|
2523
2007
|
state: "violation"
|
|
2524
2008
|
};
|
|
2525
2009
|
},
|
|
2526
|
-
docs: "https://pnpm.io/settings#
|
|
2010
|
+
docs: "https://pnpm.io/settings/store#userunningstoreserver",
|
|
2527
2011
|
file: pnpmWorkspace$1
|
|
2528
2012
|
} },
|
|
2529
2013
|
description: "Flag use of an external store server process, which introduces a trust boundary where tampered packages could be served.",
|
|
2530
|
-
docs: "https://pnpm.io/settings#
|
|
2014
|
+
docs: "https://pnpm.io/settings/store#userunningstoreserver",
|
|
2531
2015
|
id: "store-server",
|
|
2532
2016
|
severity: "info",
|
|
2533
2017
|
title: "Review store server usage"
|
|
2534
2018
|
});
|
|
2535
2019
|
//#endregion
|
|
2536
|
-
//#region src/
|
|
2020
|
+
//#region src/core/rules/strict-allow-scripts.ts
|
|
2537
2021
|
const { npmrc } = CONFIG_FILES;
|
|
2538
2022
|
const strictAllowScripts = defineRule({
|
|
2539
2023
|
bindings: { npm: {
|
|
@@ -2575,7 +2059,7 @@ const strictAllowScripts = defineRule({
|
|
|
2575
2059
|
title: "Require explicit script allow-listing"
|
|
2576
2060
|
});
|
|
2577
2061
|
//#endregion
|
|
2578
|
-
//#region src/
|
|
2062
|
+
//#region src/core/rules/strict-release-age.ts
|
|
2579
2063
|
const { aubeWorkspace: aubeWorkspace$2 } = CONFIG_FILES;
|
|
2580
2064
|
const strictReleaseAge = withAubeParanoid(requireConfigKey({
|
|
2581
2065
|
bindings: { aube: {
|
|
@@ -2592,7 +2076,7 @@ const strictReleaseAge = withAubeParanoid(requireConfigKey({
|
|
|
2592
2076
|
title: "Enforce strict release age gate"
|
|
2593
2077
|
}));
|
|
2594
2078
|
//#endregion
|
|
2595
|
-
//#region src/
|
|
2079
|
+
//#region src/core/rules/strict-store-integrity.ts
|
|
2596
2080
|
const { aubeWorkspace: aubeWorkspace$1 } = CONFIG_FILES;
|
|
2597
2081
|
const strictStoreIntegrity = defineRule({
|
|
2598
2082
|
bindings: { aube: {
|
|
@@ -2632,20 +2116,21 @@ const strictStoreIntegrity = defineRule({
|
|
|
2632
2116
|
title: "Require tarball integrity metadata"
|
|
2633
2117
|
});
|
|
2634
2118
|
//#endregion
|
|
2635
|
-
//#region src/
|
|
2119
|
+
//#region src/core/rules/trust-policy.ts
|
|
2636
2120
|
const { aubeWorkspace, pnpmWorkspace } = CONFIG_FILES;
|
|
2637
2121
|
const trustPolicy = withAubeParanoid(requireConfigKey({
|
|
2638
2122
|
bindings: {
|
|
2639
2123
|
aube: {
|
|
2640
2124
|
docs: "https://aube.jdx.dev/security.html",
|
|
2641
2125
|
documentedDefault: "no-downgrade",
|
|
2126
|
+
defaultSafety: "unconditional",
|
|
2642
2127
|
file: aubeWorkspace,
|
|
2643
2128
|
keyPath: ["trustPolicy"],
|
|
2644
2129
|
message: "Set `trustPolicy: no-downgrade` in aube-workspace.yaml to block packages whose trust level has decreased.",
|
|
2645
2130
|
value: "no-downgrade"
|
|
2646
2131
|
},
|
|
2647
2132
|
pnpm: {
|
|
2648
|
-
docs: "https://pnpm.io/settings#trustpolicy",
|
|
2133
|
+
docs: "https://pnpm.io/settings/dependency-resolution#trustpolicy",
|
|
2649
2134
|
file: pnpmWorkspace,
|
|
2650
2135
|
keyPath: ["trustPolicy"],
|
|
2651
2136
|
message: "Set `trustPolicy: no-downgrade` in pnpm-workspace.yaml to block packages whose trust level has decreased.",
|
|
@@ -2654,15 +2139,15 @@ const trustPolicy = withAubeParanoid(requireConfigKey({
|
|
|
2654
2139
|
}
|
|
2655
2140
|
},
|
|
2656
2141
|
description: "Fail installation when a package trust level has decreased compared to previous releases, catching publisher credential downgrades.",
|
|
2657
|
-
docs: "https://pnpm.io/settings#trustpolicy",
|
|
2142
|
+
docs: "https://pnpm.io/settings/dependency-resolution#trustpolicy",
|
|
2658
2143
|
id: "trust-policy",
|
|
2659
2144
|
severity: "warn",
|
|
2660
2145
|
title: "Enforce trust policy on dependency updates"
|
|
2661
2146
|
}));
|
|
2662
2147
|
//#endregion
|
|
2663
|
-
//#region src/
|
|
2664
|
-
const bindingFor = (pm) => {
|
|
2665
|
-
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));
|
|
2666
2151
|
return { check(ctx) {
|
|
2667
2152
|
const version = ctx.pmVersion;
|
|
2668
2153
|
if (version === void 0) return { state: "na" };
|
|
@@ -2685,9 +2170,24 @@ const bindingFor = (pm) => {
|
|
|
2685
2170
|
} : { state: "ok" };
|
|
2686
2171
|
} };
|
|
2687
2172
|
};
|
|
2173
|
+
const createUnsupportedSettings = (includesFile) => defineRule({
|
|
2174
|
+
id: "unsupported-settings",
|
|
2175
|
+
title: "Use settings available in the target PM version",
|
|
2176
|
+
description: "Report configured settings whose recorded introduction version is newer than the declared or explicit stable PM target. Reports each affected file separately, grouping its unsupported keys. Findings retain their evaluation directory. Only the coverage table below is checked. Unknown targets and unlisted settings are not evaluated for availability.",
|
|
2177
|
+
severity: "error",
|
|
2178
|
+
docs: "https://github.com/pHo9UBenaA/siro/blob/main/docs/rules.md#unsupported-settings--error",
|
|
2179
|
+
bindings: {
|
|
2180
|
+
npm: bindingFor("npm", includesFile),
|
|
2181
|
+
pnpm: bindingFor("pnpm", includesFile),
|
|
2182
|
+
yarn: bindingFor("yarn", includesFile),
|
|
2183
|
+
bun: bindingFor("bun", includesFile),
|
|
2184
|
+
deno: bindingFor("deno", includesFile)
|
|
2185
|
+
}
|
|
2186
|
+
});
|
|
2187
|
+
const unsupportedSettings = createUnsupportedSettings(() => true);
|
|
2688
2188
|
//#endregion
|
|
2689
|
-
//#region src/
|
|
2690
|
-
const
|
|
2189
|
+
//#region src/core/rules/builtin-rules.ts
|
|
2190
|
+
const createBuiltinRules = (time) => [
|
|
2691
2191
|
advisoryCheck,
|
|
2692
2192
|
approvedGitRepos,
|
|
2693
2193
|
auditSuppression,
|
|
@@ -2703,7 +2203,7 @@ const rules = [
|
|
|
2703
2203
|
frozenLockfile,
|
|
2704
2204
|
frozenStore,
|
|
2705
2205
|
hardenedMode,
|
|
2706
|
-
|
|
2206
|
+
createMinimumReleaseAge(time),
|
|
2707
2207
|
namedRegistries,
|
|
2708
2208
|
paranoidMode,
|
|
2709
2209
|
patchedDependencies,
|
|
@@ -2715,23 +2215,59 @@ const rules = [
|
|
|
2715
2215
|
strictReleaseAge,
|
|
2716
2216
|
strictStoreIntegrity,
|
|
2717
2217
|
trustPolicy,
|
|
2718
|
-
|
|
2719
|
-
id: "unsupported-settings",
|
|
2720
|
-
title: "Use settings available in the target PM version",
|
|
2721
|
-
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. Workspace findings retain their member directory. Only the coverage table below is checked. Unknown targets and unlisted settings are not evaluated for availability.",
|
|
2722
|
-
severity: "error",
|
|
2723
|
-
docs: "https://github.com/pHo9UBenaA/siro/blob/main/docs/rules.md#unsupported-settings--error",
|
|
2724
|
-
bindings: {
|
|
2725
|
-
npm: bindingFor("npm"),
|
|
2726
|
-
pnpm: bindingFor("pnpm"),
|
|
2727
|
-
yarn: bindingFor("yarn"),
|
|
2728
|
-
bun: bindingFor("bun"),
|
|
2729
|
-
deno: bindingFor("deno")
|
|
2730
|
-
}
|
|
2731
|
-
})
|
|
2218
|
+
unsupportedSettings
|
|
2732
2219
|
];
|
|
2220
|
+
const builtinScope = {
|
|
2221
|
+
"advisory-check": "installation",
|
|
2222
|
+
"approved-git-repos": "installation",
|
|
2223
|
+
"audit-suppression": "installation",
|
|
2224
|
+
"block-auto-install": "installation",
|
|
2225
|
+
"block-exotic-subdeps": "installation",
|
|
2226
|
+
"bun-security-scanner": "installation",
|
|
2227
|
+
"checksum-verification": "installation",
|
|
2228
|
+
"commit-lockfile": "installation",
|
|
2229
|
+
"dependency-overrides": "installation",
|
|
2230
|
+
"disable-lifecycle-scripts": "installation",
|
|
2231
|
+
"enforce-strict-ssl": "installation",
|
|
2232
|
+
"files-field": "manifest",
|
|
2233
|
+
"frozen-lockfile": "installation",
|
|
2234
|
+
"frozen-store": "installation",
|
|
2235
|
+
"hardened-mode": "installation",
|
|
2236
|
+
"minimum-release-age": "installation",
|
|
2237
|
+
"named-registries": "installation",
|
|
2238
|
+
"paranoid-mode": "installation",
|
|
2239
|
+
"patched-dependencies": "installation",
|
|
2240
|
+
"pin-exact-versions": "installation",
|
|
2241
|
+
provenance: "installation",
|
|
2242
|
+
"publish-access": "manifest",
|
|
2243
|
+
"store-server": "installation",
|
|
2244
|
+
"strict-allow-scripts": "installation",
|
|
2245
|
+
"strict-release-age": "installation",
|
|
2246
|
+
"strict-store-integrity": "installation",
|
|
2247
|
+
"trust-policy": "installation",
|
|
2248
|
+
"unsupported-settings": "split"
|
|
2249
|
+
};
|
|
2250
|
+
const scopeOf = (id) => Object.hasOwn(builtinScope, id) ? builtinScope[id] : "custom";
|
|
2251
|
+
//#endregion
|
|
2252
|
+
//#region src/adapters/exclusions.ts
|
|
2253
|
+
const compileExclusions = (patterns) => {
|
|
2254
|
+
try {
|
|
2255
|
+
const matchers = patterns.map((pattern) => new Minimatch(pattern.replace(/(?:\/\*\*)+$/u, "").replace(/[\\[\]]/gu, "\\$&"), {
|
|
2256
|
+
dot: true,
|
|
2257
|
+
nocase: false,
|
|
2258
|
+
nonegate: true,
|
|
2259
|
+
nocomment: true,
|
|
2260
|
+
nobrace: true,
|
|
2261
|
+
noext: true,
|
|
2262
|
+
platform: "linux"
|
|
2263
|
+
}));
|
|
2264
|
+
return (directory) => matchers.some((matcher) => matcher.match(directory));
|
|
2265
|
+
} catch (error) {
|
|
2266
|
+
throw new ConfigError(`exclude: ${error instanceof Error ? error.message : String(error)}`);
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2733
2269
|
//#endregion
|
|
2734
|
-
//#region src/
|
|
2270
|
+
//#region src/core/apply-config.ts
|
|
2735
2271
|
/** Select active rules and keep user severity choices as explicit runtime data. */
|
|
2736
2272
|
const applyConfig = (base, config) => {
|
|
2737
2273
|
const merged = [...base, ...config?.customRules ?? []];
|
|
@@ -2759,7 +2295,7 @@ const applyConfig = (base, config) => {
|
|
|
2759
2295
|
};
|
|
2760
2296
|
};
|
|
2761
2297
|
//#endregion
|
|
2762
|
-
//#region src/
|
|
2298
|
+
//#region src/core/detect-pms.ts
|
|
2763
2299
|
const registerSignals = (signals, pm, owner) => {
|
|
2764
2300
|
for (const file of signals) {
|
|
2765
2301
|
const prior = owner.get(file);
|
|
@@ -2791,24 +2327,25 @@ const detectPMs = (ctx) => {
|
|
|
2791
2327
|
return PMS.filter((pm) => found.has(pm));
|
|
2792
2328
|
};
|
|
2793
2329
|
//#endregion
|
|
2794
|
-
//#region src/
|
|
2330
|
+
//#region src/core/resolve-pms.ts
|
|
2795
2331
|
/** Apply the explicit selection or detection, then the configured allow-list. */
|
|
2796
2332
|
const resolvePMs = (ctx, opts) => {
|
|
2797
2333
|
const { pmOverride, allowed } = opts;
|
|
2798
2334
|
const detected = pmOverride ? [pmOverride] : detectPMs(ctx);
|
|
2799
2335
|
const pms = allowed ? detected.filter((pm) => allowed.includes(pm)) : detected;
|
|
2800
2336
|
if (pms.length > 0) return pms;
|
|
2337
|
+
if (opts.optional && detected.length === 0) return [];
|
|
2801
2338
|
if (!allowed) throw new UsageError(`No package manager detected. Pass --pm <${PMS.join("|")}> to be explicit.`);
|
|
2802
2339
|
if (pmOverride) throw new UsageError(`--pm ${pmOverride} is not in siro.config.ts pms (${allowed.join(", ")}). Adjust the config or the flag.`);
|
|
2803
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.`);
|
|
2804
2341
|
throw new UsageError(`No package manager detected, and siro.config.ts restricts pms to ${allowed.join(", ")}. Adjust the config or remove the restriction.`);
|
|
2805
2342
|
};
|
|
2806
2343
|
//#endregion
|
|
2807
|
-
//#region src/
|
|
2344
|
+
//#region src/core/decide-severity.ts
|
|
2808
2345
|
/** Resolve the most specific severity signal for one finding. */
|
|
2809
2346
|
const decideSeverity = (status, binding, rule, userOverride) => userOverride ?? status.severity ?? binding.severity ?? rule.severity;
|
|
2810
2347
|
//#endregion
|
|
2811
|
-
//#region src/
|
|
2348
|
+
//#region src/core/render-version-note.ts
|
|
2812
2349
|
const buildVersionNoteParts = (versionNote) => {
|
|
2813
2350
|
const parts = [];
|
|
2814
2351
|
if (typeof versionNote.configAvailableSince !== "undefined") parts.push(`available since ${versionNote.configAvailableSince}`);
|
|
@@ -2823,151 +2360,704 @@ const renderVersionNoteMessage = (message, versionNote) => {
|
|
|
2823
2360
|
return `${message} (${parts.join("; ")})`;
|
|
2824
2361
|
};
|
|
2825
2362
|
//#endregion
|
|
2826
|
-
//#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
|
|
2827
2388
|
const resolveBindingProjectType = (ctx, pm, parseConfig) => {
|
|
2828
2389
|
if (ctx.projectType !== void 0) return ctx.projectType;
|
|
2829
|
-
|
|
2830
|
-
return resolvePackageJsonProjectType(ctx);
|
|
2390
|
+
return pm === "deno" ? resolveDenoProjectType(ctx, parseConfig(CONFIG_FILES.denoJson)) : resolvePackageJsonProjectType(ctx);
|
|
2831
2391
|
};
|
|
2832
|
-
/**
|
|
2392
|
+
/** Select applicable PM bindings; response handling is shared with manifest checks. */
|
|
2833
2393
|
const runLint = (opts) => {
|
|
2834
|
-
const {
|
|
2394
|
+
const { repository, pms, ruleSet, severityOverrides } = opts;
|
|
2835
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
|
+
}
|
|
2836
2404
|
const summary = {
|
|
2837
2405
|
error: 0,
|
|
2838
2406
|
info: 0,
|
|
2839
2407
|
warn: 0
|
|
2840
2408
|
};
|
|
2841
|
-
const
|
|
2842
|
-
for (const rule of ruleSet) for (const pm of pms) {
|
|
2843
|
-
const binding = rule.bindings[pm];
|
|
2844
|
-
if (!binding || rule.projectTypes && !rule.projectTypes.includes(resolveBindingProjectType(ctx, pm, parseConfig))) continue;
|
|
2845
|
-
const ruleContext = {
|
|
2846
|
-
...ctx,
|
|
2847
|
-
readConfig: parseConfig,
|
|
2848
|
-
pmVersion: opts.pmVersions?.[pm]
|
|
2849
|
-
};
|
|
2850
|
-
const response = binding.check(ruleContext, parseConfig(binding.file));
|
|
2851
|
-
if (!isCheckStatusShape(response)) throw new ConfigError(`Rule '${rule.id}' returned an invalid check result.`);
|
|
2852
|
-
const statuses = response.state === "violations" ? response.violations : [response];
|
|
2853
|
-
for (const status of statuses) {
|
|
2854
|
-
if (status.state !== "violation") continue;
|
|
2855
|
-
const finding = {
|
|
2856
|
-
ruleId: rule.id,
|
|
2857
|
-
pm,
|
|
2858
|
-
severity: decideSeverity(status, binding, rule, severityOverrides?.get(rule.id)),
|
|
2859
|
-
message: renderVersionNoteMessage(status.message, binding.versionNote),
|
|
2860
|
-
file: status.file ?? binding.file?.path,
|
|
2861
|
-
docs: binding.docs ?? rule.docs,
|
|
2862
|
-
actual: status.actual,
|
|
2863
|
-
expected: status.expected,
|
|
2864
|
-
remediation: guardRemediationAvailability(pm, ruleContext.pmVersion, status.remediation)
|
|
2865
|
-
};
|
|
2866
|
-
findings.push(finding);
|
|
2867
|
-
summary[finding.severity] += 1;
|
|
2868
|
-
}
|
|
2869
|
-
}
|
|
2409
|
+
for (const finding of findings) summary[finding.severity] += 1;
|
|
2870
2410
|
return {
|
|
2871
2411
|
findings,
|
|
2872
2412
|
summary
|
|
2873
2413
|
};
|
|
2874
2414
|
};
|
|
2875
2415
|
//#endregion
|
|
2876
|
-
//#region src/
|
|
2877
|
-
/**
|
|
2878
|
-
const
|
|
2879
|
-
|
|
2416
|
+
//#region src/core/contracts/deno-json.ts
|
|
2417
|
+
/** Validate consumed publication metadata, not Deno's entire versioned schema. */
|
|
2418
|
+
const validateDenoMetadata = (config) => {
|
|
2419
|
+
const name = getByPath(config, ["name"]);
|
|
2420
|
+
if (name != null && typeof name !== "string") throw new ConfigError("deno.json: name must be a string or null.");
|
|
2421
|
+
const publish = getByPath(config, ["publish"]);
|
|
2422
|
+
if (publish == null || typeof publish === "boolean") return;
|
|
2423
|
+
if (!isPlainRecord(publish)) throw new ConfigError("deno.json: publish must be a boolean, object or null.");
|
|
2424
|
+
const include = getByPath(publish, ["include"]);
|
|
2425
|
+
if (include != null && (!Array.isArray(include) || !Array.from(include).every((item) => typeof item === "string"))) throw new ConfigError("deno.json: publish.include must be a string array or null.");
|
|
2426
|
+
};
|
|
2427
|
+
//#endregion
|
|
2428
|
+
//#region src/core/parse-config-file.ts
|
|
2429
|
+
const EMPTY_FILE = Object.freeze({});
|
|
2430
|
+
/** Create a lazy parser for one repository context; callers own its lifetime. */
|
|
2431
|
+
const createConfigParser = (codecFor, ctx) => {
|
|
2432
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2433
|
+
return (file) => {
|
|
2434
|
+
if (file === void 0) return EMPTY_FILE;
|
|
2435
|
+
const cacheKey = `${file.kind}:${file.path}`;
|
|
2436
|
+
const cached = cache.get(cacheKey);
|
|
2437
|
+
if (cached !== void 0) return cached;
|
|
2438
|
+
const text = ctx.readText(file.path);
|
|
2439
|
+
if (text === void 0) {
|
|
2440
|
+
cache.set(cacheKey, EMPTY_FILE);
|
|
2441
|
+
return EMPTY_FILE;
|
|
2442
|
+
}
|
|
2443
|
+
const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
|
|
2444
|
+
cache.set(cacheKey, parsed);
|
|
2445
|
+
return parsed;
|
|
2446
|
+
};
|
|
2447
|
+
};
|
|
2448
|
+
/** Pair the context with a fresh lazy parser; construction performs no reads. */
|
|
2449
|
+
const createRepositoryEvaluation = (ctx, codecFor) => ({
|
|
2450
|
+
ctx,
|
|
2451
|
+
parseConfig: createConfigParser(codecFor, ctx)
|
|
2452
|
+
});
|
|
2453
|
+
//#endregion
|
|
2454
|
+
//#region src/core/discovery.ts
|
|
2455
|
+
/** One context per selected directory, shared by publication and installation passes. */
|
|
2456
|
+
const discover = (cwd, fs, excluded, projectType, dependencies) => {
|
|
2457
|
+
if (typeof fs.readDirectories !== "function") throw new ConfigError("FileSystem.readDirectories is required for package discovery; no native filesystem fallback is used.");
|
|
2458
|
+
const { paths, createRepoContext, codecFor } = dependencies;
|
|
2459
|
+
const pending = [asRelPath(".")];
|
|
2460
|
+
const directories = [];
|
|
2461
|
+
while (pending.length) {
|
|
2462
|
+
const directory = pending.pop();
|
|
2463
|
+
const absolute = paths.resolve(cwd, directory);
|
|
2464
|
+
let repository;
|
|
2465
|
+
const manifests = [];
|
|
2466
|
+
try {
|
|
2467
|
+
repository = createRepositoryEvaluation(createRepoContext(absolute, fs, projectType), codecFor);
|
|
2468
|
+
const { ctx, parseConfig } = repository;
|
|
2469
|
+
if (ctx.packageJson !== void 0) manifests.push("package.json");
|
|
2470
|
+
if (ctx.readText(CONFIG_FILES.denoJson.path) !== void 0) {
|
|
2471
|
+
validateDenoMetadata(parseConfig(CONFIG_FILES.denoJson));
|
|
2472
|
+
manifests.push("deno.json");
|
|
2473
|
+
} else if (ctx.exists(asRelPath("deno.jsonc"))) throw new ConfigError("deno.jsonc is not supported; use strict deno.json.");
|
|
2474
|
+
} catch (error) {
|
|
2475
|
+
if (error instanceof ConfigError && directory !== ".") throw new ConfigError(`${directory}/${error.message}`);
|
|
2476
|
+
throw error;
|
|
2477
|
+
}
|
|
2478
|
+
directories.push({
|
|
2479
|
+
directory,
|
|
2480
|
+
repository,
|
|
2481
|
+
manifests
|
|
2482
|
+
});
|
|
2483
|
+
const names = fs.readDirectories(absolute);
|
|
2484
|
+
if (!Array.isArray(names)) throw new ConfigError(`${directory}: FileSystem.readDirectories must return a dense array.`);
|
|
2485
|
+
const children = /* @__PURE__ */ new Set();
|
|
2486
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
2487
|
+
if (!Object.hasOwn(names, i)) throw new ConfigError(`${directory}: FileSystem.readDirectories must return a dense array.`);
|
|
2488
|
+
const child = paths.child(directory, names[i]);
|
|
2489
|
+
if (names[i] === ".git" || names[i] === "node_modules" || excluded(child) || children.has(child)) continue;
|
|
2490
|
+
children.add(child);
|
|
2491
|
+
pending.push(child);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return directories.sort((a, b) => a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0);
|
|
2495
|
+
};
|
|
2496
|
+
//#endregion
|
|
2497
|
+
//#region src/core/manifest-checks.ts
|
|
2498
|
+
const manifestProjectType = (repository, file) => file === "deno.json" ? resolveDenoProjectType(repository.ctx, repository.parseConfig(CONFIG_FILES.denoJson)) : resolvePackageJsonProjectType(repository.ctx);
|
|
2499
|
+
const selectBinding = {
|
|
2500
|
+
"files-field": (file) => file === "deno.json" ? denoPublishBinding : packageJsonFilesBinding,
|
|
2501
|
+
"publish-access": (file, repository, targets) => {
|
|
2502
|
+
if (file !== "package.json") return void 0;
|
|
2503
|
+
return repository.ctx.packageJson?.publishConfig?.access === "private" && targets.length > 0 ? "pm" : publishAccessBinding;
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
const checkManifest = (repository, file, targets, rules, overrides) => {
|
|
2507
|
+
const findings = [];
|
|
2508
|
+
const projectType = manifestProjectType(repository, file);
|
|
2509
|
+
const pms = targets.map((target) => target.pm);
|
|
2510
|
+
const pmVersions = Object.fromEntries(targets.map((target) => [target.pm, target.version]));
|
|
2511
|
+
for (const rule of rules) {
|
|
2512
|
+
if (rule.projectTypes && !rule.projectTypes.includes(projectType)) continue;
|
|
2513
|
+
if (rule.id === "unsupported-settings") findings.push(...runLint({
|
|
2514
|
+
repository,
|
|
2515
|
+
pms,
|
|
2516
|
+
pmVersions,
|
|
2517
|
+
ruleSet: [{
|
|
2518
|
+
...rule,
|
|
2519
|
+
bindings: createUnsupportedSettings((path) => path === file).bindings
|
|
2520
|
+
}],
|
|
2521
|
+
severityOverrides: overrides
|
|
2522
|
+
}).findings);
|
|
2523
|
+
else if (Object.hasOwn(selectBinding, rule.id)) {
|
|
2524
|
+
const binding = selectBinding[rule.id](file, repository, targets);
|
|
2525
|
+
if (binding === "pm") findings.push(...runLint({
|
|
2526
|
+
repository,
|
|
2527
|
+
pms,
|
|
2528
|
+
pmVersions,
|
|
2529
|
+
ruleSet: [rule],
|
|
2530
|
+
severityOverrides: overrides
|
|
2531
|
+
}).findings);
|
|
2532
|
+
else if (binding) findings.push(...evaluateBinding(repository, rule, binding, overrides));
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
return findings;
|
|
2536
|
+
};
|
|
2537
|
+
//#endregion
|
|
2538
|
+
//#region src/core/rebase-finding.ts
|
|
2539
|
+
/** Context-local rule output becomes cwd-relative once, without mutating rule-owned data. */
|
|
2540
|
+
const rebaseFinding = (directory, finding) => {
|
|
2541
|
+
if (directory === ".") return {
|
|
2542
|
+
...finding,
|
|
2543
|
+
directory
|
|
2544
|
+
};
|
|
2545
|
+
const remediation = finding.remediation;
|
|
2546
|
+
const rebaseOperation = (operation) => ({
|
|
2547
|
+
...operation,
|
|
2548
|
+
file: {
|
|
2549
|
+
...operation.file,
|
|
2550
|
+
path: `${directory}/${operation.file.path}`
|
|
2551
|
+
}
|
|
2552
|
+
});
|
|
2553
|
+
return {
|
|
2554
|
+
...finding,
|
|
2555
|
+
directory,
|
|
2556
|
+
...finding.file === void 0 ? {} : { file: `${directory}/${finding.file}` },
|
|
2557
|
+
remediation: remediation?.kind === "automatic" ? {
|
|
2558
|
+
...remediation,
|
|
2559
|
+
operations: [rebaseOperation(remediation.operations[0]), ...remediation.operations.slice(1).map(rebaseOperation)]
|
|
2560
|
+
} : remediation?.kind === "manual" ? {
|
|
2561
|
+
...remediation,
|
|
2562
|
+
steps: [`Work in ${directory} for this finding.`, ...remediation.steps]
|
|
2563
|
+
} : remediation
|
|
2564
|
+
};
|
|
2565
|
+
};
|
|
2566
|
+
//#endregion
|
|
2567
|
+
//#region src/core/lint.ts
|
|
2568
|
+
const validateLintOptions = (options, paths) => {
|
|
2569
|
+
if (!options || !paths.isAbsolute(options.cwd)) throw new UsageError("cwd must be an absolute filesystem path.");
|
|
2570
|
+
if ("workspaces" in options) throw new UsageError("workspaces was removed in 0.6.0. Package discovery is recursive by default; use exclude and installationRoots to specify scope.");
|
|
2880
2571
|
if ("customRules" in options || "reporters" in options) throw new UsageError("Pass customRules and reporters inside the config option.");
|
|
2881
2572
|
if (options.pm !== void 0 && !isPM(options.pm)) throw new UsageError(`Unknown package manager: ${String(options.pm)}`);
|
|
2882
2573
|
if (options.projectType !== void 0 && !isProjectType(options.projectType)) throw new UsageError(`Unknown project type: ${String(options.projectType)}`);
|
|
2883
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.");
|
|
2884
|
-
|
|
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);
|
|
2885
2579
|
const config = options.config === void 0 ? void 0 : parseConfig(options.config);
|
|
2886
|
-
const
|
|
2887
|
-
const
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
const
|
|
2893
|
-
const pmVersions = {
|
|
2894
|
-
...declaredPMVersion(ctx.packageJson?.packageManager),
|
|
2895
|
-
...config?.pmVersions,
|
|
2896
|
-
...options.pm && options.pmVersion ? { [options.pm]: options.pmVersion } : {}
|
|
2897
|
-
};
|
|
2898
|
-
const fs = options.fs ?? nodeFileSystem;
|
|
2580
|
+
const fs = options.fs === void 0 ? dependencies.fileSystem : options.fs;
|
|
2581
|
+
const excluded = dependencies.compileExclusions(parseExcludes(options.exclude !== void 0 ? options.exclude : config?.exclude ?? []));
|
|
2582
|
+
const installations = parseInstallationRoots(options.installationRoots !== void 0 ? options.installationRoots : config?.installationRoots ?? ["."]);
|
|
2583
|
+
const configured = applyConfig(dependencies.rules, config);
|
|
2584
|
+
const needsCustomTarget = configured.rules.some((rule) => scopeOf(rule.id) === "custom");
|
|
2585
|
+
const discovered = discover(options.cwd, fs, excluded, options.projectType ?? config?.projectType, dependencies);
|
|
2586
|
+
for (const entry of installations) if (!discovered.some((item) => item.directory === entry.path)) throw new UsageError(`${entry.path}: installation root must be an existing, selected ordinary directory using its exact enumerated spelling (no symlinks, excluded or hard-skipped paths).`);
|
|
2899
2587
|
return {
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2588
|
+
evaluation: {
|
|
2589
|
+
directories: discovered.map((item) => {
|
|
2590
|
+
const { ctx } = item.repository;
|
|
2591
|
+
const entry = installations.find((root) => root.path === item.directory);
|
|
2592
|
+
let pms;
|
|
2593
|
+
let versions = declaredPMVersion(ctx.packageJson?.packageManager);
|
|
2594
|
+
if (item.directory === ".") {
|
|
2595
|
+
pms = resolvePMs(ctx, {
|
|
2596
|
+
allowed: config?.pms,
|
|
2597
|
+
pmOverride: options.pm,
|
|
2598
|
+
optional: !entry && !needsCustomTarget
|
|
2599
|
+
});
|
|
2600
|
+
versions = {
|
|
2601
|
+
...versions,
|
|
2602
|
+
...config?.pmVersions,
|
|
2603
|
+
...options.pm && options.pmVersion ? { [options.pm]: options.pmVersion } : {}
|
|
2604
|
+
};
|
|
2605
|
+
} else if (entry) {
|
|
2606
|
+
try {
|
|
2607
|
+
pms = resolvePMs(ctx, { pmOverride: entry.pm });
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
if (error instanceof UsageError) throw new UsageError(`${item.directory}: ${error.message}`);
|
|
2610
|
+
throw error;
|
|
2611
|
+
}
|
|
2612
|
+
if (entry.pm && entry.pmVersion) versions = {
|
|
2613
|
+
...versions,
|
|
2614
|
+
[entry.pm]: entry.pmVersion
|
|
2615
|
+
};
|
|
2616
|
+
} else {
|
|
2617
|
+
const declared = ctx.packageJson?.packageManager === void 0 ? void 0 : parsePackageManagerField(ctx.packageJson.packageManager);
|
|
2618
|
+
pms = PMS.filter((pm) => pm === declared || pm === "deno" && item.manifests.includes("deno.json"));
|
|
2619
|
+
}
|
|
2620
|
+
return {
|
|
2621
|
+
...item,
|
|
2622
|
+
installation: entry !== void 0,
|
|
2623
|
+
targets: pms.map((pm) => ({
|
|
2624
|
+
pm,
|
|
2625
|
+
...versions[pm] === void 0 ? {} : { version: versions[pm] }
|
|
2626
|
+
}))
|
|
2627
|
+
};
|
|
2628
|
+
}),
|
|
2629
|
+
ruleSet: configured.rules,
|
|
2630
|
+
severityOverrides: configured.severityOverrides
|
|
2631
|
+
},
|
|
2907
2632
|
reporters: config?.reporters ?? []
|
|
2908
2633
|
};
|
|
2909
2634
|
};
|
|
2910
|
-
|
|
2911
|
-
const
|
|
2912
|
-
const
|
|
2913
|
-
const
|
|
2914
|
-
const
|
|
2915
|
-
const
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
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
|
|
2926
2656
|
});
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
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 [];
|
|
2935
2665
|
});
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2666
|
+
try {
|
|
2667
|
+
local.push(...runLint({
|
|
2668
|
+
repository,
|
|
2669
|
+
pms: targets.map(({ pm }) => pm),
|
|
2670
|
+
pmVersions: Object.fromEntries(targets.map(({ pm, version }) => [pm, version])),
|
|
2671
|
+
ruleSet: scopedRules,
|
|
2672
|
+
severityOverrides
|
|
2673
|
+
}).findings);
|
|
2674
|
+
} catch (error) {
|
|
2675
|
+
if (error instanceof ConfigError && directory !== ".") throw new ConfigError(`${directory}/${error.message}`);
|
|
2676
|
+
throw error;
|
|
2677
|
+
}
|
|
2678
|
+
local.sort((a, b) => (ruleOrder.get(a.ruleId) ?? 0) - (ruleOrder.get(b.ruleId) ?? 0));
|
|
2679
|
+
findings.push(...local.map((finding) => rebaseFinding(directory, finding)));
|
|
2680
|
+
}
|
|
2681
|
+
const summary = {
|
|
2682
|
+
error: 0,
|
|
2683
|
+
warn: 0,
|
|
2684
|
+
info: 0
|
|
2685
|
+
};
|
|
2686
|
+
for (const finding of findings) summary[finding.severity] += 1;
|
|
2687
|
+
return {
|
|
2688
|
+
findings,
|
|
2689
|
+
summary,
|
|
2690
|
+
inspection: {
|
|
2691
|
+
manifests,
|
|
2692
|
+
installationRoots
|
|
2693
|
+
}
|
|
2694
|
+
};
|
|
2695
|
+
};
|
|
2696
|
+
const lint$1 = (options, dependencies) => runPreparedLint(prepareLint(options, dependencies).evaluation);
|
|
2697
|
+
//#endregion
|
|
2698
|
+
//#region src/core/filter.ts
|
|
2699
|
+
const EXIT_SUCCESS = 0;
|
|
2700
|
+
const EXIT_FAILURE = 1;
|
|
2701
|
+
const meetsThreshold = (severity, threshold) => SEVERITY_RANK[severity] >= SEVERITY_RANK[threshold];
|
|
2702
|
+
/** Keep only findings at or above `threshold`, recomputing the summary. */
|
|
2703
|
+
const filterBySeverity = (result, threshold) => {
|
|
2704
|
+
const findings = [];
|
|
2705
|
+
const summary = {
|
|
2706
|
+
error: 0,
|
|
2707
|
+
info: 0,
|
|
2708
|
+
warn: 0
|
|
2709
|
+
};
|
|
2710
|
+
for (const finding of result.findings) if (meetsThreshold(finding.severity, threshold)) {
|
|
2711
|
+
findings.push(finding);
|
|
2712
|
+
summary[finding.severity] += 1;
|
|
2941
2713
|
}
|
|
2942
2714
|
return {
|
|
2715
|
+
...result,
|
|
2943
2716
|
findings,
|
|
2944
2717
|
summary
|
|
2945
2718
|
};
|
|
2946
2719
|
};
|
|
2947
|
-
|
|
2720
|
+
/**
|
|
2721
|
+
* Exit code for a lint run. Non-zero when any finding meets `threshold`
|
|
2722
|
+
* (default: only `error` fails the run).
|
|
2723
|
+
*/
|
|
2724
|
+
const exitCodeForLint = (result, threshold = "error") => {
|
|
2725
|
+
if (result.findings.some((finding) => meetsThreshold(finding.severity, threshold))) return EXIT_FAILURE;
|
|
2726
|
+
return EXIT_SUCCESS;
|
|
2727
|
+
};
|
|
2948
2728
|
//#endregion
|
|
2949
|
-
//#region src/
|
|
2729
|
+
//#region src/core/lint-command.ts
|
|
2950
2730
|
/** Evaluate and report. Executable config loading belongs to the CLI adapter. */
|
|
2951
|
-
const lintCommand = async (options, io) => {
|
|
2731
|
+
const lintCommand$1 = async (options, io, dependencies, reporters) => {
|
|
2952
2732
|
if (!options) throw new UsageError("Lint options are required.");
|
|
2953
2733
|
if (options.severity !== void 0 && !isSeverity(options.severity)) throw new UsageError(`Invalid severity: ${String(options.severity)}`);
|
|
2954
|
-
const prepared = prepareLint(options);
|
|
2955
|
-
const registry = createRegistry(prepared.reporters);
|
|
2956
|
-
const selection = options.reporter ??
|
|
2734
|
+
const prepared = prepareLint(options, dependencies);
|
|
2735
|
+
const registry = reporters.createRegistry(prepared.reporters);
|
|
2736
|
+
const selection = options.reporter ?? reporters.defaultName;
|
|
2957
2737
|
const reporter = typeof selection === "string" ? registry.get(selection) : selection;
|
|
2958
2738
|
if (!isReporterShape(reporter)) throw new UsageError(`${typeof selection === "string" ? "Unknown" : "Invalid"} reporter: ${String(selection)} (available: ${[...registry.keys()].join(", ")})`);
|
|
2959
|
-
const result = runPreparedLint(prepared);
|
|
2739
|
+
const result = runPreparedLint(prepared.evaluation);
|
|
2960
2740
|
const exitCode = exitCodeForLint(result, options.severity ?? "error");
|
|
2961
|
-
|
|
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;
|
|
2962
2769
|
return exitCode;
|
|
2963
2770
|
};
|
|
2964
2771
|
//#endregion
|
|
2772
|
+
//#region src/core/contracts/package-json.ts
|
|
2773
|
+
const PackageJsonSchema = vb.looseObject({
|
|
2774
|
+
files: vb.optional(vb.array(vb.string())),
|
|
2775
|
+
name: vb.optional(vb.string()),
|
|
2776
|
+
packageManager: vb.optional(vb.string()),
|
|
2777
|
+
private: vb.optional(vb.boolean()),
|
|
2778
|
+
publishConfig: vb.optional(vb.looseObject({
|
|
2779
|
+
access: vb.optional(vb.picklist([
|
|
2780
|
+
"public",
|
|
2781
|
+
"restricted",
|
|
2782
|
+
"private"
|
|
2783
|
+
])),
|
|
2784
|
+
provenance: vb.optional(vb.boolean())
|
|
2785
|
+
})),
|
|
2786
|
+
trustedDependencies: vb.optional(vb.array(vb.string()))
|
|
2787
|
+
});
|
|
2788
|
+
const parsePackageJson = (value) => {
|
|
2789
|
+
if (!isPlainRecord(value)) throw new ConfigError("package.json: expected an object at the root.");
|
|
2790
|
+
const result = vb.safeParse(PackageJsonSchema, value);
|
|
2791
|
+
if (result.success) return result.output;
|
|
2792
|
+
const issue = result.issues[0];
|
|
2793
|
+
const field = vb.getDotPath(issue);
|
|
2794
|
+
throw new ConfigError(`package.json: ${field ? `${field}: ` : ""}${issue.message}`);
|
|
2795
|
+
};
|
|
2796
|
+
//#endregion
|
|
2797
|
+
//#region src/adapters/repo-context.ts
|
|
2798
|
+
const tryParseJson = (text) => {
|
|
2799
|
+
try {
|
|
2800
|
+
return JSON.parse(text);
|
|
2801
|
+
} catch (error) {
|
|
2802
|
+
let errMsg = String(error);
|
|
2803
|
+
if (error instanceof Error) errMsg = error.message;
|
|
2804
|
+
throw new ConfigError(`package.json: invalid JSON — ${errMsg}`);
|
|
2805
|
+
}
|
|
2806
|
+
};
|
|
2807
|
+
const readPackageJson = (raw) => {
|
|
2808
|
+
return parsePackageJson(tryParseJson(raw.trim()));
|
|
2809
|
+
};
|
|
2810
|
+
const createRepoContext = (root, fs = nodeFileSystem, projectType) => {
|
|
2811
|
+
if (fs === nodeFileSystem) assertDirectory(root);
|
|
2812
|
+
const packageFile = resolveIn(root, asRelPath("package.json"));
|
|
2813
|
+
const raw = fs.readText(packageFile);
|
|
2814
|
+
const texts = new Map([[packageFile, raw]]);
|
|
2815
|
+
const readText = (relPath) => {
|
|
2816
|
+
const file = resolveIn(root, relPath);
|
|
2817
|
+
if (!texts.has(file)) texts.set(file, fs.readText(file));
|
|
2818
|
+
return texts.get(file);
|
|
2819
|
+
};
|
|
2820
|
+
const exists = (relPath) => fs.exists(resolveIn(root, relPath));
|
|
2821
|
+
let packageJson = void 0;
|
|
2822
|
+
if (typeof raw !== "undefined") packageJson = readPackageJson(raw);
|
|
2823
|
+
return {
|
|
2824
|
+
exists,
|
|
2825
|
+
packageJson,
|
|
2826
|
+
projectType,
|
|
2827
|
+
readText,
|
|
2828
|
+
root
|
|
2829
|
+
};
|
|
2830
|
+
};
|
|
2831
|
+
//#endregion
|
|
2832
|
+
//#region src/adapters/codecs/ini.ts
|
|
2833
|
+
const INTEGER_PATTERN = /^-?\d+$/u;
|
|
2834
|
+
const coerceBoolean = (str) => {
|
|
2835
|
+
if (str === "true") return true;
|
|
2836
|
+
if (str === "false") return false;
|
|
2837
|
+
};
|
|
2838
|
+
const coerceInteger = (str) => {
|
|
2839
|
+
if (INTEGER_PATTERN.test(str)) return Number(str);
|
|
2840
|
+
};
|
|
2841
|
+
const coerce = (raw) => {
|
|
2842
|
+
if (typeof raw !== "string") return raw;
|
|
2843
|
+
return coerceBoolean(raw) ?? coerceInteger(raw) ?? raw;
|
|
2844
|
+
};
|
|
2845
|
+
//#endregion
|
|
2846
|
+
//#region src/adapters/codecs/store.ts
|
|
2847
|
+
const CODECS = {
|
|
2848
|
+
json: { parse(text) {
|
|
2849
|
+
const trimmed = text.trim();
|
|
2850
|
+
return toParsedConfig(JSON.parse(trimmed));
|
|
2851
|
+
} },
|
|
2852
|
+
npmrc: { parse(text) {
|
|
2853
|
+
const raw = toParsedConfig(ini.parse(text));
|
|
2854
|
+
const config = {};
|
|
2855
|
+
for (const [key, value] of Object.entries(raw)) if (Array.isArray(value)) config[key] = value.map((elem) => coerce(elem));
|
|
2856
|
+
else if (typeof value === "object" && value !== null) config[key] = toParsedConfig(value);
|
|
2857
|
+
else config[key] = coerce(value);
|
|
2858
|
+
return config;
|
|
2859
|
+
} },
|
|
2860
|
+
toml: { parse(text) {
|
|
2861
|
+
if (text.trim() === "") return {};
|
|
2862
|
+
return toParsedConfig(parse$1(text.startsWith("") ? text.slice(1) : text));
|
|
2863
|
+
} },
|
|
2864
|
+
yaml: { parse(text) {
|
|
2865
|
+
if (text.trim() === "") return {};
|
|
2866
|
+
const document = parseDocument(text);
|
|
2867
|
+
if (document.errors.length > 0) throw document.errors[0];
|
|
2868
|
+
if (document.contents === null) return {};
|
|
2869
|
+
return toParsedConfig(document.toJS({ onAnchor: (value, count) => {
|
|
2870
|
+
if (count > 1) JSON.stringify(value);
|
|
2871
|
+
} }));
|
|
2872
|
+
} }
|
|
2873
|
+
};
|
|
2874
|
+
/** Look up the codec for a parseable kind. Total — every CodecKind has one. */
|
|
2875
|
+
const codecFor = (kind) => CODECS[kind];
|
|
2876
|
+
//#endregion
|
|
2877
|
+
//#region src/adapters/reporters/github.ts
|
|
2878
|
+
const COMMAND = {
|
|
2879
|
+
error: "error",
|
|
2880
|
+
info: "notice",
|
|
2881
|
+
warn: "warning"
|
|
2882
|
+
};
|
|
2883
|
+
const escapeData = (raw) => raw.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
2884
|
+
const escapeProp = (raw) => escapeData(raw).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
2885
|
+
/** Emit GitHub Actions workflow commands (annotations on PRs). */
|
|
2886
|
+
const githubReporter = {
|
|
2887
|
+
async format(result, io, context) {
|
|
2888
|
+
for (const finding of result.findings) {
|
|
2889
|
+
let file = "";
|
|
2890
|
+
if (finding.file) file = `file=${escapeProp(path.resolve(context.cwd, finding.file))},`;
|
|
2891
|
+
let body = `[${finding.pm ?? "package"}] ${finding.directory}: ${finding.message}`;
|
|
2892
|
+
if (finding.docs) body += ` (${finding.docs})`;
|
|
2893
|
+
await io.stdout(`::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`);
|
|
2894
|
+
}
|
|
2895
|
+
},
|
|
2896
|
+
name: "github"
|
|
2897
|
+
};
|
|
2898
|
+
//#endregion
|
|
2899
|
+
//#region src/adapters/safe-text.ts
|
|
2900
|
+
/** Visible escaping for untrusted human-facing text; never rewrite API/native paths. */
|
|
2901
|
+
const safeText = (text) => text.replace(/[\u0000-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`).replaceAll("##[", "#\\u0023[").replace(/^(\s*)::/u, "$1\\u003a:");
|
|
2902
|
+
/** JSON syntax and decoded values stay identical; CI must not parse legacy commands in it. */
|
|
2903
|
+
const safeJsonText = (json) => json.replace(/[\u007f-\u009f\u061c\u200e\u200f\u2028-\u202e\u2066-\u2069]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`).replaceAll("##[", "\\u0023\\u0023[");
|
|
2904
|
+
//#endregion
|
|
2905
|
+
//#region src/adapters/reporters/json.ts
|
|
2906
|
+
/**
|
|
2907
|
+
* Versioned machine-readable output — the public contract consumed by
|
|
2908
|
+
* external fixers (see docs/json-output.md). Bump `schemaVersion` on any
|
|
2909
|
+
* breaking shape change and update that doc in the same commit.
|
|
2910
|
+
*/
|
|
2911
|
+
const SCHEMA_VERSION = 3;
|
|
2912
|
+
const jsonReporter = {
|
|
2913
|
+
async format(result, io) {
|
|
2914
|
+
await io.stdout(safeJsonText(JSON.stringify({
|
|
2915
|
+
schemaVersion: SCHEMA_VERSION,
|
|
2916
|
+
siroVersion: version,
|
|
2917
|
+
findings: result.findings,
|
|
2918
|
+
summary: result.summary,
|
|
2919
|
+
inspection: result.inspection
|
|
2920
|
+
}, void 0, 2)));
|
|
2921
|
+
},
|
|
2922
|
+
name: "json"
|
|
2923
|
+
};
|
|
2924
|
+
//#endregion
|
|
2925
|
+
//#region src/adapters/reporters/pretty.ts
|
|
2926
|
+
const GLYPH = {
|
|
2927
|
+
error: "✖ error",
|
|
2928
|
+
info: "ℹ info",
|
|
2929
|
+
warn: "⚠ warn"
|
|
2930
|
+
};
|
|
2931
|
+
/**
|
|
2932
|
+
* Decide colour at call time, not at import time.
|
|
2933
|
+
*
|
|
2934
|
+
* picocolors snapshots `isColorSupported` when imported, so a later
|
|
2935
|
+
* `process.env.NO_COLOR = '1'` (or a different stdout) is ignored. The
|
|
2936
|
+
* reporter is invoked per `siro lint` run from many contexts (TTY, CI
|
|
2937
|
+
* with FORCE_COLOR, redirected output, …) — checking each call is the
|
|
2938
|
+
* only way NO_COLOR can truly win, as https://no-color.org/ requires.
|
|
2939
|
+
*/
|
|
2940
|
+
const colorSupportedNow = () => {
|
|
2941
|
+
const { env } = process;
|
|
2942
|
+
if (typeof env.NO_COLOR !== "undefined" && env.NO_COLOR !== "") return false;
|
|
2943
|
+
if ("FORCE_COLOR" in env) return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "";
|
|
2944
|
+
return pc.isColorSupported;
|
|
2945
|
+
};
|
|
2946
|
+
const renderFinding = (finding, ctx) => {
|
|
2947
|
+
const where = ctx.colors.dim(` (${safeText(finding.file ?? finding.directory)})`);
|
|
2948
|
+
ctx.io.stdout(`${ctx.tag[finding.severity](GLYPH[finding.severity])} [${finding.pm ?? "package"}] ${ctx.colors.bold(safeText(finding.ruleId))}${where}`);
|
|
2949
|
+
ctx.io.stdout(` ${safeText(finding.message)}`);
|
|
2950
|
+
for (const step of finding.remediation?.kind === "manual" ? finding.remediation.steps : []) ctx.io.stdout(` ↳ ${safeText(step)}`);
|
|
2951
|
+
if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${safeText(finding.docs)}`));
|
|
2952
|
+
};
|
|
2953
|
+
const buildRenderCtx = (io) => {
|
|
2954
|
+
const colors = pc.createColors(colorSupportedNow());
|
|
2955
|
+
return {
|
|
2956
|
+
colors,
|
|
2957
|
+
io,
|
|
2958
|
+
tag: {
|
|
2959
|
+
error: colors.red,
|
|
2960
|
+
info: colors.cyan,
|
|
2961
|
+
warn: colors.yellow
|
|
2962
|
+
}
|
|
2963
|
+
};
|
|
2964
|
+
};
|
|
2965
|
+
const prettyReporter = {
|
|
2966
|
+
async format(result, io) {
|
|
2967
|
+
const lines = [];
|
|
2968
|
+
const collect = (line) => {
|
|
2969
|
+
lines.push(line);
|
|
2970
|
+
};
|
|
2971
|
+
const ctx = buildRenderCtx({
|
|
2972
|
+
stdout: collect,
|
|
2973
|
+
stderr: collect
|
|
2974
|
+
});
|
|
2975
|
+
collect(`Inspection: ${result.inspection.manifests.length} manifests; installation roots: ${result.inspection.installationRoots.map((root) => safeText(root.directory)).join(", ") || "none"}.`);
|
|
2976
|
+
collect("Unknown PM/version targets have no availability assessment; installation scope is explicit.");
|
|
2977
|
+
if (result.findings.length === 0) collect(ctx.colors.green("✔ No security best-practice issues found."));
|
|
2978
|
+
else {
|
|
2979
|
+
for (const finding of result.findings) renderFinding(finding, ctx);
|
|
2980
|
+
const { error, warn, info } = result.summary;
|
|
2981
|
+
collect("");
|
|
2982
|
+
collect(`Summary: ${error} error, ${warn} warn, ${info} info`);
|
|
2983
|
+
}
|
|
2984
|
+
await io.stdout(lines.join("\n"));
|
|
2985
|
+
},
|
|
2986
|
+
name: "pretty"
|
|
2987
|
+
};
|
|
2988
|
+
//#endregion
|
|
2989
|
+
//#region src/adapters/reporters/registry.ts
|
|
2990
|
+
const BUILTINS = [
|
|
2991
|
+
prettyReporter,
|
|
2992
|
+
jsonReporter,
|
|
2993
|
+
githubReporter
|
|
2994
|
+
];
|
|
2995
|
+
const DEFAULT_REPORTER_NAME = prettyReporter.name;
|
|
2996
|
+
const JSON_REPORTER_NAME = jsonReporter.name;
|
|
2997
|
+
/** Names of every built-in reporter, in stable display order. */
|
|
2998
|
+
const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
|
|
2999
|
+
/** Later registrations replace earlier reporters with the same name. */
|
|
3000
|
+
const createRegistry = (extras = []) => new Map([...BUILTINS, ...extras].map((reporter) => [reporter.name, reporter]));
|
|
3001
|
+
const dependencies = {
|
|
3002
|
+
rules: createBuiltinRules({
|
|
3003
|
+
now: () => Date.now(),
|
|
3004
|
+
parse: (value) => Date.parse(value)
|
|
3005
|
+
}),
|
|
3006
|
+
fileSystem: nodeFileSystem,
|
|
3007
|
+
paths: nodePaths,
|
|
3008
|
+
createRepoContext,
|
|
3009
|
+
codecFor,
|
|
3010
|
+
compileExclusions
|
|
3011
|
+
};
|
|
3012
|
+
/** Public Node API: callers can replace the filesystem without assembling the application. */
|
|
3013
|
+
const lint = (options) => lint$1(options, dependencies);
|
|
3014
|
+
const lintCommand = (options, io) => lintCommand$1(options, io, dependencies, {
|
|
3015
|
+
defaultName: DEFAULT_REPORTER_NAME,
|
|
3016
|
+
createRegistry
|
|
3017
|
+
});
|
|
3018
|
+
//#endregion
|
|
2965
3019
|
//#region src/adapters/node-io.ts
|
|
2966
|
-
const
|
|
2967
|
-
|
|
2968
|
-
|
|
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
|
+
};
|
|
2969
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);
|
|
2970
3060
|
//#endregion
|
|
2971
|
-
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 };
|
|
2972
3062
|
|
|
2973
|
-
//# sourceMappingURL=node-io-
|
|
3063
|
+
//# sourceMappingURL=node-io-CAszdmko.mjs.map
|