@pho9ubenaa/siro 0.5.1 → 0.6.1
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 +60 -91
- package/dist/cli.mjs +64 -24
- package/dist/index.d.mts +106 -54
- package/dist/index.mjs +3 -3
- package/dist/{node-io-BDHg4J2w.mjs → node-io-BpAHyeBG.mjs} +1131 -829
- package/package.json +1 -1
|
@@ -1,16 +1,17 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, opendirSync, readSync, realpathSync, statSync } from "node:fs";
|
|
1
2
|
import path from "node:path";
|
|
2
|
-
import { lstatSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import satisfies from "semver/functions/satisfies.js";
|
|
5
5
|
import * as vb from "valibot";
|
|
6
6
|
import { lt, parse } from "semver";
|
|
7
7
|
import valid from "semver/functions/valid.js";
|
|
8
|
-
import {
|
|
8
|
+
import { Minimatch } from "minimatch";
|
|
9
9
|
import ini from "ini";
|
|
10
10
|
import { parse as parse$1 } from "smol-toml";
|
|
11
11
|
import { parseDocument } from "yaml";
|
|
12
|
+
import { types } from "node:util";
|
|
12
13
|
import pc from "picocolors";
|
|
13
|
-
//#region src/
|
|
14
|
+
//#region src/core/contracts/errors.ts
|
|
14
15
|
var SiroError = class extends Error {
|
|
15
16
|
exitCode;
|
|
16
17
|
constructor(message, exitCode) {
|
|
@@ -31,6 +32,8 @@ var UsageError = class extends SiroError {
|
|
|
31
32
|
this.name = "UsageError";
|
|
32
33
|
}
|
|
33
34
|
};
|
|
35
|
+
/** Parser diagnostics constructed from constant classifications, never source excerpts. */
|
|
36
|
+
var ConfigParseError = class extends Error {};
|
|
34
37
|
/**
|
|
35
38
|
* Run `fn` and wrap any non-`ConfigError` failure as a `ConfigError` prefixed
|
|
36
39
|
* with `filePath`. A `ConfigError` that bubbles up from a nested call is re-thrown unchanged so
|
|
@@ -41,13 +44,188 @@ const wrapCodecError = (filePath, fn) => {
|
|
|
41
44
|
return fn();
|
|
42
45
|
} catch (error) {
|
|
43
46
|
if (error instanceof ConfigError) throw error;
|
|
44
|
-
|
|
45
|
-
if (error instanceof Error) msg = error.message;
|
|
46
|
-
throw new ConfigError(`${filePath}: ${msg}`);
|
|
47
|
+
throw new ConfigError(`${filePath}: ${error instanceof ConfigParseError ? error.message : "Invalid configuration."}`);
|
|
47
48
|
}
|
|
48
49
|
};
|
|
49
50
|
//#endregion
|
|
50
|
-
//#region src/
|
|
51
|
+
//#region src/core/contracts/records.ts
|
|
52
|
+
/** Configuration maps are ordinary objects or null-prototype dictionaries. */
|
|
53
|
+
const isPlainRecord = (value) => {
|
|
54
|
+
if (typeof value !== "object" || value === null) return false;
|
|
55
|
+
const prototype = Object.getPrototypeOf(value);
|
|
56
|
+
return prototype === Object.prototype || prototype === null;
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
//#region src/core/contracts/scan-limits.ts
|
|
60
|
+
const DEFAULT_SCAN_LIMITS = Object.freeze({
|
|
61
|
+
maxFileBytes: 8 * 1024 * 1024,
|
|
62
|
+
maxTotalBytes: 64 * 1024 * 1024,
|
|
63
|
+
maxEntries: 1e5,
|
|
64
|
+
maxDirectories: 1e4,
|
|
65
|
+
maxDirectoryDepth: 128,
|
|
66
|
+
maxConfigDepth: 128,
|
|
67
|
+
maxFindings: 5e4,
|
|
68
|
+
maxOutputBytes: 32 * 1024 * 1024
|
|
69
|
+
});
|
|
70
|
+
const resolveScanLimits = (input) => {
|
|
71
|
+
if (input !== void 0 && (!isPlainRecord(input) || Object.keys(input).some((key) => !Object.hasOwn(DEFAULT_SCAN_LIMITS, key)))) throw new UsageError("limits must be an object of known scan limits.");
|
|
72
|
+
const limits = {
|
|
73
|
+
...DEFAULT_SCAN_LIMITS,
|
|
74
|
+
...input
|
|
75
|
+
};
|
|
76
|
+
for (const [key, value] of Object.entries(limits)) if (!Number.isSafeInteger(value) || value <= 0) throw new UsageError(`${key} must be a positive safe integer.`);
|
|
77
|
+
return limits;
|
|
78
|
+
};
|
|
79
|
+
const SCAN_SCOPE_ADVICE = "Reduce the scan scope. If the target contains independent projects, scan each project directory separately.";
|
|
80
|
+
const checkLimit = (name, value, limits) => {
|
|
81
|
+
if (value > limits[name]) {
|
|
82
|
+
const advice = name === "maxFileBytes" ? "Reduce the size of the input file." : SCAN_SCOPE_ADVICE;
|
|
83
|
+
throw new ConfigError(`Inspection exceeds ${name} (${limits[name]}). ${advice}`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
/** Counts UTF-8 without allocating another copy of a potentially large input. */
|
|
87
|
+
const utf8Bytes = (text) => {
|
|
88
|
+
let bytes = 0;
|
|
89
|
+
for (const character of text) {
|
|
90
|
+
const code = character.codePointAt(0);
|
|
91
|
+
bytes += code <= 127 ? 1 : code <= 2047 ? 2 : code <= 65535 ? 3 : 4;
|
|
92
|
+
}
|
|
93
|
+
return bytes;
|
|
94
|
+
};
|
|
95
|
+
/** Iterative traversal; shared YAML mappings are legal, cycles are not. Root depth is 1. */
|
|
96
|
+
const checkConfigDepth = (value, maxDepth) => {
|
|
97
|
+
const active = /* @__PURE__ */ new Set();
|
|
98
|
+
const seen = /* @__PURE__ */ new Map();
|
|
99
|
+
const pending = [{
|
|
100
|
+
value,
|
|
101
|
+
depth: 1
|
|
102
|
+
}];
|
|
103
|
+
while (pending.length) {
|
|
104
|
+
const item = pending.pop();
|
|
105
|
+
if (item.value === null || typeof item.value !== "object") continue;
|
|
106
|
+
if (item.leave) {
|
|
107
|
+
active.delete(item.value);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (active.has(item.value)) throw new ConfigError("Configuration contains a circular reference.");
|
|
111
|
+
if (item.depth > maxDepth) throw new ConfigError(`Configuration exceeds maxConfigDepth (${maxDepth}). Simplify the nesting of the configuration.`);
|
|
112
|
+
if ((seen.get(item.value) ?? 0) >= item.depth) continue;
|
|
113
|
+
seen.set(item.value, item.depth);
|
|
114
|
+
active.add(item.value);
|
|
115
|
+
pending.push({
|
|
116
|
+
...item,
|
|
117
|
+
leave: true
|
|
118
|
+
});
|
|
119
|
+
for (const child of Object.values(item.value)) pending.push({
|
|
120
|
+
value: child,
|
|
121
|
+
depth: item.depth + 1
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/** Output budgets are output failures (70), not policy findings or incomplete input (2). */
|
|
126
|
+
const outputBudget = (maxBytes) => {
|
|
127
|
+
let bytes = 0;
|
|
128
|
+
return (text) => {
|
|
129
|
+
bytes += utf8Bytes(text);
|
|
130
|
+
if (bytes > maxBytes) throw new SiroError(`Output exceeds maxOutputBytes (${maxBytes}). ${SCAN_SCOPE_ADVICE}`, 70);
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/adapters/node-errors.ts
|
|
135
|
+
/** Only the Error and code properties are established by this guard. */
|
|
136
|
+
const isNodeError = (value) => value instanceof Error && "code" in value && typeof value.code === "string";
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/adapters/node-file-system.ts
|
|
139
|
+
/** Root ancestors are canonicalized; selected cwd and all below-root components must not be links. */
|
|
140
|
+
const strictPaths = (root) => {
|
|
141
|
+
const selected = path.resolve(root);
|
|
142
|
+
if (lstatSync(selected).isSymbolicLink()) throw new ConfigError("Strict filesystem rejects a symlink cwd.");
|
|
143
|
+
const canonical = realpathSync(selected);
|
|
144
|
+
return (file) => {
|
|
145
|
+
const relative = path.relative(root, file);
|
|
146
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new ConfigError("Strict filesystem path escapes cwd.");
|
|
147
|
+
let current = canonical;
|
|
148
|
+
for (const component of relative.split(path.sep).filter(Boolean)) {
|
|
149
|
+
current = path.join(current, component);
|
|
150
|
+
if (lstatSync(current).isSymbolicLink()) throw new ConfigError(`${file}: strict filesystem rejects symlinks.`);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
/** Construct fresh native budgets for each inspection. No counters survive another scan. */
|
|
155
|
+
const createNodeFileSystem = (limits = DEFAULT_SCAN_LIMITS, strictRoot) => {
|
|
156
|
+
const checkPath = strictRoot === void 0 ? () => {} : strictPaths(strictRoot);
|
|
157
|
+
let totalBytes = 0;
|
|
158
|
+
let entries = 0;
|
|
159
|
+
const regularFile = (file) => {
|
|
160
|
+
checkPath(file);
|
|
161
|
+
const stat = statSync(file);
|
|
162
|
+
if (!stat.isFile()) throw new ConfigError(`${file}: expected a regular file.`);
|
|
163
|
+
return stat;
|
|
164
|
+
};
|
|
165
|
+
return {
|
|
166
|
+
readDirectories(directory) {
|
|
167
|
+
checkPath(directory);
|
|
168
|
+
const stream = opendirSync(directory);
|
|
169
|
+
const names = [];
|
|
170
|
+
try {
|
|
171
|
+
for (let entry = stream.readSync(); entry !== null; entry = stream.readSync()) {
|
|
172
|
+
checkLimit("maxEntries", ++entries, limits);
|
|
173
|
+
if (entry.isDirectory()) names.push(entry.name);
|
|
174
|
+
}
|
|
175
|
+
} finally {
|
|
176
|
+
stream.closeSync();
|
|
177
|
+
}
|
|
178
|
+
return names;
|
|
179
|
+
},
|
|
180
|
+
exists(file) {
|
|
181
|
+
try {
|
|
182
|
+
regularFile(file);
|
|
183
|
+
return true;
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
readText(file) {
|
|
190
|
+
let fd;
|
|
191
|
+
try {
|
|
192
|
+
const stat = regularFile(file);
|
|
193
|
+
checkLimit("maxFileBytes", stat.size, limits);
|
|
194
|
+
checkLimit("maxTotalBytes", totalBytes + stat.size, limits);
|
|
195
|
+
fd = openSync(file, constants.O_RDONLY | constants.O_NONBLOCK | (strictRoot === void 0 ? 0 : constants.O_NOFOLLOW ?? 0));
|
|
196
|
+
if (!fstatSync(fd).isFile()) throw new ConfigError(`${file}: expected a regular file.`);
|
|
197
|
+
const chunks = [];
|
|
198
|
+
let size = 0;
|
|
199
|
+
for (;;) {
|
|
200
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, limits.maxFileBytes - size + 1, limits.maxTotalBytes - totalBytes + 1));
|
|
201
|
+
const read = readSync(fd, buffer);
|
|
202
|
+
if (read === 0) break;
|
|
203
|
+
size += read;
|
|
204
|
+
totalBytes += read;
|
|
205
|
+
checkLimit("maxFileBytes", size, limits);
|
|
206
|
+
checkLimit("maxTotalBytes", totalBytes, limits);
|
|
207
|
+
chunks.push(buffer.subarray(0, read));
|
|
208
|
+
}
|
|
209
|
+
return Buffer.concat(chunks, size).toString("utf8");
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (isNodeError(error) && error.code === "ENOENT") return;
|
|
212
|
+
throw error;
|
|
213
|
+
} finally {
|
|
214
|
+
if (fd !== void 0) closeSync(fd);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
const nodeFileSystem = {
|
|
220
|
+
readDirectories: (directory) => createNodeFileSystem().readDirectories(directory),
|
|
221
|
+
exists: (file) => createNodeFileSystem().exists(file),
|
|
222
|
+
readText: (file) => createNodeFileSystem().readText(file)
|
|
223
|
+
};
|
|
224
|
+
const assertDirectory = (root) => {
|
|
225
|
+
if (!statSync(root).isDirectory()) throw new UsageError("The lint target must be a directory.");
|
|
226
|
+
};
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/core/contracts/paths.ts
|
|
51
229
|
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
230
|
const asRelPath = (value) => {
|
|
53
231
|
if (!isRelPath(value)) throw new TypeError("Expected a repository-relative path without parent traversal.");
|
|
@@ -62,66 +240,21 @@ const asAbsPath = (value) => {
|
|
|
62
240
|
};
|
|
63
241
|
const nodePaths = {
|
|
64
242
|
isAbsolute: isAbsPath,
|
|
65
|
-
resolve
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const isNodeError = (value) => value instanceof Error && "code" in value && typeof value.code === "string";
|
|
72
|
-
//#endregion
|
|
73
|
-
//#region src/adapters/node-file-system.ts
|
|
74
|
-
const assertRegularFile = (filePath) => {
|
|
75
|
-
if (!statSync(filePath).isFile()) throw new ConfigError(`${filePath}: expected a regular file.`);
|
|
76
|
-
};
|
|
77
|
-
const nodeFileSystem = {
|
|
78
|
-
readDirectories(directory) {
|
|
79
|
-
return readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
243
|
+
resolve(root, relative) {
|
|
244
|
+
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)}`);
|
|
245
|
+
const resolved = path.join(root, relative);
|
|
246
|
+
const within = path.relative(root, resolved);
|
|
247
|
+
if (within === ".." || within.startsWith(`..${path.sep}`) || path.isAbsolute(within)) throw new ConfigError("Path escapes repository root.");
|
|
248
|
+
return asAbsPath(resolved);
|
|
80
249
|
},
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
target = lstatSync(path.join(parent, name), { bigint: true });
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (isNodeError(error) && error.code === "ENOENT") return void 0;
|
|
87
|
-
throw error;
|
|
88
|
-
}
|
|
89
|
-
if (!target.isDirectory()) return void 0;
|
|
90
|
-
const names = readdirSync(parent, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
91
|
-
if (names.includes(name)) return name;
|
|
92
|
-
const matches = names.filter((candidate) => {
|
|
93
|
-
const entry = lstatSync(path.join(parent, candidate), { bigint: true });
|
|
94
|
-
return entry.isDirectory() && entry.dev === target.dev && entry.ino === target.ino;
|
|
95
|
-
});
|
|
96
|
-
if (target.ino === 0n || matches.length !== 1) throw new ConfigError(`${parent}: cannot identify the resolved workspace directory ${name}.`);
|
|
97
|
-
return matches[0];
|
|
98
|
-
},
|
|
99
|
-
exists(filePath) {
|
|
100
|
-
try {
|
|
101
|
-
assertRegularFile(filePath);
|
|
102
|
-
return true;
|
|
103
|
-
} catch (error) {
|
|
104
|
-
if (isNodeError(error) && error.code === "ENOENT") return false;
|
|
105
|
-
throw error;
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
readText(filePath) {
|
|
109
|
-
try {
|
|
110
|
-
assertRegularFile(filePath);
|
|
111
|
-
return readFileSync(filePath, "utf8");
|
|
112
|
-
} catch (error) {
|
|
113
|
-
if (isNodeError(error) && error.code === "ENOENT") return;
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
250
|
+
child(parent, name) {
|
|
251
|
+
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)}`);
|
|
252
|
+
return parent === "." ? name : `${parent}/${name}`;
|
|
116
253
|
}
|
|
117
254
|
};
|
|
118
|
-
const resolveIn = (root, relPath) => nodePaths.resolve(root, relPath);
|
|
119
|
-
const assertDirectory = (root) => {
|
|
120
|
-
if (!statSync(root).isDirectory()) throw new UsageError("The lint target must be a directory.");
|
|
121
|
-
};
|
|
122
255
|
//#endregion
|
|
123
256
|
//#region package.json
|
|
124
|
-
var version$1 = "0.
|
|
257
|
+
var version$1 = "0.6.1";
|
|
125
258
|
var engines = { "node": "^22.18.0 || ^24.0.0" };
|
|
126
259
|
//#endregion
|
|
127
260
|
//#region src/version.ts
|
|
@@ -131,11 +264,12 @@ const SUPPORTED_NODE_RANGE = engines.node;
|
|
|
131
264
|
//#region src/adapters/node-version.ts
|
|
132
265
|
const isSupportedNodeVersion = (version) => satisfies(version, SUPPORTED_NODE_RANGE);
|
|
133
266
|
//#endregion
|
|
134
|
-
//#region src/
|
|
267
|
+
//#region src/core/contracts/pms.ts
|
|
135
268
|
/**
|
|
136
269
|
* Single source of truth for package managers and severities.
|
|
137
|
-
* Types, runtime sets, and ordering
|
|
138
|
-
*
|
|
270
|
+
* Types, runtime sets, and ordering derive from these tuples. Adding a manager
|
|
271
|
+
* still requires local detection signals, applicable rule bindings and verified
|
|
272
|
+
* policy/availability evidence; the tuple alone does not implement support.
|
|
139
273
|
*/
|
|
140
274
|
const PMS = [
|
|
141
275
|
"npm",
|
|
@@ -171,24 +305,68 @@ const parsePackageManagerField = (value) => {
|
|
|
171
305
|
if (typeof name !== "undefined" && isPM(name)) return name;
|
|
172
306
|
};
|
|
173
307
|
//#endregion
|
|
174
|
-
//#region src/
|
|
175
|
-
|
|
176
|
-
const
|
|
308
|
+
//#region src/core/pm-versions.ts
|
|
309
|
+
/** Release history establishes stable versions, not ranges or prerelease builds. */
|
|
310
|
+
const isStableVersion = (value) => {
|
|
311
|
+
if (typeof value !== "string" || !/^\d/u.test(value) || value.trim() !== value) return false;
|
|
312
|
+
const version = parse(value);
|
|
313
|
+
return version !== null && version.prerelease.length === 0;
|
|
314
|
+
};
|
|
315
|
+
/** Keep name-only detection independent from whether the declaration pins a version. */
|
|
316
|
+
const declaredPMVersion = (declaration) => {
|
|
317
|
+
const [pm, version, ...extra] = declaration?.trim().split("@") ?? [];
|
|
318
|
+
return pm && isPM(pm) && isStableVersion(version) && extra.length === 0 ? { [pm]: version } : {};
|
|
319
|
+
};
|
|
177
320
|
//#endregion
|
|
178
|
-
//#region src/
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
321
|
+
//#region src/core/inspection-options.ts
|
|
322
|
+
const denseArray = (value, name) => {
|
|
323
|
+
if (!Array.isArray(value)) throw new ConfigError(`${name} must be a dense array.`);
|
|
324
|
+
for (let index = 0; index < value.length; index += 1) if (!Object.hasOwn(value, index)) throw new ConfigError(`${name} must be a dense array.`);
|
|
325
|
+
return value;
|
|
326
|
+
};
|
|
327
|
+
const relativeInput = (value, name) => {
|
|
328
|
+
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.`);
|
|
329
|
+
return value.split("/").filter((part) => part && part !== ".").join("/") || ".";
|
|
330
|
+
};
|
|
331
|
+
const parseExcludes = (value) => [...new Set(denseArray(value, "exclude").map((input) => {
|
|
332
|
+
const pattern = relativeInput(input, "exclude");
|
|
333
|
+
if (pattern === "." || pattern.startsWith("!")) throw new ConfigError("exclude cannot exclude cwd (.) or re-include with leading !.");
|
|
334
|
+
return pattern;
|
|
335
|
+
}))];
|
|
336
|
+
const parseInstallationRoots = (value) => {
|
|
337
|
+
const roots = /* @__PURE__ */ new Map();
|
|
338
|
+
for (const input of denseArray(value, "installationRoots")) {
|
|
339
|
+
if (typeof input !== "string" && (!isPlainRecord(input) || Object.keys(input).some((key) => ![
|
|
340
|
+
"path",
|
|
341
|
+
"pm",
|
|
342
|
+
"pmVersion"
|
|
343
|
+
].includes(key)))) throw new ConfigError("installationRoots entries must be paths or { path, pm?, pmVersion? }.");
|
|
344
|
+
const entry = typeof input === "string" ? { path: input } : input;
|
|
345
|
+
const path = relativeInput(entry.path, "installationRoots");
|
|
346
|
+
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.`);
|
|
347
|
+
if (path === "." && (entry.pm !== void 0 || entry.pmVersion !== void 0)) throw new ConfigError("Use top-level pm / pmVersion / pms / pmVersions for installation root \".\".");
|
|
348
|
+
const root = {
|
|
349
|
+
path,
|
|
350
|
+
pm: entry.pm,
|
|
351
|
+
pmVersion: entry.pmVersion
|
|
352
|
+
};
|
|
353
|
+
const previous = roots.get(path);
|
|
354
|
+
if (previous && (previous.pm !== root.pm || previous.pmVersion !== root.pmVersion)) throw new ConfigError(`${path}: conflicting installationRoots entries.`);
|
|
355
|
+
roots.set(path, root);
|
|
356
|
+
}
|
|
357
|
+
return [...roots.values()];
|
|
184
358
|
};
|
|
185
359
|
//#endregion
|
|
186
|
-
//#region src/
|
|
360
|
+
//#region src/core/contracts/project-type.ts
|
|
361
|
+
const PROJECT_TYPES = ["application", "package"];
|
|
362
|
+
const isProjectType = (value) => PROJECT_TYPES.some((projectType) => projectType === value);
|
|
363
|
+
//#endregion
|
|
364
|
+
//#region src/core/contracts/reporter.ts
|
|
187
365
|
const isReporterShape = (value) => {
|
|
188
366
|
return isPlainRecord(value) && typeof value.name === "string" && typeof value.format === "function";
|
|
189
367
|
};
|
|
190
368
|
//#endregion
|
|
191
|
-
//#region src/
|
|
369
|
+
//#region src/core/contracts/config-value.ts
|
|
192
370
|
const CODEC_KINDS = [
|
|
193
371
|
"json",
|
|
194
372
|
"npmrc",
|
|
@@ -208,17 +386,17 @@ const getByPath = (config, keyPath) => {
|
|
|
208
386
|
/** Accept only mapping roots while preserving unvalidated values inside them. */
|
|
209
387
|
const toParsedConfig = (value) => {
|
|
210
388
|
if (isPlainRecord(value)) return value;
|
|
211
|
-
throw new
|
|
389
|
+
throw new ConfigParseError("Config root must be a mapping.");
|
|
212
390
|
};
|
|
213
391
|
//#endregion
|
|
214
|
-
//#region src/
|
|
392
|
+
//#region src/core/contracts/config-file-ref.ts
|
|
215
393
|
const CONFIG_FILE_KINDS = new Set(CODEC_KINDS);
|
|
216
394
|
const isConfigFileRefShape = (value) => {
|
|
217
395
|
if (!isPlainRecord(value) || typeof value.kind !== "string" || !isRelPath(value.path)) return false;
|
|
218
396
|
return CONFIG_FILE_KINDS.has(value.kind);
|
|
219
397
|
};
|
|
220
398
|
//#endregion
|
|
221
|
-
//#region src/
|
|
399
|
+
//#region src/core/contracts/rule.ts
|
|
222
400
|
const defineRule = (rule) => rule;
|
|
223
401
|
const isOptionalString = (value) => typeof value === "undefined" || typeof value === "string";
|
|
224
402
|
const isConfigValueShape = (value) => typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
@@ -252,22 +430,26 @@ const isRuleShape = (value) => {
|
|
|
252
430
|
return typeof value.id === "string" && typeof value.title === "string" && typeof value.description === "string" && typeof value.severity === "string" && isSeverity(value.severity) && isOptionalString(value.docs) && isProjectTypesShape(value.projectTypes) && isBindingsShape(value.bindings);
|
|
253
431
|
};
|
|
254
432
|
//#endregion
|
|
255
|
-
//#region src/
|
|
256
|
-
/**
|
|
257
|
-
const
|
|
258
|
-
if (typeof value
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
/** Keep name-only detection independent from whether the declaration pins a version. */
|
|
263
|
-
const declaredPMVersion = (declaration) => {
|
|
264
|
-
const [pm, version, ...extra] = declaration?.trim().split("@") ?? [];
|
|
265
|
-
return pm && isPM(pm) && isStableVersion(version) && extra.length === 0 ? { [pm]: version } : {};
|
|
433
|
+
//#region src/core/contracts/synchronous.ts
|
|
434
|
+
/** Reject only the submitted async value, without a process-wide rejection handler. */
|
|
435
|
+
const assertSynchronous = (value, label) => {
|
|
436
|
+
if (value !== null && (typeof value === "object" || typeof value === "function") && "then" in value && typeof value.then === "function") {
|
|
437
|
+
Promise.resolve(value).catch(() => {});
|
|
438
|
+
throw new ConfigError(`${label} must be synchronous; Promise/thenable values are not supported.`);
|
|
439
|
+
}
|
|
266
440
|
};
|
|
267
441
|
//#endregion
|
|
268
|
-
//#region src/
|
|
442
|
+
//#region src/core/parse-siro-config.ts
|
|
269
443
|
const RuleSettingSchema = vb.union([vb.picklist(SEVERITIES), vb.literal("off")]);
|
|
270
444
|
const ConfigSchema = vb.strictObject({
|
|
445
|
+
exclude: vb.optional(vb.custom((value) => {
|
|
446
|
+
parseExcludes(value);
|
|
447
|
+
return true;
|
|
448
|
+
})),
|
|
449
|
+
installationRoots: vb.optional(vb.custom((value) => {
|
|
450
|
+
parseInstallationRoots(value);
|
|
451
|
+
return true;
|
|
452
|
+
})),
|
|
271
453
|
customRules: vb.optional(vb.array(vb.custom(isRuleShape, "must be a structurally valid rule"))),
|
|
272
454
|
pms: vb.optional(vb.pipe(vb.array(vb.picklist(PMS)), vb.minLength(1, "must not be empty (omit the key to auto-detect, or list at least one PM)"))),
|
|
273
455
|
projectType: vb.optional(vb.picklist(PROJECT_TYPES)),
|
|
@@ -298,13 +480,14 @@ const formatIssues = (issues) => issues.map((issue) => {
|
|
|
298
480
|
return issue.message;
|
|
299
481
|
}).join("; ");
|
|
300
482
|
const parseConfig = (candidate, name = "siro.config") => {
|
|
483
|
+
assertSynchronous(candidate, name);
|
|
301
484
|
if (!isPlainRecord(candidate)) throw new ConfigError(`${name} must export a config object (got ${Array.isArray(candidate) ? "an array" : typeof candidate}).`);
|
|
302
485
|
const result = vb.safeParse(ConfigSchema, Object.fromEntries(Object.entries(candidate)));
|
|
303
486
|
if (!result.success) throw new ConfigError(`${name}: ${formatIssues(result.issues)}`);
|
|
304
487
|
return result.output;
|
|
305
488
|
};
|
|
306
489
|
//#endregion
|
|
307
|
-
//#region src/
|
|
490
|
+
//#region src/load-config.ts
|
|
308
491
|
const CONFIG_NAMES = [
|
|
309
492
|
"siro.config.ts",
|
|
310
493
|
"siro.config.mjs",
|
|
@@ -328,7 +511,7 @@ const loadConfig = async (cwd, nodeVersion = process.versions.node) => {
|
|
|
328
511
|
return parseConfig(mod !== null && typeof mod === "object" && "default" in mod ? mod.default : mod, name);
|
|
329
512
|
};
|
|
330
513
|
//#endregion
|
|
331
|
-
//#region src/
|
|
514
|
+
//#region src/core/config-files.ts
|
|
332
515
|
/** Known configuration locations and their parsers. */
|
|
333
516
|
const CONFIG_FILES = {
|
|
334
517
|
aubeWorkspace: {
|
|
@@ -361,7 +544,7 @@ const CONFIG_FILES = {
|
|
|
361
544
|
}
|
|
362
545
|
};
|
|
363
546
|
//#endregion
|
|
364
|
-
//#region src/
|
|
547
|
+
//#region src/core/rules/setting-availability.ts
|
|
365
548
|
const { npmrc: npmrc$7, packageJson: packageJson$2, pnpmWorkspace: pnpmWorkspace$11, yarnrc: yarnrc$10, bunfig: bunfig$6 } = CONFIG_FILES;
|
|
366
549
|
const settingAvailability = [
|
|
367
550
|
{
|
|
@@ -482,7 +665,7 @@ const settingAvailability = [
|
|
|
482
665
|
}
|
|
483
666
|
];
|
|
484
667
|
//#endregion
|
|
485
|
-
//#region src/
|
|
668
|
+
//#region src/core/rules/remediation-availability.ts
|
|
486
669
|
const sameSetting = (setting, target) => target.file.path === setting.file.path && target.file.kind === setting.file.kind && target.keyPath.length === setting.keyPath.length && target.keyPath.every((key, index) => key === setting.keyPath[index]);
|
|
487
670
|
/** Undefined means the target version or the setting's introduction is unknown. */
|
|
488
671
|
const settingSupportedByTarget = (pm, version, target) => {
|
|
@@ -501,7 +684,7 @@ const guardRemediationAvailability = (pm, version, remediation, targets = remedi
|
|
|
501
684
|
};
|
|
502
685
|
};
|
|
503
686
|
//#endregion
|
|
504
|
-
//#region src/
|
|
687
|
+
//#region src/core/rules/remediation.ts
|
|
505
688
|
/** Scalar writes must not discard an existing container or replace a non-object parent. */
|
|
506
689
|
const proposeChanges = (config, operations) => {
|
|
507
690
|
for (const operation of operations) {
|
|
@@ -525,7 +708,7 @@ const proposeChanges = (config, operations) => {
|
|
|
525
708
|
};
|
|
526
709
|
};
|
|
527
710
|
//#endregion
|
|
528
|
-
//#region src/
|
|
711
|
+
//#region src/core/rules/builders/require-config-key.ts
|
|
529
712
|
const overrideBindings = (rule, overrides) => ({
|
|
530
713
|
...rule,
|
|
531
714
|
bindings: {
|
|
@@ -536,7 +719,7 @@ const overrideBindings = (rule, overrides) => ({
|
|
|
536
719
|
const accepts = (spec, actual) => spec.accept ? spec.accept(actual) : actual === spec.value;
|
|
537
720
|
const checkKeyValue = (spec, config) => {
|
|
538
721
|
const actual = getByPath(config, spec.keyPath);
|
|
539
|
-
const coveredByDefault = actual === void 0 && spec.documentedDefault !== void 0 && spec.
|
|
722
|
+
const coveredByDefault = actual === void 0 && spec.documentedDefault !== void 0 && spec.defaultSafety === "unconditional" && accepts(spec, spec.documentedDefault);
|
|
540
723
|
if (!coveredByDefault && accepts(spec, actual)) return { state: "ok" };
|
|
541
724
|
const severity = coveredByDefault ? spec.defaultSatisfiedSeverity ?? "info" : void 0;
|
|
542
725
|
if (severity === "off") return { state: "ok" };
|
|
@@ -589,7 +772,7 @@ const requireConfigKey = (options) => {
|
|
|
589
772
|
};
|
|
590
773
|
};
|
|
591
774
|
//#endregion
|
|
592
|
-
//#region src/
|
|
775
|
+
//#region src/core/rules/builders/with-aube-paranoid.ts
|
|
593
776
|
const withAubeParanoid = (rule) => {
|
|
594
777
|
const { aube } = rule.bindings;
|
|
595
778
|
if (!aube) return rule;
|
|
@@ -602,13 +785,14 @@ const withAubeParanoid = (rule) => {
|
|
|
602
785
|
} });
|
|
603
786
|
};
|
|
604
787
|
//#endregion
|
|
605
|
-
//#region src/
|
|
788
|
+
//#region src/core/rules/advisory-check.ts
|
|
606
789
|
const { aubeWorkspace: aubeWorkspace$8 } = CONFIG_FILES;
|
|
607
790
|
const advisoryCheck = withAubeParanoid(requireConfigKey({
|
|
608
791
|
bindings: { aube: {
|
|
609
792
|
accept: (value) => value === "on" || value === "required",
|
|
610
793
|
docs: "https://aube.jdx.dev/security.html",
|
|
611
794
|
documentedDefault: "on",
|
|
795
|
+
defaultSafety: "unconditional",
|
|
612
796
|
file: aubeWorkspace$8,
|
|
613
797
|
keyPath: ["advisoryCheck"],
|
|
614
798
|
message: "Set `advisoryCheck: on` (or `required`) in aube-workspace.yaml to check dependencies against known-malicious advisories.",
|
|
@@ -621,11 +805,11 @@ const advisoryCheck = withAubeParanoid(requireConfigKey({
|
|
|
621
805
|
title: "Enable malicious-package advisory checks"
|
|
622
806
|
}));
|
|
623
807
|
//#endregion
|
|
624
|
-
//#region src/
|
|
808
|
+
//#region src/core/rules/config-predicates.ts
|
|
625
809
|
const isNonBlankString = (value) => typeof value === "string" && value.trim().length > 0;
|
|
626
810
|
const isStringList = (value) => Array.isArray(value) && Array.from(value).every(isNonBlankString);
|
|
627
811
|
//#endregion
|
|
628
|
-
//#region src/
|
|
812
|
+
//#region src/core/rules/approved-git-repos.ts
|
|
629
813
|
const { yarnrc: yarnrc$9 } = CONFIG_FILES;
|
|
630
814
|
const message = "Set `approvedGitRepositories: []` in .yarnrc.yml to block all git: protocol dependencies (or list approved repository globs).";
|
|
631
815
|
const approvedGitRepos = defineRule({
|
|
@@ -655,7 +839,7 @@ const approvedGitRepos = defineRule({
|
|
|
655
839
|
title: "Approve git repository dependencies"
|
|
656
840
|
});
|
|
657
841
|
//#endregion
|
|
658
|
-
//#region src/
|
|
842
|
+
//#region src/core/rules/audit-suppression.ts
|
|
659
843
|
const { yarnrc: yarnrc$8 } = CONFIG_FILES;
|
|
660
844
|
const isNonEmptyArray = (value) => Array.isArray(value) && value.length > 0;
|
|
661
845
|
const SUPPRESSION_KEYS = ["npmAuditIgnoreAdvisories", "npmAuditExcludePackages"];
|
|
@@ -688,7 +872,7 @@ const auditSuppression = defineRule({
|
|
|
688
872
|
title: "Review audit suppression entries"
|
|
689
873
|
});
|
|
690
874
|
//#endregion
|
|
691
|
-
//#region src/
|
|
875
|
+
//#region src/core/rules/block-auto-install.ts
|
|
692
876
|
const { bunfig: bunfig$5 } = CONFIG_FILES;
|
|
693
877
|
const blockAutoInstall = requireConfigKey({
|
|
694
878
|
bindings: { bun: {
|
|
@@ -706,7 +890,7 @@ const blockAutoInstall = requireConfigKey({
|
|
|
706
890
|
title: "Block automatic package installation"
|
|
707
891
|
});
|
|
708
892
|
//#endregion
|
|
709
|
-
//#region src/
|
|
893
|
+
//#region src/core/rules/block-exotic-subdeps.ts
|
|
710
894
|
const { aubeWorkspace: aubeWorkspace$7, npmrc: npmrc$6, pnpmWorkspace: pnpmWorkspace$10 } = CONFIG_FILES;
|
|
711
895
|
const isRootOrNone = (value) => value === "root" || value === "none";
|
|
712
896
|
const npmMessage = "Set `allow-git=none` and `allow-remote=none` in .npmrc to block git/tarball URLs; use `root` only when direct URL dependencies are required.";
|
|
@@ -743,14 +927,16 @@ const blockExoticSubdeps = overrideBindings(requireConfigKey({
|
|
|
743
927
|
aube: {
|
|
744
928
|
docs: "https://aube.jdx.dev/security.html",
|
|
745
929
|
documentedDefault: true,
|
|
930
|
+
defaultSafety: "unconditional",
|
|
746
931
|
file: aubeWorkspace$7,
|
|
747
932
|
keyPath: ["blockExoticSubdeps"],
|
|
748
933
|
message: "Set `blockExoticSubdeps: true` in aube-workspace.yaml to block git/tarball URLs in transitive dependencies.",
|
|
749
934
|
value: true
|
|
750
935
|
},
|
|
751
936
|
pnpm: {
|
|
752
|
-
docs: "https://pnpm.io/settings#blockexoticsubdeps",
|
|
937
|
+
docs: "https://pnpm.io/settings/dependency-resolution#blockexoticsubdeps",
|
|
753
938
|
documentedDefault: true,
|
|
939
|
+
defaultSafety: "conditional",
|
|
754
940
|
file: pnpmWorkspace$10,
|
|
755
941
|
keyPath: ["blockExoticSubdeps"],
|
|
756
942
|
message: "Set `blockExoticSubdeps: true` in pnpm-workspace.yaml to block git/tarball URLs in transitive dependencies.",
|
|
@@ -762,13 +948,13 @@ const blockExoticSubdeps = overrideBindings(requireConfigKey({
|
|
|
762
948
|
}
|
|
763
949
|
},
|
|
764
950
|
description: "Refuse to install transitive dependencies sourced from git or tarball URLs, which bypass registry integrity checking.",
|
|
765
|
-
docs: "https://pnpm.io/settings#blockexoticsubdeps",
|
|
951
|
+
docs: "https://pnpm.io/settings/dependency-resolution#blockexoticsubdeps",
|
|
766
952
|
id: "block-exotic-subdeps",
|
|
767
953
|
severity: "warn",
|
|
768
954
|
title: "Block exotic subdependencies"
|
|
769
955
|
}), { npm: npmBinding$1 });
|
|
770
956
|
//#endregion
|
|
771
|
-
//#region src/
|
|
957
|
+
//#region src/core/rules/bun-security-scanner.ts
|
|
772
958
|
const { bunfig: bunfig$4 } = CONFIG_FILES;
|
|
773
959
|
const bunSecurityScanner = defineRule({
|
|
774
960
|
bindings: { bun: {
|
|
@@ -807,12 +993,13 @@ const bunSecurityScanner = defineRule({
|
|
|
807
993
|
title: "Enable a bun install-time security scanner"
|
|
808
994
|
});
|
|
809
995
|
//#endregion
|
|
810
|
-
//#region src/
|
|
996
|
+
//#region src/core/rules/checksum-verification.ts
|
|
811
997
|
const { yarnrc: yarnrc$7 } = CONFIG_FILES;
|
|
812
998
|
const checksumVerification = requireConfigKey({
|
|
813
999
|
bindings: { yarn: {
|
|
814
1000
|
docs: "https://yarnpkg.com/configuration/yarnrc#checksumBehavior",
|
|
815
1001
|
documentedDefault: "throw",
|
|
1002
|
+
defaultSafety: "unconditional",
|
|
816
1003
|
file: yarnrc$7,
|
|
817
1004
|
keyPath: ["checksumBehavior"],
|
|
818
1005
|
message: "Set `checksumBehavior: throw` in .yarnrc.yml to reject packages whose checksum does not match the lockfile.",
|
|
@@ -825,7 +1012,7 @@ const checksumVerification = requireConfigKey({
|
|
|
825
1012
|
title: "Verify package checksums"
|
|
826
1013
|
});
|
|
827
1014
|
//#endregion
|
|
828
|
-
//#region src/
|
|
1015
|
+
//#region src/core/signals.ts
|
|
829
1016
|
const PM_SIGNALS = {
|
|
830
1017
|
aube: {
|
|
831
1018
|
configs: [CONFIG_FILES.aubeWorkspace.path],
|
|
@@ -860,11 +1047,11 @@ const PM_SIGNALS = {
|
|
|
860
1047
|
}
|
|
861
1048
|
};
|
|
862
1049
|
//#endregion
|
|
863
|
-
//#region src/
|
|
1050
|
+
//#region src/core/rules/commit-lockfile.ts
|
|
864
1051
|
const LOCKFILE_DOCS = {
|
|
865
1052
|
aube: "https://github.com/aubepkg/aube/blob/main/docs/package-manager/lockfiles.md",
|
|
866
1053
|
bun: "https://bun.com/docs/install/lockfile",
|
|
867
|
-
deno: "https://docs.deno.com/runtime/
|
|
1054
|
+
deno: "https://docs.deno.com/runtime/reference/deno_json/#lockfile",
|
|
868
1055
|
npm: "https://docs.npmjs.com/cli/v12/configuring-npm/package-lock-json",
|
|
869
1056
|
pnpm: "https://pnpm.io/git#lockfiles",
|
|
870
1057
|
yarn: "https://yarnpkg.com/getting-started/qa#should-lockfiles-be-committed-to-the-repository"
|
|
@@ -873,7 +1060,7 @@ const LOCKFILE_VERSION_NOTES = { deno: { configAvailableSince: "deno 1.28.0" } }
|
|
|
873
1060
|
const lockfileBinding = (pm) => {
|
|
874
1061
|
const { lockfiles, reusesLockfiles } = PM_SIGNALS[pm];
|
|
875
1062
|
const [primary] = lockfiles;
|
|
876
|
-
const accepted = [...lockfiles, ...reusesLockfiles ?? []]
|
|
1063
|
+
const accepted = [...lockfiles, ...reusesLockfiles ?? []];
|
|
877
1064
|
return {
|
|
878
1065
|
check(ctx) {
|
|
879
1066
|
if (pm === "aube" && ctx.exists(asRelPath("bun.lockb")) && !ctx.exists(asRelPath("bun.lock"))) return {
|
|
@@ -884,7 +1071,20 @@ const lockfileBinding = (pm) => {
|
|
|
884
1071
|
steps: ["Run `bun install --save-text-lockfile` to generate bun.lock, then commit it."]
|
|
885
1072
|
}
|
|
886
1073
|
};
|
|
887
|
-
if (
|
|
1074
|
+
if (pm === "npm") {
|
|
1075
|
+
if (ctx.exists(asRelPath(primary))) return { state: "ok" };
|
|
1076
|
+
if (ctx.exists(asRelPath("npm-shrinkwrap.json"))) {
|
|
1077
|
+
if (ctx.pmVersion && lt(ctx.pmVersion, "12.0.0")) return { state: "ok" };
|
|
1078
|
+
return {
|
|
1079
|
+
state: "violation",
|
|
1080
|
+
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.",
|
|
1081
|
+
remediation: {
|
|
1082
|
+
kind: "manual",
|
|
1083
|
+
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."]
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
} else if (accepted.some((lf) => ctx.exists(asRelPath(lf)))) return { state: "ok" };
|
|
888
1088
|
return {
|
|
889
1089
|
remediation: {
|
|
890
1090
|
kind: "manual",
|
|
@@ -935,13 +1135,13 @@ const commitLockfile = defineRule({
|
|
|
935
1135
|
yarn: lockfileBinding("yarn")
|
|
936
1136
|
},
|
|
937
1137
|
description: "Lockfiles pin the full dependency tree and integrity hashes, enabling reproducible, verifiable installs (e.g. `npm ci`).",
|
|
938
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1138
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#3-include-lockfiles",
|
|
939
1139
|
id: "commit-lockfile",
|
|
940
1140
|
severity: "error",
|
|
941
1141
|
title: "Commit a lockfile"
|
|
942
1142
|
});
|
|
943
1143
|
//#endregion
|
|
944
|
-
//#region src/
|
|
1144
|
+
//#region src/core/rules/dependency-overrides.ts
|
|
945
1145
|
const { aubeWorkspace: aubeWorkspace$6, pnpmWorkspace: pnpmWorkspace$9 } = CONFIG_FILES;
|
|
946
1146
|
const isNonEmptyObject$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
947
1147
|
const checkOverrides = (config, file) => {
|
|
@@ -967,16 +1167,16 @@ const makeBinding = (file, fileName, docs) => ({
|
|
|
967
1167
|
const dependencyOverrides = defineRule({
|
|
968
1168
|
bindings: {
|
|
969
1169
|
aube: makeBinding(aubeWorkspace$6, "aube-workspace.yaml", "https://aube.sh/settings/"),
|
|
970
|
-
pnpm: makeBinding(pnpmWorkspace$9, "pnpm-workspace.yaml", "https://pnpm.io/settings#overrides")
|
|
1170
|
+
pnpm: makeBinding(pnpmWorkspace$9, "pnpm-workspace.yaml", "https://pnpm.io/settings/dependency-resolution#overrides")
|
|
971
1171
|
},
|
|
972
1172
|
description: "Flag dependency overrides that can replace transitive packages with arbitrary versions or forks — a supply-chain injection vector.",
|
|
973
|
-
docs: "https://pnpm.io/settings#overrides",
|
|
1173
|
+
docs: "https://pnpm.io/settings/dependency-resolution#overrides",
|
|
974
1174
|
id: "dependency-overrides",
|
|
975
1175
|
severity: "info",
|
|
976
1176
|
title: "Review dependency overrides"
|
|
977
1177
|
});
|
|
978
1178
|
//#endregion
|
|
979
|
-
//#region src/
|
|
1179
|
+
//#region src/core/rules/disable-lifecycle-scripts.ts
|
|
980
1180
|
const { npmrc: npmrc$5, pnpmWorkspace: pnpmWorkspace$8, yarnrc: yarnrc$6, aubeWorkspace: aubeWorkspace$5, bunfig: bunfig$3 } = CONFIG_FILES;
|
|
981
1181
|
const pnpmBinding$4 = {
|
|
982
1182
|
check(ctx, config) {
|
|
@@ -1050,7 +1250,7 @@ const pnpmBinding$4 = {
|
|
|
1050
1250
|
}])
|
|
1051
1251
|
};
|
|
1052
1252
|
},
|
|
1053
|
-
docs: "https://pnpm.io/settings#strictdepbuilds",
|
|
1253
|
+
docs: "https://pnpm.io/settings/build#strictdepbuilds",
|
|
1054
1254
|
file: pnpmWorkspace$8,
|
|
1055
1255
|
versionNote: {
|
|
1056
1256
|
configAvailableSince: "pnpm 10.3.0",
|
|
@@ -1137,6 +1337,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
|
|
|
1137
1337
|
yarn: {
|
|
1138
1338
|
docs: "https://yarnpkg.com/configuration/yarnrc#enableScripts",
|
|
1139
1339
|
documentedDefault: false,
|
|
1340
|
+
defaultSafety: "conditional",
|
|
1140
1341
|
file: yarnrc$6,
|
|
1141
1342
|
keyPath: ["enableScripts"],
|
|
1142
1343
|
message: "Set `enableScripts: false` in .yarnrc.yml to pin the policy across versions.",
|
|
@@ -1148,7 +1349,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
|
|
|
1148
1349
|
}
|
|
1149
1350
|
},
|
|
1150
1351
|
description: "Malicious postinstall scripts are a primary supply-chain attack vector. Prevent automatic execution of dependency lifecycle scripts.",
|
|
1151
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1352
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#1-disable-lifecycle-scripts",
|
|
1152
1353
|
id: "disable-lifecycle-scripts",
|
|
1153
1354
|
severity: "error",
|
|
1154
1355
|
title: "Disable dependency lifecycle scripts"
|
|
@@ -1158,7 +1359,7 @@ const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigK
|
|
|
1158
1359
|
pnpm: pnpmBinding$4
|
|
1159
1360
|
}));
|
|
1160
1361
|
//#endregion
|
|
1161
|
-
//#region src/
|
|
1362
|
+
//#region src/core/rules/enforce-strict-ssl.ts
|
|
1162
1363
|
const { npmrc: npmrc$4, yarnrc: yarnrc$5 } = CONFIG_FILES;
|
|
1163
1364
|
const yarnMessage = "Set `enableStrictSsl: true` in .yarnrc.yml to enforce SSL certificate validation for registry connections.";
|
|
1164
1365
|
const yarnBinding = {
|
|
@@ -1197,6 +1398,7 @@ const enforceStrictSsl = overrideBindings(requireConfigKey({
|
|
|
1197
1398
|
bindings: { npm: {
|
|
1198
1399
|
docs: "https://docs.npmjs.com/cli/v11/using-npm/config#strict-ssl",
|
|
1199
1400
|
documentedDefault: true,
|
|
1401
|
+
defaultSafety: "unconditional",
|
|
1200
1402
|
file: npmrc$4,
|
|
1201
1403
|
keyPath: ["strict-ssl"],
|
|
1202
1404
|
message: "Set `strict-ssl=true` in .npmrc to enforce SSL certificate validation.",
|
|
@@ -1209,7 +1411,7 @@ const enforceStrictSsl = overrideBindings(requireConfigKey({
|
|
|
1209
1411
|
title: "Enforce SSL for registry connections"
|
|
1210
1412
|
}), { yarn: yarnBinding });
|
|
1211
1413
|
//#endregion
|
|
1212
|
-
//#region src/
|
|
1414
|
+
//#region src/core/resolve-project-type.ts
|
|
1213
1415
|
const isPublishableName = (value) => typeof value === "string" && value.trim().length > 0;
|
|
1214
1416
|
const resolveProjectType = (selected, inferredPackage) => selected ?? (inferredPackage ? "package" : "application");
|
|
1215
1417
|
const resolvePackageJsonProjectType = (ctx) => {
|
|
@@ -1219,10 +1421,10 @@ const resolvePackageJsonProjectType = (ctx) => {
|
|
|
1219
1421
|
};
|
|
1220
1422
|
const resolveDenoProjectType = (ctx, denoConfig) => resolveProjectType(ctx.projectType, getByPath(denoConfig, ["publish"]) !== false && isPublishableName(getByPath(denoConfig, ["name"])));
|
|
1221
1423
|
//#endregion
|
|
1222
|
-
//#region src/
|
|
1424
|
+
//#region src/core/rules/publishable.ts
|
|
1223
1425
|
const isPublishable = (ctx) => resolvePackageJsonProjectType(ctx) === "package";
|
|
1224
1426
|
//#endregion
|
|
1225
|
-
//#region src/
|
|
1427
|
+
//#region src/core/rules/files-field.ts
|
|
1226
1428
|
const { packageJson: packageJson$1, denoJson: denoJson$3 } = CONFIG_FILES;
|
|
1227
1429
|
const packageJsonFilesBinding = {
|
|
1228
1430
|
check(ctx) {
|
|
@@ -1242,47 +1444,48 @@ const packageJsonFilesBinding = {
|
|
|
1242
1444
|
docs: "https://docs.npmjs.com/cli/v11/configuring-npm/package-json#files",
|
|
1243
1445
|
file: packageJson$1
|
|
1244
1446
|
};
|
|
1447
|
+
const denoPublishBinding = {
|
|
1448
|
+
check(ctx, config) {
|
|
1449
|
+
if (resolveDenoProjectType(ctx, config) !== "package") return { state: "na" };
|
|
1450
|
+
const include = getByPath(config, ["publish", "include"]);
|
|
1451
|
+
if (isStringList(include) && include.length > 0) return { state: "ok" };
|
|
1452
|
+
return {
|
|
1453
|
+
remediation: {
|
|
1454
|
+
kind: "manual",
|
|
1455
|
+
steps: ["Add a non-empty `publish.include` array to deno.json; use `publish.exclude` for additional exclusions."]
|
|
1456
|
+
},
|
|
1457
|
+
actual: include,
|
|
1458
|
+
message: "Add `publish.include` to deno.json to limit what gets published.",
|
|
1459
|
+
state: "violation"
|
|
1460
|
+
};
|
|
1461
|
+
},
|
|
1462
|
+
docs: "https://docs.deno.com/runtime/reference/deno_json/#publish---override-.gitignore",
|
|
1463
|
+
file: denoJson$3
|
|
1464
|
+
};
|
|
1245
1465
|
const filesField = defineRule({
|
|
1246
1466
|
bindings: {
|
|
1247
1467
|
aube: packageJsonFilesBinding,
|
|
1248
1468
|
bun: packageJsonFilesBinding,
|
|
1249
|
-
deno:
|
|
1250
|
-
check(ctx, config) {
|
|
1251
|
-
if (resolveDenoProjectType(ctx, config) !== "package") return { state: "na" };
|
|
1252
|
-
const include = getByPath(config, ["publish", "include"]);
|
|
1253
|
-
if (isStringList(include) && include.length > 0) return { state: "ok" };
|
|
1254
|
-
return {
|
|
1255
|
-
remediation: {
|
|
1256
|
-
kind: "manual",
|
|
1257
|
-
steps: ["Add a non-empty `publish.include` array to deno.json; use `publish.exclude` for additional exclusions."]
|
|
1258
|
-
},
|
|
1259
|
-
actual: include,
|
|
1260
|
-
message: "Add `publish.include` to deno.json to limit what gets published.",
|
|
1261
|
-
state: "violation"
|
|
1262
|
-
};
|
|
1263
|
-
},
|
|
1264
|
-
docs: "https://docs.deno.com/runtime/reference/cli/publish/#how-publishing-works",
|
|
1265
|
-
file: denoJson$3
|
|
1266
|
-
},
|
|
1469
|
+
deno: denoPublishBinding,
|
|
1267
1470
|
npm: packageJsonFilesBinding,
|
|
1268
1471
|
pnpm: packageJsonFilesBinding,
|
|
1269
1472
|
yarn: packageJsonFilesBinding
|
|
1270
1473
|
},
|
|
1271
1474
|
description: "An explicit `files` array in package.json restricts what gets published, preventing accidental inclusion of secrets or local files.",
|
|
1272
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1475
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#12-review-published-files",
|
|
1273
1476
|
id: "files-field",
|
|
1274
1477
|
projectTypes: ["package"],
|
|
1275
1478
|
severity: "info",
|
|
1276
1479
|
title: "Declare a published files allow-list"
|
|
1277
1480
|
});
|
|
1278
1481
|
//#endregion
|
|
1279
|
-
//#region src/
|
|
1482
|
+
//#region src/core/rules/frozen-lockfile.ts
|
|
1280
1483
|
const { pnpmWorkspace: pnpmWorkspace$7, yarnrc: yarnrc$4, bunfig: bunfig$2, denoJson: denoJson$2 } = CONFIG_FILES;
|
|
1281
1484
|
const aubeFrozenCommand = "Use `aube ci` or `aube install --frozen-lockfile` to fail when the lockfile would change.";
|
|
1282
1485
|
const frozenLockfile = overrideBindings(requireConfigKey({
|
|
1283
1486
|
bindings: {
|
|
1284
1487
|
deno: {
|
|
1285
|
-
docs: "https://docs.deno.com/runtime/
|
|
1488
|
+
docs: "https://docs.deno.com/runtime/reference/deno_json/#lockfile",
|
|
1286
1489
|
file: denoJson$2,
|
|
1287
1490
|
keyPath: ["lock", "frozen"],
|
|
1288
1491
|
message: "Set `lock.frozen: true` in deno.json for reproducible, verified installs.",
|
|
@@ -1297,7 +1500,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1297
1500
|
versionNote: { configAvailableSince: "bun 0.6.10" }
|
|
1298
1501
|
},
|
|
1299
1502
|
pnpm: {
|
|
1300
|
-
docs: "https://pnpm.io/
|
|
1503
|
+
docs: "https://pnpm.io/cli/install#--frozen-lockfile",
|
|
1301
1504
|
file: pnpmWorkspace$7,
|
|
1302
1505
|
keyPath: ["frozenLockfile"],
|
|
1303
1506
|
message: "pnpm defaults frozenLockfile to true in CI — set it explicitly in pnpm-workspace.yaml to pin the policy outside CI too.",
|
|
@@ -1306,6 +1509,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1306
1509
|
yarn: {
|
|
1307
1510
|
docs: "https://yarnpkg.com/configuration/yarnrc#enableImmutableInstalls",
|
|
1308
1511
|
documentedDefault: true,
|
|
1512
|
+
defaultSafety: "conditional",
|
|
1309
1513
|
file: yarnrc$4,
|
|
1310
1514
|
keyPath: ["enableImmutableInstalls"],
|
|
1311
1515
|
message: "Set `enableImmutableInstalls: true` in .yarnrc.yml to pin the policy outside CI too.",
|
|
@@ -1317,7 +1521,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1317
1521
|
}
|
|
1318
1522
|
},
|
|
1319
1523
|
description: "Refuse to mutate the lockfile on install so unexpected dependency changes fail loudly.",
|
|
1320
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1524
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#3-include-lockfiles",
|
|
1321
1525
|
id: "frozen-lockfile",
|
|
1322
1526
|
severity: "warn",
|
|
1323
1527
|
title: "Freeze the lockfile"
|
|
@@ -1336,7 +1540,7 @@ const frozenLockfile = overrideBindings(requireConfigKey({
|
|
|
1336
1540
|
severity: "info"
|
|
1337
1541
|
} });
|
|
1338
1542
|
//#endregion
|
|
1339
|
-
//#region src/
|
|
1543
|
+
//#region src/core/rules/frozen-store.ts
|
|
1340
1544
|
const { pnpmWorkspace: pnpmWorkspace$6 } = CONFIG_FILES;
|
|
1341
1545
|
const frozenStore = defineRule({
|
|
1342
1546
|
bindings: { pnpm: {
|
|
@@ -1367,12 +1571,13 @@ const frozenStore = defineRule({
|
|
|
1367
1571
|
title: "Consider a pre-populated read-only store"
|
|
1368
1572
|
});
|
|
1369
1573
|
//#endregion
|
|
1370
|
-
//#region src/
|
|
1574
|
+
//#region src/core/rules/hardened-mode.ts
|
|
1371
1575
|
const { yarnrc: yarnrc$3 } = CONFIG_FILES;
|
|
1372
1576
|
const hardenedMode = requireConfigKey({
|
|
1373
1577
|
bindings: { yarn: {
|
|
1374
1578
|
docs: "https://yarnpkg.com/configuration/yarnrc#enableHardenedMode",
|
|
1375
1579
|
documentedDefault: true,
|
|
1580
|
+
defaultSafety: "conditional",
|
|
1376
1581
|
file: yarnrc$3,
|
|
1377
1582
|
keyPath: ["enableHardenedMode"],
|
|
1378
1583
|
message: "yarn enables hardened mode automatically only for pull requests on public repositories — set `enableHardenedMode: true` in .yarnrc.yml to extend end-to-end install verification to every install.",
|
|
@@ -1389,7 +1594,7 @@ const hardenedMode = requireConfigKey({
|
|
|
1389
1594
|
title: "Enable hardened mode (yarn)"
|
|
1390
1595
|
});
|
|
1391
1596
|
//#endregion
|
|
1392
|
-
//#region src/
|
|
1597
|
+
//#region src/core/rules/deno-release-age.ts
|
|
1393
1598
|
const DENO_DURATION = /^\+?P(?:\+?\d+[Ww]|(?:\d+[Dd])*(?:T(?:\d+[HhMm]|\d+(?:\.\d+)?[Ss])+)?)$/u;
|
|
1394
1599
|
const DENO_MIN_TIMESTAMP = Date.UTC(-262143, 0, 1);
|
|
1395
1600
|
const DENO_UNIT_SECONDS = {
|
|
@@ -1427,7 +1632,7 @@ const isActiveDenoReleaseAge = (value, now, parse) => {
|
|
|
1427
1632
|
return !Number.isNaN(midnight.valueOf()) && midnight.toISOString().slice(0, 10) === date && timestamp < now;
|
|
1428
1633
|
};
|
|
1429
1634
|
//#endregion
|
|
1430
|
-
//#region src/
|
|
1635
|
+
//#region src/core/rules/minimum-release-age.ts
|
|
1431
1636
|
const HOURS_PER_DAY = 24;
|
|
1432
1637
|
const MINUTES_PER_HOUR = 60;
|
|
1433
1638
|
const SECONDS_PER_MINUTE = 60;
|
|
@@ -1444,7 +1649,7 @@ const isNonDisabledDenoReleaseAge = (value, now, parse) => {
|
|
|
1444
1649
|
if (!isPlainRecord(value)) return isActiveDenoReleaseAge(value, now, parse);
|
|
1445
1650
|
if (Object.keys(value).some((key) => key !== "age" && key !== "exclude")) return false;
|
|
1446
1651
|
if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
|
|
1447
|
-
return value.age
|
|
1652
|
+
return value.age != null && isActiveDenoReleaseAge(value.age, now, parse);
|
|
1448
1653
|
};
|
|
1449
1654
|
const denoAgeUsesFallback = (value) => {
|
|
1450
1655
|
if (value === void 0 || value === null) return true;
|
|
@@ -1453,13 +1658,14 @@ const denoAgeUsesFallback = (value) => {
|
|
|
1453
1658
|
if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
|
|
1454
1659
|
return value.age == null;
|
|
1455
1660
|
};
|
|
1456
|
-
const isPositiveDenoNpmrcDays = (value) => typeof value === "number" && Number.isSafeInteger(value) && value
|
|
1457
|
-
const baseRule$
|
|
1661
|
+
const isPositiveDenoNpmrcDays = (value, now, parse) => typeof value === "number" && Number.isSafeInteger(value) && isActiveDenoReleaseAge(value * MINUTES_PER_DAY, now, parse);
|
|
1662
|
+
const baseRule$2 = requireConfigKey({
|
|
1458
1663
|
bindings: {
|
|
1459
1664
|
aube: {
|
|
1460
1665
|
accept: isPositiveNumber,
|
|
1461
1666
|
docs: "https://aube.sh/settings/",
|
|
1462
1667
|
documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
|
|
1668
|
+
defaultSafety: "unconditional",
|
|
1463
1669
|
file: aubeWorkspace$4,
|
|
1464
1670
|
keyPath: ["minimumReleaseAge"],
|
|
1465
1671
|
message: `Set minimumReleaseAge (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in aube-workspace.yaml.`,
|
|
@@ -1476,8 +1682,9 @@ const baseRule$1 = requireConfigKey({
|
|
|
1476
1682
|
},
|
|
1477
1683
|
pnpm: {
|
|
1478
1684
|
accept: isPositiveNumber,
|
|
1479
|
-
docs: "https://pnpm.io/settings#minimumreleaseage",
|
|
1685
|
+
docs: "https://pnpm.io/settings/dependency-resolution#minimumreleaseage",
|
|
1480
1686
|
documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
|
|
1687
|
+
defaultSafety: "conditional",
|
|
1481
1688
|
file: pnpmWorkspace$5,
|
|
1482
1689
|
keyPath: ["minimumReleaseAge"],
|
|
1483
1690
|
message: `Set minimumReleaseAge (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in pnpm-workspace.yaml.`,
|
|
@@ -1491,6 +1698,7 @@ const baseRule$1 = requireConfigKey({
|
|
|
1491
1698
|
accept: isPositiveYarnDuration,
|
|
1492
1699
|
docs: "https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate",
|
|
1493
1700
|
documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
|
|
1701
|
+
defaultSafety: "conditional",
|
|
1494
1702
|
file: yarnrc$2,
|
|
1495
1703
|
keyPath: ["npmMinimalAgeGate"],
|
|
1496
1704
|
message: `Set npmMinimalAgeGate (~${RECOMMENDED_RELEASE_AGE_MINUTES} minutes for a 3-day cooldown) in .yarnrc.yml.`,
|
|
@@ -1502,7 +1710,7 @@ const baseRule$1 = requireConfigKey({
|
|
|
1502
1710
|
}
|
|
1503
1711
|
},
|
|
1504
1712
|
description: "Refuse to install releases newer than a cooldown window so freshly published (possibly compromised) versions are skipped.",
|
|
1505
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#set-
|
|
1713
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#2-set-cooldowns--minimum-release-age",
|
|
1506
1714
|
id: "minimum-release-age",
|
|
1507
1715
|
severity: "warn",
|
|
1508
1716
|
title: "Set a minimum release age"
|
|
@@ -1563,10 +1771,11 @@ const createDenoBinding = (time) => ({
|
|
|
1563
1771
|
},
|
|
1564
1772
|
check(ctx, config) {
|
|
1565
1773
|
const actual = getByPath(config, ["minimumDependencyAge"]);
|
|
1774
|
+
const now = time.now();
|
|
1566
1775
|
if (denoAgeUsesFallback(actual)) {
|
|
1567
1776
|
const npmrcConfig = ctx.readConfig(npmrc$3);
|
|
1568
1777
|
const npmrcAge = getByPath(npmrcConfig, ["min-release-age"]);
|
|
1569
|
-
if (isPositiveDenoNpmrcDays(npmrcAge)) return { state: "ok" };
|
|
1778
|
+
if (isPositiveDenoNpmrcDays(npmrcAge, now, time.parse)) return { state: "ok" };
|
|
1570
1779
|
if (npmrcAge === 0) return {
|
|
1571
1780
|
state: "violation",
|
|
1572
1781
|
actual: npmrcAge,
|
|
@@ -1581,7 +1790,7 @@ const createDenoBinding = (time) => ({
|
|
|
1581
1790
|
}])
|
|
1582
1791
|
};
|
|
1583
1792
|
}
|
|
1584
|
-
if (isNonDisabledDenoReleaseAge(actual,
|
|
1793
|
+
if (isNonDisabledDenoReleaseAge(actual, now, time.parse)) return { state: "ok" };
|
|
1585
1794
|
const objectAge = isPlainRecord(actual);
|
|
1586
1795
|
const invalidObject = objectAge && (Object.keys(actual).some((key) => key !== "age" && key !== "exclude") || actual.exclude !== void 0 && !isStringList(actual.exclude));
|
|
1587
1796
|
return {
|
|
@@ -1601,12 +1810,12 @@ const createDenoBinding = (time) => ({
|
|
|
1601
1810
|
};
|
|
1602
1811
|
}
|
|
1603
1812
|
});
|
|
1604
|
-
const createMinimumReleaseAge = (time) => overrideBindings(baseRule$
|
|
1813
|
+
const createMinimumReleaseAge = (time) => overrideBindings(baseRule$2, {
|
|
1605
1814
|
npm: createNpmBinding(time),
|
|
1606
1815
|
deno: createDenoBinding(time)
|
|
1607
1816
|
});
|
|
1608
1817
|
//#endregion
|
|
1609
|
-
//#region src/
|
|
1818
|
+
//#region src/core/rules/named-registries.ts
|
|
1610
1819
|
const { pnpmWorkspace: pnpmWorkspace$4 } = CONFIG_FILES;
|
|
1611
1820
|
const isNonEmptyObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
1612
1821
|
const namedRegistries = defineRule({
|
|
@@ -1624,17 +1833,17 @@ const namedRegistries = defineRule({
|
|
|
1624
1833
|
state: "violation"
|
|
1625
1834
|
};
|
|
1626
1835
|
},
|
|
1627
|
-
docs: "https://pnpm.io/settings#namedregistries",
|
|
1836
|
+
docs: "https://pnpm.io/settings/dependency-resolution#namedregistries",
|
|
1628
1837
|
file: pnpmWorkspace$4
|
|
1629
1838
|
} },
|
|
1630
1839
|
description: "Flag named registry mappings that redirect package resolution to custom registries, which may enable dependency confusion attacks.",
|
|
1631
|
-
docs: "https://pnpm.io/settings#namedregistries",
|
|
1840
|
+
docs: "https://pnpm.io/settings/dependency-resolution#namedregistries",
|
|
1632
1841
|
id: "named-registries",
|
|
1633
1842
|
severity: "info",
|
|
1634
1843
|
title: "Review named registry mappings"
|
|
1635
1844
|
});
|
|
1636
1845
|
//#endregion
|
|
1637
|
-
//#region src/
|
|
1846
|
+
//#region src/core/rules/paranoid-mode.ts
|
|
1638
1847
|
const { aubeWorkspace: aubeWorkspace$3 } = CONFIG_FILES;
|
|
1639
1848
|
const paranoidMode = requireConfigKey({
|
|
1640
1849
|
bindings: { aube: {
|
|
@@ -1651,7 +1860,7 @@ const paranoidMode = requireConfigKey({
|
|
|
1651
1860
|
title: "Enable paranoid security mode"
|
|
1652
1861
|
});
|
|
1653
1862
|
//#endregion
|
|
1654
|
-
//#region src/
|
|
1863
|
+
//#region src/core/rules/patched-dependencies.ts
|
|
1655
1864
|
const { pnpmWorkspace: pnpmWorkspace$3 } = CONFIG_FILES;
|
|
1656
1865
|
const isNonEmptyObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
1657
1866
|
const patchedDependencies = defineRule({
|
|
@@ -1669,17 +1878,17 @@ const patchedDependencies = defineRule({
|
|
|
1669
1878
|
state: "violation"
|
|
1670
1879
|
};
|
|
1671
1880
|
},
|
|
1672
|
-
docs: "https://pnpm.io/
|
|
1881
|
+
docs: "https://pnpm.io/cli/patch#patcheddependencies",
|
|
1673
1882
|
file: pnpmWorkspace$3
|
|
1674
1883
|
} },
|
|
1675
1884
|
description: "Review local patches separately from the registry artifacts they modify.",
|
|
1676
|
-
docs: "https://pnpm.io/
|
|
1885
|
+
docs: "https://pnpm.io/cli/patch#patcheddependencies",
|
|
1677
1886
|
id: "patched-dependencies",
|
|
1678
1887
|
severity: "info",
|
|
1679
1888
|
title: "Review patched dependencies"
|
|
1680
1889
|
});
|
|
1681
1890
|
//#endregion
|
|
1682
|
-
//#region src/
|
|
1891
|
+
//#region src/core/rules/pin-exact-versions.ts
|
|
1683
1892
|
const { npmrc: npmrc$2, pnpmWorkspace: pnpmWorkspace$2, yarnrc: yarnrc$1, bunfig, denoJson } = CONFIG_FILES;
|
|
1684
1893
|
const REGISTRY_VERSION = /^(?:npm|jsr):(?:@[^/@]+\/)?[^/@]+@(?<version>[^/]*)/u;
|
|
1685
1894
|
const isUnpinnedRegistryImport = (specifier) => {
|
|
@@ -1688,9 +1897,14 @@ const isUnpinnedRegistryImport = (specifier) => {
|
|
|
1688
1897
|
const normalized = version?.startsWith("=") ? version.slice(1) : version;
|
|
1689
1898
|
return normalized === void 0 || valid(normalized) === null;
|
|
1690
1899
|
};
|
|
1691
|
-
const collectUnpinnedImports = (imports) => {
|
|
1900
|
+
const collectUnpinnedImports = (imports, location) => {
|
|
1901
|
+
if (!isPlainRecord(imports)) throw new ConfigError(`deno.json: ${location} must be an import mapping object.`);
|
|
1692
1902
|
const offenders = [];
|
|
1693
|
-
for (const [name, value] of Object.entries(imports))
|
|
1903
|
+
for (const [name, value] of Object.entries(imports)) {
|
|
1904
|
+
if (value === null) continue;
|
|
1905
|
+
if (typeof value !== "string") throw new ConfigError(`deno.json: ${location}.${name} must be a string or null.`);
|
|
1906
|
+
if (isUnpinnedRegistryImport(value)) offenders.push(`${location}.${name}=${value}`);
|
|
1907
|
+
}
|
|
1694
1908
|
return offenders;
|
|
1695
1909
|
};
|
|
1696
1910
|
const MAX_SAMPLE_COUNT = 3;
|
|
@@ -1701,23 +1915,26 @@ const formatOffenders = (offenders) => {
|
|
|
1701
1915
|
return {
|
|
1702
1916
|
remediation: {
|
|
1703
1917
|
kind: "manual",
|
|
1704
|
-
steps: ["
|
|
1918
|
+
steps: ["Pin each reported imports/scopes entry in deno.json to an exact version. For top-level imports, `deno add --save-exact <pkg>` can help; edit scoped mappings at their reported locations."]
|
|
1705
1919
|
},
|
|
1706
1920
|
message: `${offenders.length} deno imports are not pinned: ${sample}${more}. Use \`deno add --save-exact\` or pin manually.`,
|
|
1707
1921
|
state: "violation"
|
|
1708
1922
|
};
|
|
1709
1923
|
};
|
|
1710
1924
|
const OK = { state: "ok" };
|
|
1711
|
-
const
|
|
1925
|
+
const unpinnedInlineImports = (config) => {
|
|
1712
1926
|
const imports = getByPath(config, ["imports"]);
|
|
1713
|
-
|
|
1714
|
-
|
|
1927
|
+
const offenders = imports == null ? [] : collectUnpinnedImports(imports, "imports");
|
|
1928
|
+
const scopes = getByPath(config, ["scopes"]);
|
|
1929
|
+
if (scopes != null) {
|
|
1930
|
+
if (!isPlainRecord(scopes)) throw new ConfigError("deno.json: scopes must be an object.");
|
|
1931
|
+
for (const [scope, mapping] of Object.entries(scopes)) for (const offender of collectUnpinnedImports(mapping, `scopes[${JSON.stringify(scope)}]`)) offenders.push(offender);
|
|
1932
|
+
}
|
|
1933
|
+
return offenders;
|
|
1715
1934
|
};
|
|
1716
1935
|
const denoBinding = {
|
|
1717
1936
|
check(_ctx, config) {
|
|
1718
|
-
const
|
|
1719
|
-
if (!imports) return OK;
|
|
1720
|
-
const offenders = collectUnpinnedImports(imports);
|
|
1937
|
+
const offenders = unpinnedInlineImports(config);
|
|
1721
1938
|
if (offenders.length === 0) return OK;
|
|
1722
1939
|
return formatOffenders(offenders);
|
|
1723
1940
|
},
|
|
@@ -1748,7 +1965,7 @@ const npmBinding = {
|
|
|
1748
1965
|
};
|
|
1749
1966
|
const aubeBinding = {
|
|
1750
1967
|
file: npmrc$2,
|
|
1751
|
-
docs: "https://aube.
|
|
1968
|
+
docs: "https://aube.sh/settings/#setting-saveprefix",
|
|
1752
1969
|
check(_ctx, config) {
|
|
1753
1970
|
const prefixes = ["save-prefix", "savePrefix"].filter((key) => Object.hasOwn(config, key)).map((key) => config[key]);
|
|
1754
1971
|
if (prefixes.length > 0 && prefixes.every((value) => value === "")) return OK;
|
|
@@ -1789,8 +2006,8 @@ const pinExactVersions = overrideBindings(requireConfigKey({
|
|
|
1789
2006
|
versionNote: { configAvailableSince: "yarn 2.0.0" }
|
|
1790
2007
|
}
|
|
1791
2008
|
},
|
|
1792
|
-
description: "Semver ranges (^, ~) auto-adopt new releases, including compromised ones. Save exact versions by default.",
|
|
1793
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
2009
|
+
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).",
|
|
2010
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#4-pin-dependency-versions",
|
|
1794
2011
|
id: "pin-exact-versions",
|
|
1795
2012
|
severity: "error",
|
|
1796
2013
|
title: "Pin exact dependency versions"
|
|
@@ -1800,7 +2017,7 @@ const pinExactVersions = overrideBindings(requireConfigKey({
|
|
|
1800
2017
|
npm: npmBinding
|
|
1801
2018
|
});
|
|
1802
2019
|
//#endregion
|
|
1803
|
-
//#region src/
|
|
2020
|
+
//#region src/core/rules/provenance.ts
|
|
1804
2021
|
const { npmrc: npmrc$1, yarnrc } = CONFIG_FILES;
|
|
1805
2022
|
const npmrcProvenance = {
|
|
1806
2023
|
file: npmrc$1,
|
|
@@ -1808,7 +2025,7 @@ const npmrcProvenance = {
|
|
|
1808
2025
|
message: "Set `provenance=true` (and publish from CI) to attest releases.",
|
|
1809
2026
|
value: true
|
|
1810
2027
|
};
|
|
1811
|
-
const
|
|
2028
|
+
const baseRule = requireConfigKey({
|
|
1812
2029
|
applies: isPublishable,
|
|
1813
2030
|
bindings: {
|
|
1814
2031
|
bun: {
|
|
@@ -1834,14 +2051,37 @@ const provenance = requireConfigKey({
|
|
|
1834
2051
|
}
|
|
1835
2052
|
},
|
|
1836
2053
|
description: "Provenance statements (via Sigstore) bind a published artifact to its recorded source and build.",
|
|
1837
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
2054
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#11-generate-provenance-statements",
|
|
1838
2055
|
id: "provenance",
|
|
1839
2056
|
projectTypes: ["package"],
|
|
1840
2057
|
severity: "warn",
|
|
1841
2058
|
title: "Publish with provenance"
|
|
1842
2059
|
});
|
|
2060
|
+
const provenance = overrideBindings(baseRule, { npm: {
|
|
2061
|
+
...baseRule.bindings.npm,
|
|
2062
|
+
check(ctx, config) {
|
|
2063
|
+
if (!isPublishable(ctx)) return { state: "na" };
|
|
2064
|
+
const publishConfig = ctx.packageJson?.publishConfig;
|
|
2065
|
+
if (!publishConfig || !Object.hasOwn(publishConfig, "provenance")) return baseRule.bindings.npm.check(ctx, config);
|
|
2066
|
+
if (publishConfig.provenance === true) return { state: "ok" };
|
|
2067
|
+
const file = CONFIG_FILES.packageJson;
|
|
2068
|
+
return {
|
|
2069
|
+
state: "violation",
|
|
2070
|
+
file: file.path,
|
|
2071
|
+
actual: publishConfig.provenance,
|
|
2072
|
+
expected: true,
|
|
2073
|
+
message: "Set publishConfig.provenance=true in package.json (and publish from CI) to attest releases.",
|
|
2074
|
+
remediation: guardRemediationAvailability("npm", ctx.pmVersion, proposeChanges(ctx.readConfig(file), [{
|
|
2075
|
+
file,
|
|
2076
|
+
op: "setKey",
|
|
2077
|
+
keyPath: ["publishConfig", "provenance"],
|
|
2078
|
+
value: true
|
|
2079
|
+
}]))
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
} });
|
|
1843
2083
|
//#endregion
|
|
1844
|
-
//#region src/
|
|
2084
|
+
//#region src/core/rules/publish-access.ts
|
|
1845
2085
|
const { packageJson } = CONFIG_FILES;
|
|
1846
2086
|
const publishAccessBinding = {
|
|
1847
2087
|
check(ctx) {
|
|
@@ -1884,7 +2124,7 @@ const publishAccess = defineRule({
|
|
|
1884
2124
|
title: "Declare publish access explicitly"
|
|
1885
2125
|
});
|
|
1886
2126
|
//#endregion
|
|
1887
|
-
//#region src/
|
|
2127
|
+
//#region src/core/rules/store-server.ts
|
|
1888
2128
|
const { pnpmWorkspace: pnpmWorkspace$1 } = CONFIG_FILES;
|
|
1889
2129
|
const storeServer = defineRule({
|
|
1890
2130
|
bindings: { pnpm: {
|
|
@@ -1901,17 +2141,17 @@ const storeServer = defineRule({
|
|
|
1901
2141
|
state: "violation"
|
|
1902
2142
|
};
|
|
1903
2143
|
},
|
|
1904
|
-
docs: "https://pnpm.io/settings#
|
|
2144
|
+
docs: "https://pnpm.io/settings/store#userunningstoreserver",
|
|
1905
2145
|
file: pnpmWorkspace$1
|
|
1906
2146
|
} },
|
|
1907
2147
|
description: "Flag use of an external store server process, which introduces a trust boundary where tampered packages could be served.",
|
|
1908
|
-
docs: "https://pnpm.io/settings#
|
|
2148
|
+
docs: "https://pnpm.io/settings/store#userunningstoreserver",
|
|
1909
2149
|
id: "store-server",
|
|
1910
2150
|
severity: "info",
|
|
1911
2151
|
title: "Review store server usage"
|
|
1912
2152
|
});
|
|
1913
2153
|
//#endregion
|
|
1914
|
-
//#region src/
|
|
2154
|
+
//#region src/core/rules/strict-allow-scripts.ts
|
|
1915
2155
|
const { npmrc } = CONFIG_FILES;
|
|
1916
2156
|
const strictAllowScripts = defineRule({
|
|
1917
2157
|
bindings: { npm: {
|
|
@@ -1953,7 +2193,7 @@ const strictAllowScripts = defineRule({
|
|
|
1953
2193
|
title: "Require explicit script allow-listing"
|
|
1954
2194
|
});
|
|
1955
2195
|
//#endregion
|
|
1956
|
-
//#region src/
|
|
2196
|
+
//#region src/core/rules/strict-release-age.ts
|
|
1957
2197
|
const { aubeWorkspace: aubeWorkspace$2 } = CONFIG_FILES;
|
|
1958
2198
|
const strictReleaseAge = withAubeParanoid(requireConfigKey({
|
|
1959
2199
|
bindings: { aube: {
|
|
@@ -1970,7 +2210,7 @@ const strictReleaseAge = withAubeParanoid(requireConfigKey({
|
|
|
1970
2210
|
title: "Enforce strict release age gate"
|
|
1971
2211
|
}));
|
|
1972
2212
|
//#endregion
|
|
1973
|
-
//#region src/
|
|
2213
|
+
//#region src/core/rules/strict-store-integrity.ts
|
|
1974
2214
|
const { aubeWorkspace: aubeWorkspace$1 } = CONFIG_FILES;
|
|
1975
2215
|
const strictStoreIntegrity = defineRule({
|
|
1976
2216
|
bindings: { aube: {
|
|
@@ -2010,20 +2250,21 @@ const strictStoreIntegrity = defineRule({
|
|
|
2010
2250
|
title: "Require tarball integrity metadata"
|
|
2011
2251
|
});
|
|
2012
2252
|
//#endregion
|
|
2013
|
-
//#region src/
|
|
2253
|
+
//#region src/core/rules/trust-policy.ts
|
|
2014
2254
|
const { aubeWorkspace, pnpmWorkspace } = CONFIG_FILES;
|
|
2015
2255
|
const trustPolicy = withAubeParanoid(requireConfigKey({
|
|
2016
2256
|
bindings: {
|
|
2017
2257
|
aube: {
|
|
2018
2258
|
docs: "https://aube.jdx.dev/security.html",
|
|
2019
2259
|
documentedDefault: "no-downgrade",
|
|
2260
|
+
defaultSafety: "unconditional",
|
|
2020
2261
|
file: aubeWorkspace,
|
|
2021
2262
|
keyPath: ["trustPolicy"],
|
|
2022
2263
|
message: "Set `trustPolicy: no-downgrade` in aube-workspace.yaml to block packages whose trust level has decreased.",
|
|
2023
2264
|
value: "no-downgrade"
|
|
2024
2265
|
},
|
|
2025
2266
|
pnpm: {
|
|
2026
|
-
docs: "https://pnpm.io/settings#trustpolicy",
|
|
2267
|
+
docs: "https://pnpm.io/settings/dependency-resolution#trustpolicy",
|
|
2027
2268
|
file: pnpmWorkspace,
|
|
2028
2269
|
keyPath: ["trustPolicy"],
|
|
2029
2270
|
message: "Set `trustPolicy: no-downgrade` in pnpm-workspace.yaml to block packages whose trust level has decreased.",
|
|
@@ -2032,15 +2273,15 @@ const trustPolicy = withAubeParanoid(requireConfigKey({
|
|
|
2032
2273
|
}
|
|
2033
2274
|
},
|
|
2034
2275
|
description: "Fail installation when a package trust level has decreased compared to previous releases, catching publisher credential downgrades.",
|
|
2035
|
-
docs: "https://pnpm.io/settings#trustpolicy",
|
|
2276
|
+
docs: "https://pnpm.io/settings/dependency-resolution#trustpolicy",
|
|
2036
2277
|
id: "trust-policy",
|
|
2037
2278
|
severity: "warn",
|
|
2038
2279
|
title: "Enforce trust policy on dependency updates"
|
|
2039
2280
|
}));
|
|
2040
2281
|
//#endregion
|
|
2041
|
-
//#region src/
|
|
2042
|
-
const bindingFor = (pm) => {
|
|
2043
|
-
const settings = settingAvailability.filter((setting) => setting.pm === pm);
|
|
2282
|
+
//#region src/core/rules/unsupported-settings.ts
|
|
2283
|
+
const bindingFor = (pm, includesFile) => {
|
|
2284
|
+
const settings = settingAvailability.filter((setting) => setting.pm === pm && includesFile(setting.file.path));
|
|
2044
2285
|
return { check(ctx) {
|
|
2045
2286
|
const version = ctx.pmVersion;
|
|
2046
2287
|
if (version === void 0) return { state: "na" };
|
|
@@ -2063,22 +2304,23 @@ const bindingFor = (pm) => {
|
|
|
2063
2304
|
} : { state: "ok" };
|
|
2064
2305
|
} };
|
|
2065
2306
|
};
|
|
2066
|
-
const
|
|
2307
|
+
const createUnsupportedSettings = (includesFile) => defineRule({
|
|
2067
2308
|
id: "unsupported-settings",
|
|
2068
2309
|
title: "Use settings available in the target PM version",
|
|
2069
|
-
description: "Report configured settings whose recorded introduction version is newer than the declared or explicit stable PM target. Reports each affected file separately, grouping its unsupported keys.
|
|
2310
|
+
description: "Report configured settings whose recorded introduction version is newer than the declared or explicit stable PM target. Reports each affected file separately, grouping its unsupported keys. Findings retain their evaluation directory. Only the coverage table below is checked. Unknown targets and unlisted settings are not evaluated for availability.",
|
|
2070
2311
|
severity: "error",
|
|
2071
2312
|
docs: "https://github.com/pHo9UBenaA/siro/blob/main/docs/rules.md#unsupported-settings--error",
|
|
2072
2313
|
bindings: {
|
|
2073
|
-
npm: bindingFor("npm"),
|
|
2074
|
-
pnpm: bindingFor("pnpm"),
|
|
2075
|
-
yarn: bindingFor("yarn"),
|
|
2076
|
-
bun: bindingFor("bun"),
|
|
2077
|
-
deno: bindingFor("deno")
|
|
2314
|
+
npm: bindingFor("npm", includesFile),
|
|
2315
|
+
pnpm: bindingFor("pnpm", includesFile),
|
|
2316
|
+
yarn: bindingFor("yarn", includesFile),
|
|
2317
|
+
bun: bindingFor("bun", includesFile),
|
|
2318
|
+
deno: bindingFor("deno", includesFile)
|
|
2078
2319
|
}
|
|
2079
2320
|
});
|
|
2321
|
+
const unsupportedSettings = createUnsupportedSettings(() => true);
|
|
2080
2322
|
//#endregion
|
|
2081
|
-
//#region src/
|
|
2323
|
+
//#region src/core/rules/builtin-rules.ts
|
|
2082
2324
|
const createBuiltinRules = (time) => [
|
|
2083
2325
|
advisoryCheck,
|
|
2084
2326
|
approvedGitRepos,
|
|
@@ -2109,448 +2351,57 @@ const createBuiltinRules = (time) => [
|
|
|
2109
2351
|
trustPolicy,
|
|
2110
2352
|
unsupportedSettings
|
|
2111
2353
|
];
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
dot: options.includeDotDirectories ?? false,
|
|
2147
|
-
nobrace: options.syntax === "wildcards",
|
|
2148
|
-
noext: options.syntax === "wildcards" || options.extendedPatterns === false,
|
|
2149
|
-
optimizationLevel: options.syntax === "shell" ? 2 : 1
|
|
2150
|
-
};
|
|
2151
|
-
};
|
|
2152
|
-
/** Compile once so membership, exclusions, and traversal share glob semantics. */
|
|
2153
|
-
const compileWorkspaceGlob = (pattern, options) => {
|
|
2154
|
-
let matcher;
|
|
2354
|
+
const builtinScope = {
|
|
2355
|
+
"advisory-check": "installation",
|
|
2356
|
+
"approved-git-repos": "installation",
|
|
2357
|
+
"audit-suppression": "installation",
|
|
2358
|
+
"block-auto-install": "installation",
|
|
2359
|
+
"block-exotic-subdeps": "installation",
|
|
2360
|
+
"bun-security-scanner": "installation",
|
|
2361
|
+
"checksum-verification": "installation",
|
|
2362
|
+
"commit-lockfile": "installation",
|
|
2363
|
+
"dependency-overrides": "installation",
|
|
2364
|
+
"disable-lifecycle-scripts": "installation",
|
|
2365
|
+
"enforce-strict-ssl": "installation",
|
|
2366
|
+
"files-field": "manifest",
|
|
2367
|
+
"frozen-lockfile": "installation",
|
|
2368
|
+
"frozen-store": "installation",
|
|
2369
|
+
"hardened-mode": "installation",
|
|
2370
|
+
"minimum-release-age": "installation",
|
|
2371
|
+
"named-registries": "installation",
|
|
2372
|
+
"paranoid-mode": "installation",
|
|
2373
|
+
"patched-dependencies": "installation",
|
|
2374
|
+
"pin-exact-versions": "installation",
|
|
2375
|
+
provenance: "installation",
|
|
2376
|
+
"publish-access": "manifest",
|
|
2377
|
+
"store-server": "installation",
|
|
2378
|
+
"strict-allow-scripts": "installation",
|
|
2379
|
+
"strict-release-age": "installation",
|
|
2380
|
+
"strict-store-integrity": "installation",
|
|
2381
|
+
"trust-policy": "installation",
|
|
2382
|
+
"unsupported-settings": "split"
|
|
2383
|
+
};
|
|
2384
|
+
const scopeOf = (id) => Object.hasOwn(builtinScope, id) ? builtinScope[id] : "custom";
|
|
2385
|
+
//#endregion
|
|
2386
|
+
//#region src/adapters/exclusions.ts
|
|
2387
|
+
const compileExclusions = (patterns) => {
|
|
2155
2388
|
try {
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2389
|
+
const matchers = patterns.map((pattern) => new Minimatch(pattern.replace(/(?:\/\*\*)+$/u, "").replace(/[\\[\]]/gu, "\\$&"), {
|
|
2390
|
+
dot: true,
|
|
2391
|
+
nocase: false,
|
|
2392
|
+
nonegate: true,
|
|
2393
|
+
nocomment: true,
|
|
2394
|
+
nobrace: true,
|
|
2395
|
+
noext: true,
|
|
2396
|
+
platform: "linux"
|
|
2397
|
+
}));
|
|
2398
|
+
return (directory) => matchers.some((matcher) => matcher.match(directory));
|
|
2162
2399
|
} catch (error) {
|
|
2163
|
-
|
|
2164
|
-
throw error;
|
|
2400
|
+
throw new ConfigError(`exclude: ${error instanceof Error ? error.message : String(error)}`);
|
|
2165
2401
|
}
|
|
2166
|
-
return {
|
|
2167
|
-
matches: (directory) => matcher.match(directory),
|
|
2168
|
-
canDescend(directory) {
|
|
2169
|
-
const parts = directory.split("/");
|
|
2170
|
-
return matcher.set.some((alternative) => (alternative.includes(GLOBSTAR) || parts.length < alternative.length) && matcher.matchOne(parts, alternative, true));
|
|
2171
|
-
}
|
|
2172
|
-
};
|
|
2173
|
-
};
|
|
2174
|
-
const minimatchGlobs = {
|
|
2175
|
-
expand: expandWorkspaceGlob,
|
|
2176
|
-
compile: compileWorkspaceGlob
|
|
2177
|
-
};
|
|
2178
|
-
//#endregion
|
|
2179
|
-
//#region src/domain/services/parse-config-file.ts
|
|
2180
|
-
const EMPTY_FILE = Object.freeze({});
|
|
2181
|
-
/** Create a fresh parser and cache for one lint command. */
|
|
2182
|
-
const createConfigParser = (codecFor, ctx) => {
|
|
2183
|
-
const cache = /* @__PURE__ */ new Map();
|
|
2184
|
-
return (file) => {
|
|
2185
|
-
if (file === void 0) return EMPTY_FILE;
|
|
2186
|
-
const cacheKey = `${file.kind}:${file.path}`;
|
|
2187
|
-
const cached = cache.get(cacheKey);
|
|
2188
|
-
if (cached !== void 0) return cached;
|
|
2189
|
-
const text = ctx.readText(file.path);
|
|
2190
|
-
if (text === void 0) {
|
|
2191
|
-
cache.set(cacheKey, EMPTY_FILE);
|
|
2192
|
-
return EMPTY_FILE;
|
|
2193
|
-
}
|
|
2194
|
-
const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
|
|
2195
|
-
cache.set(cacheKey, parsed);
|
|
2196
|
-
return parsed;
|
|
2197
|
-
};
|
|
2198
2402
|
};
|
|
2199
2403
|
//#endregion
|
|
2200
|
-
//#region src/
|
|
2201
|
-
/** Detect the basic * and ? markers shared across workspace dialects. */
|
|
2202
|
-
const hasBasicWorkspaceWildcard = (pattern) => pattern.includes("*") || pattern.includes("?");
|
|
2203
|
-
/** Workspace declarations use POSIX separators regardless of the host platform. */
|
|
2204
|
-
const stripTrailingWorkspaceSlashes = (pattern) => {
|
|
2205
|
-
let end = pattern.length;
|
|
2206
|
-
while (end > 0 && pattern[end - 1] === "/") end -= 1;
|
|
2207
|
-
return pattern.slice(0, end);
|
|
2208
|
-
};
|
|
2209
|
-
//#endregion
|
|
2210
|
-
//#region src/application/workspace-prefix.ts
|
|
2211
|
-
/** Anchor Deno's native literal prefix before applying its glob suffix. */
|
|
2212
|
-
const anchorWorkspacePrefix = (pattern, glob, resolveChild) => {
|
|
2213
|
-
const parts = pattern.split("/");
|
|
2214
|
-
const firstWildcard = parts.findIndex(hasBasicWorkspaceWildcard);
|
|
2215
|
-
const prefix = parts.slice(0, firstWildcard < 0 ? parts.length : firstWildcard);
|
|
2216
|
-
if (prefix.length === 0 || pattern === ".") return glob;
|
|
2217
|
-
const declared = prefix.join("/");
|
|
2218
|
-
let resolved;
|
|
2219
|
-
let attempted = false;
|
|
2220
|
-
const resolve = () => {
|
|
2221
|
-
if (!attempted) {
|
|
2222
|
-
attempted = true;
|
|
2223
|
-
let current = ".";
|
|
2224
|
-
for (const part of prefix) {
|
|
2225
|
-
const child = resolveChild(current, part);
|
|
2226
|
-
if (child === void 0) return void 0;
|
|
2227
|
-
current = current === "." ? child : `${current}/${child}`;
|
|
2228
|
-
}
|
|
2229
|
-
resolved = current;
|
|
2230
|
-
}
|
|
2231
|
-
return resolved;
|
|
2232
|
-
};
|
|
2233
|
-
const rewrite = (directory, actual) => directory === actual ? declared : directory.startsWith(`${actual}/`) ? declared + directory.slice(actual.length) : void 0;
|
|
2234
|
-
return {
|
|
2235
|
-
matches(directory) {
|
|
2236
|
-
if (directory === ".") return false;
|
|
2237
|
-
const actual = resolve();
|
|
2238
|
-
const rewritten = actual === void 0 ? void 0 : rewrite(directory, actual);
|
|
2239
|
-
return rewritten !== void 0 && glob.matches(rewritten);
|
|
2240
|
-
},
|
|
2241
|
-
canDescend(directory) {
|
|
2242
|
-
const actual = resolve();
|
|
2243
|
-
if (actual === void 0) return false;
|
|
2244
|
-
if (directory === "." || actual.startsWith(`${directory}/`)) return true;
|
|
2245
|
-
const rewritten = rewrite(directory, actual);
|
|
2246
|
-
return rewritten !== void 0 && glob.canDescend(rewritten);
|
|
2247
|
-
}
|
|
2248
|
-
};
|
|
2249
|
-
};
|
|
2250
|
-
//#endregion
|
|
2251
|
-
//#region src/application/workspace-glob-policy.ts
|
|
2252
|
-
const workspaceGlobOptions = (caseInsensitive) => ({
|
|
2253
|
-
kind: "directory",
|
|
2254
|
-
syntax: "shell",
|
|
2255
|
-
caseInsensitive
|
|
2256
|
-
});
|
|
2257
|
-
//#endregion
|
|
2258
|
-
//#region src/application/workspace-dialects.ts
|
|
2259
|
-
const containsUnsupportedAubeGlobSyntax = (pattern) => /[[\]{}()]/u.test(pattern);
|
|
2260
|
-
const matchesCrossPathGlob = (pattern, value) => {
|
|
2261
|
-
const patternCharacters = [...pattern];
|
|
2262
|
-
const valueCharacters = [...value];
|
|
2263
|
-
let patternIndex = 0;
|
|
2264
|
-
let valueIndex = 0;
|
|
2265
|
-
let starIndex = -1;
|
|
2266
|
-
let retryIndex = 0;
|
|
2267
|
-
while (valueIndex < valueCharacters.length) {
|
|
2268
|
-
const character = patternCharacters[patternIndex];
|
|
2269
|
-
if (character === "?" || character === valueCharacters[valueIndex]) {
|
|
2270
|
-
patternIndex += 1;
|
|
2271
|
-
valueIndex += 1;
|
|
2272
|
-
} else if (character === "*") {
|
|
2273
|
-
starIndex = patternIndex;
|
|
2274
|
-
retryIndex = valueIndex;
|
|
2275
|
-
patternIndex += 1;
|
|
2276
|
-
} else if (starIndex >= 0) {
|
|
2277
|
-
patternIndex = starIndex + 1;
|
|
2278
|
-
retryIndex += 1;
|
|
2279
|
-
valueIndex = retryIndex;
|
|
2280
|
-
} else return false;
|
|
2281
|
-
}
|
|
2282
|
-
while (patternCharacters[patternIndex] === "*") patternIndex += 1;
|
|
2283
|
-
return patternIndex === patternCharacters.length;
|
|
2284
|
-
};
|
|
2285
|
-
/** The bounded glob subset verified against the recorded Deno/Aube sources. */
|
|
2286
|
-
const compileAdditionalWorkspaceGlob = (pattern, pm, excluded, caseInsensitive, globs) => {
|
|
2287
|
-
const parts = pattern.split("/");
|
|
2288
|
-
if (parts.some((part) => part.includes("**") && part !== "**")) throw new ConfigError(`${pm}: ** must occupy an entire workspace path component.`);
|
|
2289
|
-
if (pm === "aube" && (containsUnsupportedAubeGlobSyntax(pattern) || pattern.includes("**") && (parts.at(-1) !== "**" || hasBasicWorkspaceWildcard(parts.slice(0, -1).join("/"))))) throw new ConfigError("Aube workspace inspection supports literals, * and ?, and a trailing ** after a literal prefix; other glob forms are not yet supported.");
|
|
2290
|
-
const matcher = globs.compile(pattern, {
|
|
2291
|
-
kind: "directory",
|
|
2292
|
-
syntax: "wildcards",
|
|
2293
|
-
includeDotDirectories: pm === "aube",
|
|
2294
|
-
caseInsensitive: pm === "deno" && (hasBasicWorkspaceWildcard(pattern) || caseInsensitive)
|
|
2295
|
-
});
|
|
2296
|
-
if (pm === "deno" && hasBasicWorkspaceWildcard(pattern)) {
|
|
2297
|
-
const files = globs.compile(`${pattern}/package.json`, {
|
|
2298
|
-
kind: "directory",
|
|
2299
|
-
syntax: "wildcards",
|
|
2300
|
-
caseInsensitive: true
|
|
2301
|
-
});
|
|
2302
|
-
return {
|
|
2303
|
-
...matcher,
|
|
2304
|
-
matches: (directory) => files.matches(directory === "." ? "package.json" : `${directory}/package.json`)
|
|
2305
|
-
};
|
|
2306
|
-
}
|
|
2307
|
-
if (pm !== "aube" || !excluded) return matcher;
|
|
2308
|
-
const self = pattern.endsWith("/**") ? pattern.slice(0, -3) : void 0;
|
|
2309
|
-
return {
|
|
2310
|
-
...matcher,
|
|
2311
|
-
matches: (directory) => matchesCrossPathGlob(pattern, directory) || directory === self
|
|
2312
|
-
};
|
|
2313
|
-
};
|
|
2314
|
-
//#endregion
|
|
2315
|
-
//#region src/application/workspaces.ts
|
|
2316
|
-
/** npm cancels an earlier exclusion when a later positive pattern matches it. */
|
|
2317
|
-
const splitNpmPattern = (raw) => {
|
|
2318
|
-
const prefix = /^!+/u.exec(raw)?.[0] ?? "";
|
|
2319
|
-
return {
|
|
2320
|
-
excluded: prefix.length % 2 === 1,
|
|
2321
|
-
pattern: raw.slice(prefix.length)
|
|
2322
|
-
};
|
|
2323
|
-
};
|
|
2324
|
-
const npmPatterns = (patterns, globs) => {
|
|
2325
|
-
const positive = [];
|
|
2326
|
-
let negative = [];
|
|
2327
|
-
for (const raw of patterns) {
|
|
2328
|
-
const parsed = splitNpmPattern(raw);
|
|
2329
|
-
const pattern = parsed.pattern.replace(/^\.?\/+/u, "");
|
|
2330
|
-
if (parsed.excluded) negative.push({
|
|
2331
|
-
pattern,
|
|
2332
|
-
glob: globs.compile(pattern, { kind: "declaration" })
|
|
2333
|
-
});
|
|
2334
|
-
else {
|
|
2335
|
-
for (let index = 0; index < negative.length; index += 1) if (negative[index]?.glob.matches(pattern)) negative.splice(index, 1);
|
|
2336
|
-
positive.push(pattern);
|
|
2337
|
-
}
|
|
2338
|
-
}
|
|
2339
|
-
return [...positive, ...negative.map(({ pattern }) => `!${pattern}`)];
|
|
2340
|
-
};
|
|
2341
|
-
/** Read declaration sources separately so Deno's two manifest sets stay distinct. */
|
|
2342
|
-
const workspaceDefinitions = (ctx, pm, dependencies) => {
|
|
2343
|
-
const { codecFor, paths, globs } = dependencies;
|
|
2344
|
-
const parse = createConfigParser(codecFor, ctx);
|
|
2345
|
-
const validate = (value, source, denoManifests = false) => {
|
|
2346
|
-
if (value === void 0) return {
|
|
2347
|
-
patterns: [],
|
|
2348
|
-
denoManifests
|
|
2349
|
-
};
|
|
2350
|
-
if (!Array.isArray(value) || !Array.from(value).every((item) => typeof item === "string")) throw new ConfigError(`${source}: expected an array of directory patterns.`);
|
|
2351
|
-
for (const pattern of value) {
|
|
2352
|
-
const positive = pm === "npm" ? splitNpmPattern(pattern).pattern : pattern.startsWith("!") ? pattern.slice(1) : pattern;
|
|
2353
|
-
if ((pm === "deno" || pm === "aube" ? [positive] : globs.expand(positive)).some((alternative) => !isRelPath(alternative) || alternative.includes("\\") || alternative.startsWith("!"))) throw new ConfigError(`${source}: use relative directory patterns without parent traversal: ${JSON.stringify(pattern)}.`);
|
|
2354
|
-
if (pm === "deno" && denoManifests && !pattern.startsWith("!") && paths.normalizePattern(positive) === ".") throw new ConfigError(`${source}: a Deno workspace cannot contain itself.`);
|
|
2355
|
-
}
|
|
2356
|
-
return {
|
|
2357
|
-
patterns: pm === "npm" ? npmPatterns(value, globs) : value,
|
|
2358
|
-
denoManifests
|
|
2359
|
-
};
|
|
2360
|
-
};
|
|
2361
|
-
const packageDefinition = () => {
|
|
2362
|
-
let value = ctx.packageJson?.workspaces;
|
|
2363
|
-
if (isPlainRecord(value)) {
|
|
2364
|
-
value = value.packages;
|
|
2365
|
-
if (value === void 0) throw new ConfigError("package.json#workspaces: expected a packages array.");
|
|
2366
|
-
}
|
|
2367
|
-
return validate(value, "package.json#workspaces");
|
|
2368
|
-
};
|
|
2369
|
-
if (pm === "pnpm") return [validate(ctx.exists(CONFIG_FILES.pnpmWorkspace.path) ? parse(CONFIG_FILES.pnpmWorkspace).packages : void 0, "pnpm-workspace.yaml#packages")];
|
|
2370
|
-
if (pm === "aube") {
|
|
2371
|
-
const file = [CONFIG_FILES.aubeWorkspace, CONFIG_FILES.pnpmWorkspace].find((candidate) => ctx.exists(candidate.path));
|
|
2372
|
-
return [file ? validate(parse(file).packages, `${file.path}#packages`) : packageDefinition()];
|
|
2373
|
-
}
|
|
2374
|
-
if (pm === "deno") return [validate(parse(CONFIG_FILES.denoJson).workspace, "deno.json#workspace", true), packageDefinition()];
|
|
2375
|
-
return [packageDefinition()];
|
|
2376
|
-
};
|
|
2377
|
-
/** Traverse only ordinary directories that can fall under a positive pattern. */
|
|
2378
|
-
const workspaceDirectories = (ctx, fs, definition, pm, dependencies) => {
|
|
2379
|
-
const { codecFor, paths, globs, caseInsensitiveGlobs } = dependencies;
|
|
2380
|
-
const defaultWorkspaceGlobOptions = workspaceGlobOptions(caseInsensitiveGlobs);
|
|
2381
|
-
const { patterns } = definition;
|
|
2382
|
-
const skipVendor = pm === "deno" && createConfigParser(codecFor, ctx)(CONFIG_FILES.denoJson).vendor === true;
|
|
2383
|
-
const positive = patterns.filter((pattern) => !pattern.startsWith("!")).map((pattern) => stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern)));
|
|
2384
|
-
const negative = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern.slice(1))));
|
|
2385
|
-
const directoryCache = /* @__PURE__ */ new Map();
|
|
2386
|
-
const readDirectories = (directory) => {
|
|
2387
|
-
const cached = directoryCache.get(directory);
|
|
2388
|
-
if (cached) return cached;
|
|
2389
|
-
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
2390
|
-
const names = fs.readDirectories(paths.resolve(ctx.root, asRelPath(directory)));
|
|
2391
|
-
if (!Array.isArray(names)) throw new ConfigError("FileSystem.readDirectories must return an array of directory names.");
|
|
2392
|
-
for (const name of Array.from(names)) if (!isRelPath(name) || name === "." || name === ".." || /[\\/]/u.test(name)) throw new ConfigError("FileSystem.readDirectories returned an invalid child directory name.");
|
|
2393
|
-
const result = [...new Set(names)].sort();
|
|
2394
|
-
directoryCache.set(directory, result);
|
|
2395
|
-
return result;
|
|
2396
|
-
};
|
|
2397
|
-
const resolveChild = (parent, name) => {
|
|
2398
|
-
if (name === ".git" || name === "node_modules") return void 0;
|
|
2399
|
-
const names = readDirectories(parent);
|
|
2400
|
-
const resolved = names.includes(name) ? name : fs.resolveDirectory?.(paths.resolve(ctx.root, asRelPath(parent)), name);
|
|
2401
|
-
if (resolved !== void 0 && !names.includes(resolved)) throw new ConfigError("FileSystem.resolveDirectory must return an enumerated ordinary child directory name.");
|
|
2402
|
-
return resolved === ".git" || resolved === "node_modules" ? void 0 : resolved;
|
|
2403
|
-
};
|
|
2404
|
-
const compile = (pattern, excluded = false) => {
|
|
2405
|
-
if (pm !== "deno" && pm !== "aube") return globs.compile(pattern, pm === "npm" ? {
|
|
2406
|
-
...defaultWorkspaceGlobOptions,
|
|
2407
|
-
hashComments: true
|
|
2408
|
-
} : defaultWorkspaceGlobOptions);
|
|
2409
|
-
const glob = compileAdditionalWorkspaceGlob(pattern, pm, excluded, caseInsensitiveGlobs, globs);
|
|
2410
|
-
return pm === "deno" && !excluded ? anchorWorkspacePrefix(pattern, glob, resolveChild) : glob;
|
|
2411
|
-
};
|
|
2412
|
-
const included = positive.map((pattern) => compile(pattern));
|
|
2413
|
-
const excluded = negative.map((pattern) => {
|
|
2414
|
-
return {
|
|
2415
|
-
glob: compile(pattern, true),
|
|
2416
|
-
subtree: pm === "deno" || pm === "aube" || pm === "bun" ? void 0 : pattern === "**" ? { matches: () => true } : pattern.endsWith("/**") ? compile(pattern.slice(0, -3), true) : void 0
|
|
2417
|
-
};
|
|
2418
|
-
});
|
|
2419
|
-
const declaredBase = (pattern) => {
|
|
2420
|
-
const parts = pattern.split("/");
|
|
2421
|
-
const wildcard = parts.findIndex(hasBasicWorkspaceWildcard);
|
|
2422
|
-
return parts.slice(0, wildcard < 0 ? parts.length : wildcard).join("/") || ".";
|
|
2423
|
-
};
|
|
2424
|
-
const relatedBases = (left, right) => left === "." || right === "." || left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
2425
|
-
const denoPositive = pm === "deno" ? positive.filter(hasBasicWorkspaceWildcard).map((pattern) => ({
|
|
2426
|
-
base: declaredBase(pattern),
|
|
2427
|
-
scope: anchorWorkspacePrefix(declaredBase(pattern), {
|
|
2428
|
-
matches: () => true,
|
|
2429
|
-
canDescend: () => true
|
|
2430
|
-
}, resolveChild)
|
|
2431
|
-
})) : [];
|
|
2432
|
-
const denoOrdered = pm === "deno" ? patterns.filter((pattern) => hasBasicWorkspaceWildcard(pattern) || pattern.startsWith("!")).map((raw) => {
|
|
2433
|
-
const isExcluded = raw.startsWith("!");
|
|
2434
|
-
const pattern = stripTrailingWorkspaceSlashes(paths.normalizePattern(isExcluded ? raw.slice(1) : raw));
|
|
2435
|
-
const glob = compile(pattern, isExcluded);
|
|
2436
|
-
if (!isExcluded) return {
|
|
2437
|
-
excluded: false,
|
|
2438
|
-
glob
|
|
2439
|
-
};
|
|
2440
|
-
return {
|
|
2441
|
-
excluded: true,
|
|
2442
|
-
glob: {
|
|
2443
|
-
...glob,
|
|
2444
|
-
matches(directory) {
|
|
2445
|
-
if (!hasBasicWorkspaceWildcard(pattern)) return directory === pattern;
|
|
2446
|
-
if (!glob.matches(directory)) return false;
|
|
2447
|
-
const applicable = denoPositive.filter((item) => item.scope.matches(directory)).map((item) => relatedBases(item.base, declaredBase(pattern)));
|
|
2448
|
-
if (applicable.some(Boolean) && applicable.some((value) => !value)) throw new ConfigError("Deno workspace exclusion has ambiguous overlapping bases; use consistent prefix spelling or separate non-overlapping patterns.");
|
|
2449
|
-
return applicable.some(Boolean);
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
};
|
|
2453
|
-
}) : [];
|
|
2454
|
-
const bunPatterns = pm === "bun" ? patterns.map((raw) => {
|
|
2455
|
-
const isExcluded = raw.startsWith("!");
|
|
2456
|
-
const pattern = stripTrailingWorkspaceSlashes(paths.normalizePattern(isExcluded ? raw.slice(1) : raw));
|
|
2457
|
-
const usesGlob = isExcluded || hasBasicWorkspaceWildcard(pattern) || pattern.includes("{") || pattern.includes("[");
|
|
2458
|
-
const glob = globs.compile(pattern, {
|
|
2459
|
-
...defaultWorkspaceGlobOptions,
|
|
2460
|
-
extendedPatterns: false
|
|
2461
|
-
});
|
|
2462
|
-
const subtree = isExcluded && pattern.endsWith("/**") ? globs.compile(pattern.slice(0, -3), {
|
|
2463
|
-
...defaultWorkspaceGlobOptions,
|
|
2464
|
-
extendedPatterns: false
|
|
2465
|
-
}) : void 0;
|
|
2466
|
-
return {
|
|
2467
|
-
isExcluded,
|
|
2468
|
-
usesGlob,
|
|
2469
|
-
glob,
|
|
2470
|
-
matches: (directory) => glob.matches(directory) || glob.matches(`${directory}/`) || subtree?.matches(directory) === true
|
|
2471
|
-
};
|
|
2472
|
-
}) : [];
|
|
2473
|
-
const bunOrdered = bunPatterns.filter(({ usesGlob }) => usesGlob);
|
|
2474
|
-
const bunLiterals = bunPatterns.filter(({ isExcluded, usesGlob }) => !isExcluded && !usesGlob);
|
|
2475
|
-
const bunTraversal = bunPatterns.filter(({ isExcluded }) => !isExcluded).map(({ glob }) => glob);
|
|
2476
|
-
const denoLiteral = pm === "deno" ? positive.filter((pattern) => !hasBasicWorkspaceWildcard(pattern)).map((pattern) => compile(pattern)) : [];
|
|
2477
|
-
if (definition.denoManifests && denoOrdered.findLast(({ glob }) => glob.matches("."))?.excluded === false) throw new ConfigError("deno.json#workspace: a Deno workspace cannot contain itself.");
|
|
2478
|
-
if (positive.length === 0 || positive.every((pattern) => pattern === ".")) return [];
|
|
2479
|
-
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
2480
|
-
const result = [];
|
|
2481
|
-
const pending = ["."];
|
|
2482
|
-
while (pending.length > 0) {
|
|
2483
|
-
const current = pending.pop();
|
|
2484
|
-
if (current === void 0) break;
|
|
2485
|
-
for (const name of readDirectories(current)) {
|
|
2486
|
-
if (name === ".git" || name === "node_modules") continue;
|
|
2487
|
-
const directory = asRelPath(current === "." ? name : `${current}/${name}`);
|
|
2488
|
-
if (pm === "bun" && name === "CMakeFiles" && !bunLiterals.some(({ glob }) => glob.matches(directory) || glob.canDescend(directory))) continue;
|
|
2489
|
-
const inVendor = skipVendor && (directory === "vendor" || directory.startsWith("vendor/"));
|
|
2490
|
-
if (inVendor && !denoLiteral.some((pattern) => pattern.matches(directory) || pattern.canDescend(directory))) continue;
|
|
2491
|
-
const isExcluded = pm !== "deno" && excluded.some(({ glob }) => glob.matches(directory) || glob.matches(`${directory}/`));
|
|
2492
|
-
if (excluded.some(({ subtree }) => subtree?.matches(directory))) continue;
|
|
2493
|
-
const lastDenoMatch = denoOrdered.findLast(({ glob }) => glob.matches(directory));
|
|
2494
|
-
const lastBunMatch = bunOrdered.findLast(({ matches }) => matches(directory));
|
|
2495
|
-
if (pm === "deno" ? denoLiteral.some((pattern) => pattern.matches(directory)) || !inVendor && lastDenoMatch?.excluded === false : pm === "bun" ? bunLiterals.some(({ glob }) => glob.matches(directory)) || lastBunMatch?.isExcluded === false : !isExcluded && included.some((pattern) => pattern.matches(directory))) result.push(directory);
|
|
2496
|
-
if ((pm === "bun" ? bunTraversal : inVendor ? denoLiteral : included).some((pattern) => pattern.canDescend(directory))) pending.push(directory);
|
|
2497
|
-
}
|
|
2498
|
-
}
|
|
2499
|
-
return result.sort();
|
|
2500
|
-
};
|
|
2501
|
-
//#endregion
|
|
2502
|
-
//#region src/application/workspace-contexts.ts
|
|
2503
|
-
/** Expand each manager's declarations into isolated publication contexts. */
|
|
2504
|
-
const collectWorkspaceMembers = (ctx, fs, pms, projectType, dependencies) => {
|
|
2505
|
-
const { codecFor, createRepoContext, paths } = dependencies;
|
|
2506
|
-
return pms.flatMap((pm) => {
|
|
2507
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2508
|
-
return workspaceDefinitions(ctx, pm, dependencies).flatMap((definition) => workspaceDirectories(ctx, fs, definition, pm, dependencies).flatMap((directory) => {
|
|
2509
|
-
const root = paths.resolve(ctx.root, directory);
|
|
2510
|
-
const manifest = paths.resolve(root, asRelPath("package.json"));
|
|
2511
|
-
const denoManifest = paths.resolve(root, asRelPath("deno.json"));
|
|
2512
|
-
const denoJsonc = paths.resolve(root, asRelPath("deno.jsonc"));
|
|
2513
|
-
const allowed = new Set(pm === "deno" ? [
|
|
2514
|
-
manifest,
|
|
2515
|
-
denoManifest,
|
|
2516
|
-
denoJsonc
|
|
2517
|
-
] : [manifest]);
|
|
2518
|
-
const memberFs = {
|
|
2519
|
-
exists: (file) => allowed.has(file) && fs.exists(file),
|
|
2520
|
-
readText: (file) => allowed.has(file) ? fs.readText(file) : void 0
|
|
2521
|
-
};
|
|
2522
|
-
try {
|
|
2523
|
-
const explicitMember = definition.patterns.some((pattern) => !pattern.startsWith("!") && !hasBasicWorkspaceWildcard(pattern) && stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern)) === directory);
|
|
2524
|
-
if (!definition.denoManifests && !fs.exists(manifest)) {
|
|
2525
|
-
if (pm === "deno" && explicitMember) throw new ConfigError("Declared npm workspace member has no package.json.");
|
|
2526
|
-
return [];
|
|
2527
|
-
}
|
|
2528
|
-
if (pm === "deno" && !fs.exists(denoManifest) && fs.exists(denoJsonc)) throw new ConfigError("deno.jsonc is not supported; use strict deno.json.");
|
|
2529
|
-
const memberCtx = createRepoContext(root, memberFs, projectType);
|
|
2530
|
-
if (!definition.denoManifests && !memberCtx.packageJson) return [];
|
|
2531
|
-
if (!memberCtx.packageJson && !memberCtx.exists(asRelPath("deno.json"))) {
|
|
2532
|
-
if (definition.denoManifests && explicitMember) throw new ConfigError("Declared Deno member has no deno.json or package.json.");
|
|
2533
|
-
return [];
|
|
2534
|
-
}
|
|
2535
|
-
if (pm === "deno") {
|
|
2536
|
-
if (createConfigParser(codecFor, memberCtx)(CONFIG_FILES.denoJson).workspace !== void 0 || memberCtx.packageJson?.workspaces !== void 0) throw new ConfigError("Nested workspace declarations are not supported by Deno.");
|
|
2537
|
-
}
|
|
2538
|
-
if (seen.has(directory)) return [];
|
|
2539
|
-
seen.add(directory);
|
|
2540
|
-
return [{
|
|
2541
|
-
directory,
|
|
2542
|
-
pm,
|
|
2543
|
-
ctx: memberCtx
|
|
2544
|
-
}];
|
|
2545
|
-
} catch (error) {
|
|
2546
|
-
if (error instanceof ConfigError) throw new ConfigError(`${directory}/${error.message}`);
|
|
2547
|
-
throw error;
|
|
2548
|
-
}
|
|
2549
|
-
}));
|
|
2550
|
-
});
|
|
2551
|
-
};
|
|
2552
|
-
//#endregion
|
|
2553
|
-
//#region src/domain/services/apply-config.ts
|
|
2404
|
+
//#region src/core/apply-config.ts
|
|
2554
2405
|
/** Select active rules and keep user severity choices as explicit runtime data. */
|
|
2555
2406
|
const applyConfig = (base, config) => {
|
|
2556
2407
|
const merged = [...base, ...config?.customRules ?? []];
|
|
@@ -2578,7 +2429,7 @@ const applyConfig = (base, config) => {
|
|
|
2578
2429
|
};
|
|
2579
2430
|
};
|
|
2580
2431
|
//#endregion
|
|
2581
|
-
//#region src/
|
|
2432
|
+
//#region src/core/detect-pms.ts
|
|
2582
2433
|
const registerSignals = (signals, pm, owner) => {
|
|
2583
2434
|
for (const file of signals) {
|
|
2584
2435
|
const prior = owner.get(file);
|
|
@@ -2610,24 +2461,25 @@ const detectPMs = (ctx) => {
|
|
|
2610
2461
|
return PMS.filter((pm) => found.has(pm));
|
|
2611
2462
|
};
|
|
2612
2463
|
//#endregion
|
|
2613
|
-
//#region src/
|
|
2464
|
+
//#region src/core/resolve-pms.ts
|
|
2614
2465
|
/** Apply the explicit selection or detection, then the configured allow-list. */
|
|
2615
2466
|
const resolvePMs = (ctx, opts) => {
|
|
2616
2467
|
const { pmOverride, allowed } = opts;
|
|
2617
2468
|
const detected = pmOverride ? [pmOverride] : detectPMs(ctx);
|
|
2618
2469
|
const pms = allowed ? detected.filter((pm) => allowed.includes(pm)) : detected;
|
|
2619
2470
|
if (pms.length > 0) return pms;
|
|
2471
|
+
if (opts.optional && detected.length === 0) return [];
|
|
2620
2472
|
if (!allowed) throw new UsageError(`No package manager detected. Pass --pm <${PMS.join("|")}> to be explicit.`);
|
|
2621
2473
|
if (pmOverride) throw new UsageError(`--pm ${pmOverride} is not in siro.config.ts pms (${allowed.join(", ")}). Adjust the config or the flag.`);
|
|
2622
2474
|
if (detected.length > 0) throw new UsageError(`Detected PMs (${detected.join(", ")}) do not match siro.config.ts pms (${allowed.join(", ")}). Adjust the config or remove the restriction.`);
|
|
2623
2475
|
throw new UsageError(`No package manager detected, and siro.config.ts restricts pms to ${allowed.join(", ")}. Adjust the config or remove the restriction.`);
|
|
2624
2476
|
};
|
|
2625
2477
|
//#endregion
|
|
2626
|
-
//#region src/
|
|
2478
|
+
//#region src/core/decide-severity.ts
|
|
2627
2479
|
/** Resolve the most specific severity signal for one finding. */
|
|
2628
2480
|
const decideSeverity = (status, binding, rule, userOverride) => userOverride ?? status.severity ?? binding.severity ?? rule.severity;
|
|
2629
2481
|
//#endregion
|
|
2630
|
-
//#region src/
|
|
2482
|
+
//#region src/core/render-version-note.ts
|
|
2631
2483
|
const buildVersionNoteParts = (versionNote) => {
|
|
2632
2484
|
const parts = [];
|
|
2633
2485
|
if (typeof versionNote.configAvailableSince !== "undefined") parts.push(`available since ${versionNote.configAvailableSince}`);
|
|
@@ -2642,130 +2494,375 @@ const renderVersionNoteMessage = (message, versionNote) => {
|
|
|
2642
2494
|
return `${message} (${parts.join("; ")})`;
|
|
2643
2495
|
};
|
|
2644
2496
|
//#endregion
|
|
2645
|
-
//#region src/
|
|
2497
|
+
//#region src/core/evaluate-binding.ts
|
|
2498
|
+
/** One response boundary for generic and PM-specific checks. Selection stays with callers. */
|
|
2499
|
+
const evaluateBinding = (repository, rule, binding, overrides, target) => {
|
|
2500
|
+
const response = binding.check({
|
|
2501
|
+
...repository.ctx,
|
|
2502
|
+
readConfig: repository.parseConfig,
|
|
2503
|
+
pmVersion: target?.version
|
|
2504
|
+
}, repository.parseConfig(binding.file));
|
|
2505
|
+
assertSynchronous(response, `Rule '${rule.id}' check`);
|
|
2506
|
+
if (!isCheckStatusShape(response)) throw new ConfigError(`Rule '${rule.id}' returned an invalid check result.`);
|
|
2507
|
+
return (response.state === "violations" ? response.violations : [response]).flatMap((status) => status.state !== "violation" ? [] : [{
|
|
2508
|
+
ruleId: rule.id,
|
|
2509
|
+
directory: ".",
|
|
2510
|
+
...target ? { pm: target.pm } : {},
|
|
2511
|
+
severity: decideSeverity(status, binding, rule, overrides?.get(rule.id)),
|
|
2512
|
+
message: renderVersionNoteMessage(status.message, binding.versionNote),
|
|
2513
|
+
file: status.file ?? binding.file?.path,
|
|
2514
|
+
docs: binding.docs ?? rule.docs,
|
|
2515
|
+
actual: status.actual,
|
|
2516
|
+
expected: status.expected,
|
|
2517
|
+
remediation: target ? guardRemediationAvailability(target.pm, target.version, status.remediation) : status.remediation
|
|
2518
|
+
}]);
|
|
2519
|
+
};
|
|
2520
|
+
//#endregion
|
|
2521
|
+
//#region src/core/run-lint.ts
|
|
2646
2522
|
const resolveBindingProjectType = (ctx, pm, parseConfig) => {
|
|
2647
2523
|
if (ctx.projectType !== void 0) return ctx.projectType;
|
|
2648
|
-
|
|
2649
|
-
return resolvePackageJsonProjectType(ctx);
|
|
2524
|
+
return pm === "deno" ? resolveDenoProjectType(ctx, parseConfig(CONFIG_FILES.denoJson)) : resolvePackageJsonProjectType(ctx);
|
|
2650
2525
|
};
|
|
2651
|
-
/**
|
|
2526
|
+
/** Select applicable PM bindings; response handling is shared with manifest checks. */
|
|
2652
2527
|
const runLint = (opts) => {
|
|
2653
|
-
const {
|
|
2528
|
+
const { repository, pms, ruleSet, severityOverrides } = opts;
|
|
2654
2529
|
const findings = [];
|
|
2655
|
-
const summary = {
|
|
2656
|
-
error: 0,
|
|
2657
|
-
info: 0,
|
|
2658
|
-
warn: 0
|
|
2659
|
-
};
|
|
2660
|
-
const parseConfig = createConfigParser(codecFor, ctx);
|
|
2661
2530
|
for (const rule of ruleSet) for (const pm of pms) {
|
|
2662
2531
|
const binding = rule.bindings[pm];
|
|
2663
|
-
if (!binding || rule.projectTypes && !rule.projectTypes.includes(resolveBindingProjectType(ctx, pm, parseConfig))) continue;
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2532
|
+
if (!binding || rule.projectTypes && !rule.projectTypes.includes(resolveBindingProjectType(repository.ctx, pm, repository.parseConfig))) continue;
|
|
2533
|
+
findings.push(...evaluateBinding(repository, rule, binding, severityOverrides, {
|
|
2534
|
+
pm,
|
|
2535
|
+
version: opts.pmVersions?.[pm]
|
|
2536
|
+
}));
|
|
2537
|
+
}
|
|
2538
|
+
return { findings };
|
|
2539
|
+
};
|
|
2540
|
+
//#endregion
|
|
2541
|
+
//#region src/core/contracts/deno-json.ts
|
|
2542
|
+
/** Validate consumed publication metadata, not Deno's entire versioned schema. */
|
|
2543
|
+
const validateDenoMetadata = (config) => {
|
|
2544
|
+
const name = getByPath(config, ["name"]);
|
|
2545
|
+
if (name != null && typeof name !== "string") throw new ConfigError("deno.json: name must be a string or null.");
|
|
2546
|
+
const publish = getByPath(config, ["publish"]);
|
|
2547
|
+
if (publish == null || typeof publish === "boolean") return;
|
|
2548
|
+
if (!isPlainRecord(publish)) throw new ConfigError("deno.json: publish must be a boolean, object or null.");
|
|
2549
|
+
const include = getByPath(publish, ["include"]);
|
|
2550
|
+
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.");
|
|
2551
|
+
};
|
|
2552
|
+
//#endregion
|
|
2553
|
+
//#region src/core/parse-config-file.ts
|
|
2554
|
+
const EMPTY_FILE = Object.freeze({});
|
|
2555
|
+
/** Create a lazy parser for one repository context; callers own its lifetime. */
|
|
2556
|
+
const createConfigParser = (codecFor, ctx, maxDepth = DEFAULT_SCAN_LIMITS.maxConfigDepth) => {
|
|
2557
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2558
|
+
return (file) => {
|
|
2559
|
+
if (file === void 0) return EMPTY_FILE;
|
|
2560
|
+
const cacheKey = `${file.kind}:${file.path}`;
|
|
2561
|
+
const cached = cache.get(cacheKey);
|
|
2562
|
+
if (cached !== void 0) return cached;
|
|
2563
|
+
const text = ctx.readText(file.path);
|
|
2564
|
+
if (text === void 0) {
|
|
2565
|
+
cache.set(cacheKey, EMPTY_FILE);
|
|
2566
|
+
return EMPTY_FILE;
|
|
2567
|
+
}
|
|
2568
|
+
const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
|
|
2569
|
+
checkConfigDepth(parsed, maxDepth);
|
|
2570
|
+
cache.set(cacheKey, parsed);
|
|
2571
|
+
return parsed;
|
|
2572
|
+
};
|
|
2573
|
+
};
|
|
2574
|
+
/** Pair the context with a fresh lazy parser; construction performs no reads. */
|
|
2575
|
+
const createRepositoryEvaluation = (ctx, codecFor, maxDepth = DEFAULT_SCAN_LIMITS.maxConfigDepth) => ({
|
|
2576
|
+
ctx,
|
|
2577
|
+
parseConfig: createConfigParser(codecFor, ctx, maxDepth)
|
|
2578
|
+
});
|
|
2579
|
+
//#endregion
|
|
2580
|
+
//#region src/core/discovery.ts
|
|
2581
|
+
/** One context per selected directory, shared by publication and installation passes. */
|
|
2582
|
+
const discover = (cwd, fs, excluded, projectType, dependencies, limits = DEFAULT_SCAN_LIMITS) => {
|
|
2583
|
+
const { paths, createRepoContext, codecFor } = dependencies;
|
|
2584
|
+
const pending = [asRelPath(".")];
|
|
2585
|
+
const directories = [];
|
|
2586
|
+
while (pending.length) {
|
|
2587
|
+
const directory = pending.pop();
|
|
2588
|
+
checkLimit("maxDirectories", directories.length + pending.length + 1, limits);
|
|
2589
|
+
checkLimit("maxDirectoryDepth", directory === "." ? 0 : directory.split("/").length, limits);
|
|
2590
|
+
const absolute = paths.resolve(cwd, directory);
|
|
2591
|
+
let repository;
|
|
2592
|
+
const manifests = [];
|
|
2593
|
+
try {
|
|
2594
|
+
repository = createRepositoryEvaluation(createRepoContext(absolute, fs, projectType), codecFor, limits.maxConfigDepth);
|
|
2595
|
+
const { ctx, parseConfig } = repository;
|
|
2596
|
+
checkConfigDepth(ctx.packageJson, limits.maxConfigDepth);
|
|
2597
|
+
if (ctx.packageJson !== void 0) manifests.push("package.json");
|
|
2598
|
+
if (ctx.readText(CONFIG_FILES.denoJson.path) !== void 0) {
|
|
2599
|
+
validateDenoMetadata(parseConfig(CONFIG_FILES.denoJson));
|
|
2600
|
+
manifests.push("deno.json");
|
|
2601
|
+
} else if (ctx.exists(asRelPath("deno.jsonc"))) throw new ConfigError("deno.jsonc is not supported; use strict deno.json.");
|
|
2602
|
+
} catch (error) {
|
|
2603
|
+
if (error instanceof ConfigError && directory !== ".") throw new ConfigError(`${directory}/${error.message}`);
|
|
2604
|
+
throw error;
|
|
2605
|
+
}
|
|
2606
|
+
directories.push({
|
|
2607
|
+
directory,
|
|
2608
|
+
repository,
|
|
2609
|
+
manifests
|
|
2610
|
+
});
|
|
2611
|
+
const names = fs.readDirectories(absolute);
|
|
2612
|
+
if (!Array.isArray(names)) throw new ConfigError(`${directory}: FileSystem.readDirectories must return a dense array.`);
|
|
2613
|
+
const children = /* @__PURE__ */ new Set();
|
|
2614
|
+
for (let i = 0; i < names.length; i += 1) {
|
|
2615
|
+
if (!Object.hasOwn(names, i)) throw new ConfigError(`${directory}: FileSystem.readDirectories must return a dense array.`);
|
|
2616
|
+
const child = paths.child(directory, names[i]);
|
|
2617
|
+
if (names[i] === ".git" || names[i] === "node_modules" || excluded(child) || children.has(child)) continue;
|
|
2618
|
+
checkLimit("maxDirectoryDepth", child.split("/").length, limits);
|
|
2619
|
+
checkLimit("maxDirectories", directories.length + pending.length + 1, limits);
|
|
2620
|
+
children.add(child);
|
|
2621
|
+
pending.push(child);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
return directories.sort((a, b) => a.directory < b.directory ? -1 : a.directory > b.directory ? 1 : 0);
|
|
2625
|
+
};
|
|
2626
|
+
//#endregion
|
|
2627
|
+
//#region src/core/manifest-checks.ts
|
|
2628
|
+
const manifestProjectType = (repository, file) => file === "deno.json" ? resolveDenoProjectType(repository.ctx, repository.parseConfig(CONFIG_FILES.denoJson)) : resolvePackageJsonProjectType(repository.ctx);
|
|
2629
|
+
const selectBinding = {
|
|
2630
|
+
"files-field": (file) => file === "deno.json" ? denoPublishBinding : packageJsonFilesBinding,
|
|
2631
|
+
"publish-access": (file, repository, targets) => {
|
|
2632
|
+
if (file !== "package.json") return void 0;
|
|
2633
|
+
return repository.ctx.packageJson?.publishConfig?.access === "private" && targets.length > 0 ? "pm" : publishAccessBinding;
|
|
2634
|
+
}
|
|
2635
|
+
};
|
|
2636
|
+
const checkManifest = (repository, file, targets, rules, overrides) => {
|
|
2637
|
+
const findings = [];
|
|
2638
|
+
const projectType = manifestProjectType(repository, file);
|
|
2639
|
+
const pms = targets.map((target) => target.pm);
|
|
2640
|
+
const pmVersions = Object.fromEntries(targets.map((target) => [target.pm, target.version]));
|
|
2641
|
+
for (const rule of rules) {
|
|
2642
|
+
if (rule.projectTypes && !rule.projectTypes.includes(projectType)) continue;
|
|
2643
|
+
if (rule.id === "unsupported-settings") findings.push(...runLint({
|
|
2644
|
+
repository,
|
|
2645
|
+
pms,
|
|
2646
|
+
pmVersions,
|
|
2647
|
+
ruleSet: [{
|
|
2648
|
+
...rule,
|
|
2649
|
+
bindings: createUnsupportedSettings((path) => path === file).bindings
|
|
2650
|
+
}],
|
|
2651
|
+
severityOverrides: overrides
|
|
2652
|
+
}).findings);
|
|
2653
|
+
else if (Object.hasOwn(selectBinding, rule.id)) {
|
|
2654
|
+
const binding = selectBinding[rule.id](file, repository, targets);
|
|
2655
|
+
if (binding === "pm") findings.push(...runLint({
|
|
2656
|
+
repository,
|
|
2657
|
+
pms,
|
|
2658
|
+
pmVersions,
|
|
2659
|
+
ruleSet: [rule],
|
|
2660
|
+
severityOverrides: overrides
|
|
2661
|
+
}).findings);
|
|
2662
|
+
else if (binding) findings.push(...evaluateBinding(repository, rule, binding, overrides));
|
|
2687
2663
|
}
|
|
2688
2664
|
}
|
|
2665
|
+
return findings;
|
|
2666
|
+
};
|
|
2667
|
+
//#endregion
|
|
2668
|
+
//#region src/core/rebase-finding.ts
|
|
2669
|
+
/** Context-local rule output becomes cwd-relative once, without mutating rule-owned data. */
|
|
2670
|
+
const rebaseFinding = (directory, finding) => {
|
|
2671
|
+
if (directory === ".") return {
|
|
2672
|
+
...finding,
|
|
2673
|
+
directory
|
|
2674
|
+
};
|
|
2675
|
+
const remediation = finding.remediation;
|
|
2676
|
+
const rebaseOperation = (operation) => ({
|
|
2677
|
+
...operation,
|
|
2678
|
+
file: {
|
|
2679
|
+
...operation.file,
|
|
2680
|
+
path: `${directory}/${operation.file.path}`
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2689
2683
|
return {
|
|
2690
|
-
|
|
2691
|
-
|
|
2684
|
+
...finding,
|
|
2685
|
+
directory,
|
|
2686
|
+
...finding.file === void 0 ? {} : { file: `${directory}/${finding.file}` },
|
|
2687
|
+
remediation: remediation?.kind === "automatic" ? {
|
|
2688
|
+
...remediation,
|
|
2689
|
+
operations: [rebaseOperation(remediation.operations[0]), ...remediation.operations.slice(1).map(rebaseOperation)]
|
|
2690
|
+
} : remediation?.kind === "manual" ? {
|
|
2691
|
+
...remediation,
|
|
2692
|
+
steps: [`Work in ${directory} for this finding.`, ...remediation.steps]
|
|
2693
|
+
} : remediation
|
|
2692
2694
|
};
|
|
2693
2695
|
};
|
|
2694
2696
|
//#endregion
|
|
2695
|
-
//#region src/
|
|
2696
|
-
/**
|
|
2697
|
-
const
|
|
2698
|
-
const
|
|
2697
|
+
//#region src/core/bounded-file-system.ts
|
|
2698
|
+
/** Bounds returned data from trusted injected IO; cannot bound allocations inside that IO. */
|
|
2699
|
+
const boundedFileSystem = (fs, limits) => {
|
|
2700
|
+
const texts = /* @__PURE__ */ new Map();
|
|
2701
|
+
let bytes = 0;
|
|
2702
|
+
let entries = 0;
|
|
2703
|
+
return {
|
|
2704
|
+
exists: (path) => fs.exists(path),
|
|
2705
|
+
readText(path) {
|
|
2706
|
+
if (!texts.has(path)) {
|
|
2707
|
+
const text = fs.readText(path);
|
|
2708
|
+
if (text !== void 0) {
|
|
2709
|
+
const size = utf8Bytes(text);
|
|
2710
|
+
checkLimit("maxFileBytes", size, limits);
|
|
2711
|
+
bytes += size;
|
|
2712
|
+
checkLimit("maxTotalBytes", bytes, limits);
|
|
2713
|
+
}
|
|
2714
|
+
texts.set(path, text);
|
|
2715
|
+
}
|
|
2716
|
+
return texts.get(path);
|
|
2717
|
+
},
|
|
2718
|
+
readDirectories(path) {
|
|
2719
|
+
const names = fs.readDirectories(path);
|
|
2720
|
+
if (Array.isArray(names)) {
|
|
2721
|
+
entries += names.length;
|
|
2722
|
+
checkLimit("maxEntries", entries, limits);
|
|
2723
|
+
}
|
|
2724
|
+
return names;
|
|
2725
|
+
}
|
|
2726
|
+
};
|
|
2727
|
+
};
|
|
2728
|
+
//#endregion
|
|
2729
|
+
//#region src/core/lint.ts
|
|
2730
|
+
const validateLintOptions = (options, paths) => {
|
|
2699
2731
|
if (!options || !paths.isAbsolute(options.cwd)) throw new UsageError("cwd must be an absolute filesystem path.");
|
|
2732
|
+
if ("workspaces" in options) throw new UsageError("workspaces was removed in 0.6.0. Package discovery is recursive by default; use exclude and installationRoots to specify scope.");
|
|
2700
2733
|
if ("customRules" in options || "reporters" in options) throw new UsageError("Pass customRules and reporters inside the config option.");
|
|
2701
2734
|
if (options.pm !== void 0 && !isPM(options.pm)) throw new UsageError(`Unknown package manager: ${String(options.pm)}`);
|
|
2702
2735
|
if (options.projectType !== void 0 && !isProjectType(options.projectType)) throw new UsageError(`Unknown project type: ${String(options.projectType)}`);
|
|
2703
2736
|
if (options.pmVersion !== void 0 && (!options.pm || !isStableVersion(options.pmVersion))) throw new UsageError("pmVersion / --pm-version requires pm / --pm and an exact stable version such as 10.16.0.");
|
|
2704
|
-
|
|
2737
|
+
};
|
|
2738
|
+
/** Validate inputs and prepare shared contexts, without running rules or child executable configs. */
|
|
2739
|
+
const prepareLint = (options, dependencies) => {
|
|
2740
|
+
validateLintOptions(options, dependencies.paths);
|
|
2705
2741
|
const config = options.config === void 0 ? void 0 : parseConfig(options.config);
|
|
2706
|
-
const
|
|
2707
|
-
const
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
if (pms.includes("deno") && !ctx.exists(asRelPath("deno.json")) && ctx.exists(asRelPath("deno.jsonc"))) throw new ConfigError("deno.jsonc is not supported. siro currently reads strict JSON from deno.json only.");
|
|
2742
|
+
const limits = resolveScanLimits(options.limits);
|
|
2743
|
+
const sourceFs = options.fs === void 0 ? dependencies.fileSystem : options.fs;
|
|
2744
|
+
if (typeof sourceFs.readDirectories !== "function") throw new ConfigError("FileSystem.readDirectories is required for package discovery; no native filesystem fallback is used.");
|
|
2745
|
+
const fs = boundedFileSystem(sourceFs, limits);
|
|
2746
|
+
const excluded = dependencies.compileExclusions(parseExcludes(options.exclude !== void 0 ? options.exclude : config?.exclude ?? []));
|
|
2747
|
+
const installations = parseInstallationRoots(options.installationRoots !== void 0 ? options.installationRoots : config?.installationRoots ?? ["."]);
|
|
2713
2748
|
const configured = applyConfig(dependencies.rules, config);
|
|
2749
|
+
const needsCustomTarget = configured.rules.some((rule) => scopeOf(rule.id) === "custom");
|
|
2750
|
+
const discovered = discover(options.cwd, fs, excluded, options.projectType ?? config?.projectType, dependencies, limits);
|
|
2751
|
+
for (const entry of installations) if (!discovered.some((item) => item.directory === entry.path)) throw new UsageError(`${entry.path}: installation root must be an existing, selected ordinary directory using its exact enumerated spelling (no symlinks, excluded or hard-skipped paths).`);
|
|
2714
2752
|
return {
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2753
|
+
evaluation: {
|
|
2754
|
+
directories: discovered.map((item) => {
|
|
2755
|
+
const { ctx } = item.repository;
|
|
2756
|
+
const entry = installations.find((root) => root.path === item.directory);
|
|
2757
|
+
let pms;
|
|
2758
|
+
let versions = declaredPMVersion(ctx.packageJson?.packageManager);
|
|
2759
|
+
if (item.directory === ".") {
|
|
2760
|
+
pms = resolvePMs(ctx, {
|
|
2761
|
+
allowed: config?.pms,
|
|
2762
|
+
pmOverride: options.pm,
|
|
2763
|
+
optional: !entry && !needsCustomTarget
|
|
2764
|
+
});
|
|
2765
|
+
versions = {
|
|
2766
|
+
...versions,
|
|
2767
|
+
...config?.pmVersions,
|
|
2768
|
+
...options.pm && options.pmVersion ? { [options.pm]: options.pmVersion } : {}
|
|
2769
|
+
};
|
|
2770
|
+
} else if (entry) {
|
|
2771
|
+
try {
|
|
2772
|
+
pms = resolvePMs(ctx, { pmOverride: entry.pm });
|
|
2773
|
+
} catch (error) {
|
|
2774
|
+
if (error instanceof UsageError) throw new UsageError(`${item.directory}: ${error.message}`);
|
|
2775
|
+
throw error;
|
|
2776
|
+
}
|
|
2777
|
+
if (entry.pm && entry.pmVersion) versions = {
|
|
2778
|
+
...versions,
|
|
2779
|
+
[entry.pm]: entry.pmVersion
|
|
2780
|
+
};
|
|
2781
|
+
} else {
|
|
2782
|
+
const declared = ctx.packageJson?.packageManager === void 0 ? void 0 : parsePackageManagerField(ctx.packageJson.packageManager);
|
|
2783
|
+
pms = PMS.filter((pm) => pm === declared || pm === "deno" && item.manifests.includes("deno.json"));
|
|
2784
|
+
}
|
|
2785
|
+
return {
|
|
2786
|
+
...item,
|
|
2787
|
+
installation: entry !== void 0,
|
|
2788
|
+
targets: pms.map((pm) => ({
|
|
2789
|
+
pm,
|
|
2790
|
+
...versions[pm] === void 0 ? {} : { version: versions[pm] }
|
|
2791
|
+
}))
|
|
2792
|
+
};
|
|
2793
|
+
}),
|
|
2794
|
+
ruleSet: configured.rules,
|
|
2795
|
+
severityOverrides: configured.severityOverrides,
|
|
2796
|
+
limits
|
|
2721
2797
|
},
|
|
2722
|
-
members: options.workspaces ? collectWorkspaceMembers(ctx, fs, pms, options.projectType ?? config?.projectType, dependencies) : [],
|
|
2723
|
-
ruleSet: configured.rules,
|
|
2724
|
-
severityOverrides: configured.severityOverrides,
|
|
2725
|
-
codecFor,
|
|
2726
2798
|
reporters: config?.reporters ?? []
|
|
2727
2799
|
};
|
|
2728
2800
|
};
|
|
2729
|
-
|
|
2730
|
-
const
|
|
2731
|
-
const
|
|
2732
|
-
const
|
|
2733
|
-
const
|
|
2734
|
-
const
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2801
|
+
const runPreparedLint = (evaluation) => {
|
|
2802
|
+
const { directories, ruleSet, severityOverrides, limits } = evaluation;
|
|
2803
|
+
const findings = [];
|
|
2804
|
+
const manifests = [];
|
|
2805
|
+
const installationRoots = [];
|
|
2806
|
+
const ruleOrder = new Map(ruleSet.map((rule, index) => [rule.id, index]));
|
|
2807
|
+
for (const item of directories) {
|
|
2808
|
+
const { directory, repository, targets, installation } = item;
|
|
2809
|
+
const local = [];
|
|
2810
|
+
for (const file of item.manifests) {
|
|
2811
|
+
const manifestTargets = targets.filter(({ pm }) => file === "deno.json" ? pm === "deno" : pm !== "deno");
|
|
2812
|
+
manifests.push({
|
|
2813
|
+
path: directory === "." ? file : `${directory}/${file}`,
|
|
2814
|
+
projectType: manifestProjectType(repository, file),
|
|
2815
|
+
targets: manifestTargets
|
|
2816
|
+
});
|
|
2817
|
+
local.push(...checkManifest(repository, file, manifestTargets, ruleSet, severityOverrides));
|
|
2818
|
+
}
|
|
2819
|
+
if (installation) installationRoots.push({
|
|
2820
|
+
directory,
|
|
2821
|
+
targets
|
|
2745
2822
|
});
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2823
|
+
const scopedRules = ruleSet.flatMap((rule) => {
|
|
2824
|
+
const scope = scopeOf(rule.id);
|
|
2825
|
+
if (scope === "installation" && installation || scope === "custom" && directory === ".") return [rule];
|
|
2826
|
+
if (scope === "split" && installation) return [{
|
|
2827
|
+
...rule,
|
|
2828
|
+
bindings: createUnsupportedSettings((file) => file !== "package.json" && file !== "deno.json").bindings
|
|
2829
|
+
}];
|
|
2830
|
+
return [];
|
|
2754
2831
|
});
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2832
|
+
try {
|
|
2833
|
+
local.push(...runLint({
|
|
2834
|
+
repository,
|
|
2835
|
+
pms: targets.map(({ pm }) => pm),
|
|
2836
|
+
pmVersions: Object.fromEntries(targets.map(({ pm, version }) => [pm, version])),
|
|
2837
|
+
ruleSet: scopedRules,
|
|
2838
|
+
severityOverrides
|
|
2839
|
+
}).findings);
|
|
2840
|
+
} catch (error) {
|
|
2841
|
+
if (error instanceof ConfigError && directory !== ".") throw new ConfigError(`${directory}/${error.message}`);
|
|
2842
|
+
throw error;
|
|
2843
|
+
}
|
|
2844
|
+
local.sort((a, b) => (ruleOrder.get(a.ruleId) ?? 0) - (ruleOrder.get(b.ruleId) ?? 0));
|
|
2845
|
+
checkLimit("maxFindings", findings.length + local.length, limits);
|
|
2846
|
+
for (const finding of local) findings.push(rebaseFinding(directory, finding));
|
|
2760
2847
|
}
|
|
2848
|
+
const summary = {
|
|
2849
|
+
error: 0,
|
|
2850
|
+
warn: 0,
|
|
2851
|
+
info: 0
|
|
2852
|
+
};
|
|
2853
|
+
for (const finding of findings) summary[finding.severity] += 1;
|
|
2761
2854
|
return {
|
|
2762
2855
|
findings,
|
|
2763
|
-
summary
|
|
2856
|
+
summary,
|
|
2857
|
+
inspection: {
|
|
2858
|
+
manifests,
|
|
2859
|
+
installationRoots
|
|
2860
|
+
}
|
|
2764
2861
|
};
|
|
2765
2862
|
};
|
|
2766
|
-
const lint$1 = (options, dependencies) => runPreparedLint(prepareLint(options, dependencies));
|
|
2863
|
+
const lint$1 = (options, dependencies) => runPreparedLint(prepareLint(options, dependencies).evaluation);
|
|
2767
2864
|
//#endregion
|
|
2768
|
-
//#region src/
|
|
2865
|
+
//#region src/core/filter.ts
|
|
2769
2866
|
const EXIT_SUCCESS = 0;
|
|
2770
2867
|
const EXIT_FAILURE = 1;
|
|
2771
2868
|
const meetsThreshold = (severity, threshold) => SEVERITY_RANK[severity] >= SEVERITY_RANK[threshold];
|
|
@@ -2782,6 +2879,7 @@ const filterBySeverity = (result, threshold) => {
|
|
|
2782
2879
|
summary[finding.severity] += 1;
|
|
2783
2880
|
}
|
|
2784
2881
|
return {
|
|
2882
|
+
...result,
|
|
2785
2883
|
findings,
|
|
2786
2884
|
summary
|
|
2787
2885
|
};
|
|
@@ -2795,7 +2893,7 @@ const exitCodeForLint = (result, threshold = "error") => {
|
|
|
2795
2893
|
return EXIT_SUCCESS;
|
|
2796
2894
|
};
|
|
2797
2895
|
//#endregion
|
|
2798
|
-
//#region src/
|
|
2896
|
+
//#region src/core/lint-command.ts
|
|
2799
2897
|
/** Evaluate and report. Executable config loading belongs to the CLI adapter. */
|
|
2800
2898
|
const lintCommand$1 = async (options, io, dependencies, reporters) => {
|
|
2801
2899
|
if (!options) throw new UsageError("Lint options are required.");
|
|
@@ -2805,23 +2903,58 @@ const lintCommand$1 = async (options, io, dependencies, reporters) => {
|
|
|
2805
2903
|
const selection = options.reporter ?? reporters.defaultName;
|
|
2806
2904
|
const reporter = typeof selection === "string" ? registry.get(selection) : selection;
|
|
2807
2905
|
if (!isReporterShape(reporter)) throw new UsageError(`${typeof selection === "string" ? "Unknown" : "Invalid"} reporter: ${String(selection)} (available: ${[...registry.keys()].join(", ")})`);
|
|
2808
|
-
const result = runPreparedLint(prepared);
|
|
2906
|
+
const result = runPreparedLint(prepared.evaluation);
|
|
2809
2907
|
const exitCode = exitCodeForLint(result, options.severity ?? "error");
|
|
2810
|
-
|
|
2908
|
+
const writes = [];
|
|
2909
|
+
const consumeOutput = outputBudget(prepared.evaluation.limits.maxOutputBytes);
|
|
2910
|
+
let outputFailed = false;
|
|
2911
|
+
let outputFailure;
|
|
2912
|
+
const recordFailure = (error) => {
|
|
2913
|
+
if (!outputFailed) {
|
|
2914
|
+
outputFailed = true;
|
|
2915
|
+
outputFailure = error;
|
|
2916
|
+
}
|
|
2917
|
+
};
|
|
2918
|
+
const track = (write) => (line) => {
|
|
2919
|
+
try {
|
|
2920
|
+
consumeOutput(`${line}\n`);
|
|
2921
|
+
const written = write(line);
|
|
2922
|
+
writes.push(Promise.resolve(written).then(() => {}, recordFailure));
|
|
2923
|
+
return written;
|
|
2924
|
+
} catch (error) {
|
|
2925
|
+
recordFailure(error);
|
|
2926
|
+
throw error;
|
|
2927
|
+
}
|
|
2928
|
+
};
|
|
2929
|
+
try {
|
|
2930
|
+
await reporter.format(filterBySeverity(result, options.severity ?? "info"), {
|
|
2931
|
+
stdout: track((line) => io.stdout(line)),
|
|
2932
|
+
stderr: track((line) => io.stderr(line))
|
|
2933
|
+
}, {
|
|
2934
|
+
cwd: options.cwd,
|
|
2935
|
+
limits: prepared.evaluation.limits
|
|
2936
|
+
});
|
|
2937
|
+
} finally {
|
|
2938
|
+
await Promise.all(writes);
|
|
2939
|
+
}
|
|
2940
|
+
if (outputFailed) throw outputFailure;
|
|
2811
2941
|
return exitCode;
|
|
2812
2942
|
};
|
|
2813
2943
|
//#endregion
|
|
2814
|
-
//#region src/
|
|
2944
|
+
//#region src/core/contracts/package-json.ts
|
|
2815
2945
|
const PackageJsonSchema = vb.looseObject({
|
|
2816
2946
|
files: vb.optional(vb.array(vb.string())),
|
|
2817
2947
|
name: vb.optional(vb.string()),
|
|
2818
2948
|
packageManager: vb.optional(vb.string()),
|
|
2819
2949
|
private: vb.optional(vb.boolean()),
|
|
2820
|
-
publishConfig: vb.optional(vb.looseObject({
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2950
|
+
publishConfig: vb.optional(vb.looseObject({
|
|
2951
|
+
access: vb.optional(vb.picklist([
|
|
2952
|
+
"public",
|
|
2953
|
+
"restricted",
|
|
2954
|
+
"private"
|
|
2955
|
+
])),
|
|
2956
|
+
provenance: vb.optional(vb.boolean())
|
|
2957
|
+
})),
|
|
2825
2958
|
trustedDependencies: vb.optional(vb.array(vb.string()))
|
|
2826
2959
|
});
|
|
2827
2960
|
const parsePackageJson = (value) => {
|
|
@@ -2833,26 +2966,29 @@ const parsePackageJson = (value) => {
|
|
|
2833
2966
|
throw new ConfigError(`package.json: ${field ? `${field}: ` : ""}${issue.message}`);
|
|
2834
2967
|
};
|
|
2835
2968
|
//#endregion
|
|
2836
|
-
//#region src/adapters/
|
|
2837
|
-
|
|
2969
|
+
//#region src/adapters/codecs/json.ts
|
|
2970
|
+
/** Strict JSON; runtime SyntaxError messages may disclose arbitrary source bytes. */
|
|
2971
|
+
const parseJson = (text, maxDepth = DEFAULT_SCAN_LIMITS.maxConfigDepth) => {
|
|
2972
|
+
let value;
|
|
2838
2973
|
try {
|
|
2839
|
-
|
|
2840
|
-
} catch
|
|
2841
|
-
|
|
2842
|
-
if (error instanceof Error) errMsg = error.message;
|
|
2843
|
-
throw new ConfigError(`package.json: invalid JSON — ${errMsg}`);
|
|
2974
|
+
value = JSON.parse(text.trim());
|
|
2975
|
+
} catch {
|
|
2976
|
+
throw new ConfigParseError("invalid JSON");
|
|
2844
2977
|
}
|
|
2978
|
+
checkConfigDepth(value, maxDepth);
|
|
2979
|
+
return value;
|
|
2845
2980
|
};
|
|
2846
|
-
const
|
|
2847
|
-
return
|
|
2848
|
-
};
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2981
|
+
const jsonCodec = { parse(text) {
|
|
2982
|
+
return toParsedConfig(parseJson(text));
|
|
2983
|
+
} };
|
|
2984
|
+
//#endregion
|
|
2985
|
+
//#region src/adapters/repo-context.ts
|
|
2986
|
+
/** Read caching and budgets belong to the scan's supplied filesystem. */
|
|
2987
|
+
const createRepoContext = (root, fs, projectType, maxDepth = DEFAULT_SCAN_LIMITS.maxConfigDepth) => {
|
|
2988
|
+
const readText = (relative) => fs.readText(nodePaths.resolve(root, relative));
|
|
2853
2989
|
const raw = readText(asRelPath("package.json"));
|
|
2854
|
-
|
|
2855
|
-
|
|
2990
|
+
const packageJson = raw === void 0 ? void 0 : parsePackageJson(wrapCodecError("package.json", () => parseJson(raw, maxDepth)));
|
|
2991
|
+
const exists = (relative) => fs.exists(nodePaths.resolve(root, relative));
|
|
2856
2992
|
return {
|
|
2857
2993
|
exists,
|
|
2858
2994
|
packageJson,
|
|
@@ -2875,37 +3011,71 @@ const coerce = (raw) => {
|
|
|
2875
3011
|
if (typeof raw !== "string") return raw;
|
|
2876
3012
|
return coerceBoolean(raw) ?? coerceInteger(raw) ?? raw;
|
|
2877
3013
|
};
|
|
3014
|
+
/**
|
|
3015
|
+
* Coerces unquoted scalars on read so rule checks can match by typed
|
|
3016
|
+
* value (`true === true`, `7 === 7`). Strings whose lexeme matches
|
|
3017
|
+
* `true`/`false`/`-?\d+` lose their string-ness — this is by design for
|
|
3018
|
+
* the npm schema, where rule values are booleans / integers / the empty
|
|
3019
|
+
* string. Don't model a customRule around a string like `"0"` on this
|
|
3020
|
+
* codec; pick yaml/json if you need lossless arbitrary-string reads.
|
|
3021
|
+
*/
|
|
3022
|
+
const iniCodec = { parse(text) {
|
|
3023
|
+
const raw = toParsedConfig(ini.parse(text));
|
|
3024
|
+
const config = {};
|
|
3025
|
+
for (const [key, value] of Object.entries(raw)) if (Array.isArray(value)) config[key] = value.map((elem) => coerce(elem));
|
|
3026
|
+
else if (typeof value === "object" && value !== null) config[key] = toParsedConfig(value);
|
|
3027
|
+
else config[key] = coerce(value);
|
|
3028
|
+
return config;
|
|
3029
|
+
} };
|
|
3030
|
+
//#endregion
|
|
3031
|
+
//#region src/adapters/codecs/toml.ts
|
|
3032
|
+
const tomlCodec = { parse(text) {
|
|
3033
|
+
if (text.trim() === "") return {};
|
|
3034
|
+
const input = text.startsWith("") ? text.slice(1) : text;
|
|
3035
|
+
let value;
|
|
3036
|
+
try {
|
|
3037
|
+
value = parse$1(input);
|
|
3038
|
+
} catch {
|
|
3039
|
+
throw new ConfigParseError("Invalid TOML syntax.");
|
|
3040
|
+
}
|
|
3041
|
+
return toParsedConfig(value);
|
|
3042
|
+
} };
|
|
3043
|
+
//#endregion
|
|
3044
|
+
//#region src/adapters/codecs/yaml.ts
|
|
3045
|
+
const parseYaml = (text, maxDepth = DEFAULT_SCAN_LIMITS.maxConfigDepth) => {
|
|
3046
|
+
if (text.trim() === "") return {};
|
|
3047
|
+
let value;
|
|
3048
|
+
try {
|
|
3049
|
+
const document = parseDocument(text, { prettyErrors: false });
|
|
3050
|
+
if (document.errors.length) throw new ConfigParseError("Invalid YAML syntax.");
|
|
3051
|
+
if (document.contents === null) return {};
|
|
3052
|
+
value = document.toJS();
|
|
3053
|
+
} catch (error) {
|
|
3054
|
+
if (error instanceof ConfigParseError) throw error;
|
|
3055
|
+
if (error instanceof Error && error.message.includes("Excessive alias count")) throw new ConfigParseError("Excessive alias count in YAML.");
|
|
3056
|
+
throw new ConfigParseError("Invalid YAML.");
|
|
3057
|
+
}
|
|
3058
|
+
try {
|
|
3059
|
+
checkConfigDepth(value, maxDepth);
|
|
3060
|
+
} catch (error) {
|
|
3061
|
+
if (error instanceof ConfigError) throw error;
|
|
3062
|
+
throw new ConfigParseError("Invalid YAML structure.");
|
|
3063
|
+
}
|
|
3064
|
+
return toParsedConfig(value);
|
|
3065
|
+
};
|
|
2878
3066
|
//#endregion
|
|
2879
3067
|
//#region src/adapters/codecs/store.ts
|
|
2880
3068
|
const CODECS = {
|
|
2881
|
-
json:
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
return config;
|
|
2892
|
-
} },
|
|
2893
|
-
toml: { parse(text) {
|
|
2894
|
-
if (text.trim() === "") return {};
|
|
2895
|
-
return toParsedConfig(parse$1(text.startsWith("") ? text.slice(1) : text));
|
|
2896
|
-
} },
|
|
2897
|
-
yaml: { parse(text) {
|
|
2898
|
-
if (text.trim() === "") return {};
|
|
2899
|
-
const document = parseDocument(text);
|
|
2900
|
-
if (document.errors.length > 0) throw document.errors[0];
|
|
2901
|
-
if (document.contents === null) return {};
|
|
2902
|
-
return toParsedConfig(document.toJS({ onAnchor: (value, count) => {
|
|
2903
|
-
if (count > 1) JSON.stringify(value);
|
|
2904
|
-
} }));
|
|
2905
|
-
} }
|
|
2906
|
-
};
|
|
2907
|
-
/** Look up the codec for a parseable kind. Total — every CodecKind has one. */
|
|
2908
|
-
const codecFor = (kind) => CODECS[kind];
|
|
3069
|
+
json: jsonCodec,
|
|
3070
|
+
npmrc: iniCodec,
|
|
3071
|
+
toml: tomlCodec,
|
|
3072
|
+
yaml: { parse: (text) => parseYaml(text) }
|
|
3073
|
+
};
|
|
3074
|
+
const createCodecFor = (limits) => (kind) => ({ parse(text) {
|
|
3075
|
+
const parsed = kind === "json" ? toParsedConfig(parseJson(text, limits.maxConfigDepth)) : kind === "yaml" ? parseYaml(text, limits.maxConfigDepth) : CODECS[kind].parse(text);
|
|
3076
|
+
checkConfigDepth(parsed, limits.maxConfigDepth);
|
|
3077
|
+
return parsed;
|
|
3078
|
+
} });
|
|
2909
3079
|
//#endregion
|
|
2910
3080
|
//#region src/adapters/reporters/github.ts
|
|
2911
3081
|
const COMMAND = {
|
|
@@ -2917,33 +3087,103 @@ const escapeData = (raw) => raw.replaceAll("%", "%25").replaceAll("\r", "%0D").r
|
|
|
2917
3087
|
const escapeProp = (raw) => escapeData(raw).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
2918
3088
|
/** Emit GitHub Actions workflow commands (annotations on PRs). */
|
|
2919
3089
|
const githubReporter = {
|
|
2920
|
-
format(result, io) {
|
|
3090
|
+
async format(result, io, context) {
|
|
3091
|
+
const consume = outputBudget(context.limits?.maxOutputBytes ?? DEFAULT_SCAN_LIMITS.maxOutputBytes);
|
|
2921
3092
|
for (const finding of result.findings) {
|
|
2922
3093
|
let file = "";
|
|
2923
|
-
if (finding.file) file = `file=${escapeProp(finding.file)},`;
|
|
2924
|
-
let body = `[${finding.pm}] ${finding.message}`;
|
|
3094
|
+
if (finding.file) file = `file=${escapeProp(path.resolve(context.cwd, finding.file))},`;
|
|
3095
|
+
let body = `[${finding.pm ?? "package"}] ${finding.directory}: ${finding.message}`;
|
|
2925
3096
|
if (finding.docs) body += ` (${finding.docs})`;
|
|
2926
|
-
|
|
3097
|
+
const line = `::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`;
|
|
3098
|
+
consume(`${line}\n`);
|
|
3099
|
+
await io.stdout(line);
|
|
2927
3100
|
}
|
|
2928
3101
|
},
|
|
2929
3102
|
name: "github"
|
|
2930
3103
|
};
|
|
2931
3104
|
//#endregion
|
|
3105
|
+
//#region src/adapters/safe-text.ts
|
|
3106
|
+
/** Visible escaping for untrusted human-facing text; never rewrite API/native paths. */
|
|
3107
|
+
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:");
|
|
3108
|
+
/** JSON syntax and decoded values stay identical; CI must not parse legacy commands in it. */
|
|
3109
|
+
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[");
|
|
3110
|
+
//#endregion
|
|
3111
|
+
//#region src/adapters/reporters/bounded-json.ts
|
|
3112
|
+
const omitted = (value) => value === void 0 || typeof value === "function" || typeof value === "symbol";
|
|
3113
|
+
/** Serialize incrementally into bounded chunks before allocating the final JSON document.
|
|
3114
|
+
* JSON primitives use the native encoder; containers preserve its own-key/array/toJSON semantics.
|
|
3115
|
+
*/
|
|
3116
|
+
const boundedJson = (input, maxBytes, maxDepth) => {
|
|
3117
|
+
const consume = outputBudget(maxBytes);
|
|
3118
|
+
const chunks = [];
|
|
3119
|
+
const active = /* @__PURE__ */ new Set();
|
|
3120
|
+
const emit = (text) => {
|
|
3121
|
+
const escaped = safeJsonText(text);
|
|
3122
|
+
consume(escaped);
|
|
3123
|
+
chunks.push(escaped);
|
|
3124
|
+
};
|
|
3125
|
+
const normalize = (value, key) => {
|
|
3126
|
+
if (value !== null && (typeof value === "object" || typeof value === "function" || typeof value === "bigint")) {
|
|
3127
|
+
const toJSON = value.toJSON;
|
|
3128
|
+
if (typeof toJSON === "function") value = Reflect.apply(toJSON, value, [key]);
|
|
3129
|
+
}
|
|
3130
|
+
if (types.isNumberObject(value)) return +value;
|
|
3131
|
+
if (types.isStringObject(value)) return `${value}`;
|
|
3132
|
+
if (types.isBooleanObject(value)) return Boolean.prototype.valueOf.call(value);
|
|
3133
|
+
if (types.isBigIntObject(value)) return BigInt.prototype.valueOf.call(value);
|
|
3134
|
+
return value;
|
|
3135
|
+
};
|
|
3136
|
+
const serialize = (value, depth) => {
|
|
3137
|
+
if (typeof value === "bigint") throw new TypeError("Do not know how to serialize a BigInt");
|
|
3138
|
+
if (value === null || typeof value !== "object") {
|
|
3139
|
+
emit(JSON.stringify(value) ?? "null");
|
|
3140
|
+
return;
|
|
3141
|
+
}
|
|
3142
|
+
if (active.has(value)) throw new TypeError("Cannot serialize a circular report.");
|
|
3143
|
+
if (depth > maxDepth) throw new TypeError("Report nesting exceeds the configured depth.");
|
|
3144
|
+
active.add(value);
|
|
3145
|
+
const array = Array.isArray(value);
|
|
3146
|
+
emit(array ? "[" : "{");
|
|
3147
|
+
let count = 0;
|
|
3148
|
+
const entry = (key, raw) => {
|
|
3149
|
+
const child = normalize(raw, key);
|
|
3150
|
+
if (!array && omitted(child)) return;
|
|
3151
|
+
emit(`${count++ === 0 ? "\n" : ",\n"}${" ".repeat(depth)}`);
|
|
3152
|
+
if (!array) {
|
|
3153
|
+
emit(JSON.stringify(key));
|
|
3154
|
+
emit(": ");
|
|
3155
|
+
}
|
|
3156
|
+
serialize(omitted(child) ? null : child, depth + 1);
|
|
3157
|
+
};
|
|
3158
|
+
if (array) {
|
|
3159
|
+
const length = value.length;
|
|
3160
|
+
for (let index = 0; index < length; index++) entry(String(index), value[index]);
|
|
3161
|
+
} else for (const key of Object.keys(value)) entry(key, value[key]);
|
|
3162
|
+
if (count) emit(`\n${" ".repeat(depth - 1)}`);
|
|
3163
|
+
emit(array ? "]" : "}");
|
|
3164
|
+
active.delete(value);
|
|
3165
|
+
};
|
|
3166
|
+
serialize(normalize(input, ""), 1);
|
|
3167
|
+
consume("\n");
|
|
3168
|
+
return chunks.join("");
|
|
3169
|
+
};
|
|
3170
|
+
//#endregion
|
|
2932
3171
|
//#region src/adapters/reporters/json.ts
|
|
2933
3172
|
/**
|
|
2934
3173
|
* Versioned machine-readable output — the public contract consumed by
|
|
2935
3174
|
* external fixers (see docs/json-output.md). Bump `schemaVersion` on any
|
|
2936
3175
|
* breaking shape change and update that doc in the same commit.
|
|
2937
3176
|
*/
|
|
2938
|
-
const SCHEMA_VERSION =
|
|
3177
|
+
const SCHEMA_VERSION = 3;
|
|
2939
3178
|
const jsonReporter = {
|
|
2940
|
-
format(result, io) {
|
|
2941
|
-
io.stdout(
|
|
3179
|
+
async format(result, io, context) {
|
|
3180
|
+
await io.stdout(boundedJson({
|
|
2942
3181
|
schemaVersion: SCHEMA_VERSION,
|
|
2943
3182
|
siroVersion: version,
|
|
2944
3183
|
findings: result.findings,
|
|
2945
|
-
summary: result.summary
|
|
2946
|
-
|
|
3184
|
+
summary: result.summary,
|
|
3185
|
+
inspection: result.inspection
|
|
3186
|
+
}, context.limits?.maxOutputBytes ?? DEFAULT_SCAN_LIMITS.maxOutputBytes, (context.limits?.maxConfigDepth ?? DEFAULT_SCAN_LIMITS.maxConfigDepth) + 16));
|
|
2947
3187
|
},
|
|
2948
3188
|
name: "json"
|
|
2949
3189
|
};
|
|
@@ -2970,12 +3210,11 @@ const colorSupportedNow = () => {
|
|
|
2970
3210
|
return pc.isColorSupported;
|
|
2971
3211
|
};
|
|
2972
3212
|
const renderFinding = (finding, ctx) => {
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
ctx.io.stdout(
|
|
2976
|
-
ctx.io.stdout(` ${
|
|
2977
|
-
|
|
2978
|
-
if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${finding.docs}`));
|
|
3213
|
+
const where = ctx.colors.dim(` (${safeText(finding.file ?? finding.directory)})`);
|
|
3214
|
+
ctx.io.stdout(`${ctx.tag[finding.severity](GLYPH[finding.severity])} [${finding.pm ?? "package"}] ${ctx.colors.bold(safeText(finding.ruleId))}${where}`);
|
|
3215
|
+
ctx.io.stdout(` ${safeText(finding.message)}`);
|
|
3216
|
+
for (const step of finding.remediation?.kind === "manual" ? finding.remediation.steps : []) ctx.io.stdout(` ↳ ${safeText(step)}`);
|
|
3217
|
+
if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${safeText(finding.docs)}`));
|
|
2979
3218
|
};
|
|
2980
3219
|
const buildRenderCtx = (io) => {
|
|
2981
3220
|
const colors = pc.createColors(colorSupportedNow());
|
|
@@ -2990,16 +3229,27 @@ const buildRenderCtx = (io) => {
|
|
|
2990
3229
|
};
|
|
2991
3230
|
};
|
|
2992
3231
|
const prettyReporter = {
|
|
2993
|
-
format(result, io) {
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3232
|
+
async format(result, io, context) {
|
|
3233
|
+
const lines = [];
|
|
3234
|
+
const consume = outputBudget(context.limits?.maxOutputBytes ?? DEFAULT_SCAN_LIMITS.maxOutputBytes);
|
|
3235
|
+
const collect = (line) => {
|
|
3236
|
+
consume(`${line}\n`);
|
|
3237
|
+
lines.push(line);
|
|
3238
|
+
};
|
|
3239
|
+
const ctx = buildRenderCtx({
|
|
3240
|
+
stdout: collect,
|
|
3241
|
+
stderr: collect
|
|
3242
|
+
});
|
|
3243
|
+
collect(`Inspection: ${result.inspection.manifests.length} manifests; installation roots: ${result.inspection.installationRoots.map((root) => safeText(root.directory)).join(", ") || "none"}.`);
|
|
3244
|
+
collect("Unknown PM/version targets have no availability assessment; installation scope is explicit.");
|
|
3245
|
+
if (result.findings.length === 0) collect(ctx.colors.green("✔ No security best-practice issues found."));
|
|
3246
|
+
else {
|
|
3247
|
+
for (const finding of result.findings) renderFinding(finding, ctx);
|
|
3248
|
+
const { error, warn, info } = result.summary;
|
|
3249
|
+
collect("");
|
|
3250
|
+
collect(`Summary: ${error} error, ${warn} warn, ${info} info`);
|
|
2998
3251
|
}
|
|
2999
|
-
|
|
3000
|
-
const { error, warn, info } = result.summary;
|
|
3001
|
-
io.stdout("");
|
|
3002
|
-
io.stdout(`Summary: ${error} error, ${warn} warn, ${info} info`);
|
|
3252
|
+
await io.stdout(lines.join("\n"));
|
|
3003
3253
|
},
|
|
3004
3254
|
name: "pretty"
|
|
3005
3255
|
};
|
|
@@ -3017,29 +3267,81 @@ const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
|
|
|
3017
3267
|
/** Later registrations replace earlier reporters with the same name. */
|
|
3018
3268
|
const createRegistry = (extras = []) => new Map([...BUILTINS, ...extras].map((reporter) => [reporter.name, reporter]));
|
|
3019
3269
|
//#endregion
|
|
3020
|
-
//#region src/
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3270
|
+
//#region src/runtime.ts
|
|
3271
|
+
/** Read time at evaluation, never at module initialization. */
|
|
3272
|
+
const rules = createBuiltinRules({
|
|
3273
|
+
now: () => Date.now(),
|
|
3274
|
+
parse: (value) => Date.parse(value)
|
|
3275
|
+
});
|
|
3276
|
+
const dependenciesFor = (options) => {
|
|
3277
|
+
if (!options || !nodePaths.isAbsolute(options.cwd)) throw new UsageError("cwd must be an absolute filesystem path.");
|
|
3278
|
+
const limits = resolveScanLimits(options.limits);
|
|
3279
|
+
if (options.rejectSymlinks !== void 0 && typeof options.rejectSymlinks !== "boolean") throw new UsageError("rejectSymlinks must be a boolean.");
|
|
3280
|
+
const native = options.fs === void 0 || options.fs === nodeFileSystem;
|
|
3281
|
+
if (options.rejectSymlinks && !native) throw new UsageError("rejectSymlinks requires the native filesystem; an injected FileSystem is trusted code.");
|
|
3282
|
+
if (native) assertDirectory(options.cwd);
|
|
3283
|
+
return {
|
|
3284
|
+
rules,
|
|
3285
|
+
fileSystem: native ? createNodeFileSystem(limits, options.rejectSymlinks ? options.cwd : void 0) : options.fs,
|
|
3286
|
+
paths: nodePaths,
|
|
3287
|
+
createRepoContext: (root, fs, projectType) => createRepoContext(root, fs, projectType, limits.maxConfigDepth),
|
|
3288
|
+
codecFor: createCodecFor(limits),
|
|
3289
|
+
compileExclusions
|
|
3290
|
+
};
|
|
3029
3291
|
};
|
|
3292
|
+
const nativeOptions = (options) => options?.fs === nodeFileSystem ? {
|
|
3293
|
+
...options,
|
|
3294
|
+
fs: void 0
|
|
3295
|
+
} : options;
|
|
3030
3296
|
/** Public Node API: callers can replace the filesystem without assembling the application. */
|
|
3031
|
-
const lint = (options) => lint$1(options,
|
|
3032
|
-
const lintCommand = (options, io) => lintCommand$1(options, io,
|
|
3297
|
+
const lint = (options) => lint$1(nativeOptions(options), dependenciesFor(options));
|
|
3298
|
+
const lintCommand = async (options, io) => lintCommand$1(nativeOptions(options), io, dependenciesFor(options), {
|
|
3033
3299
|
defaultName: DEFAULT_REPORTER_NAME,
|
|
3034
3300
|
createRegistry
|
|
3035
3301
|
});
|
|
3036
3302
|
//#endregion
|
|
3037
3303
|
//#region src/adapters/node-io.ts
|
|
3038
|
-
const
|
|
3039
|
-
|
|
3040
|
-
|
|
3304
|
+
const writer = (stream) => {
|
|
3305
|
+
let failure;
|
|
3306
|
+
const pending = /* @__PURE__ */ new Set();
|
|
3307
|
+
const fail = (cause) => {
|
|
3308
|
+
failure ??= new Error(`Output failed: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
3309
|
+
for (const reject of pending) reject(failure);
|
|
3310
|
+
pending.clear();
|
|
3311
|
+
return failure;
|
|
3312
|
+
};
|
|
3313
|
+
let observing = false;
|
|
3314
|
+
return (line) => {
|
|
3315
|
+
if (!observing) {
|
|
3316
|
+
observing = true;
|
|
3317
|
+
stream.on("error", fail);
|
|
3318
|
+
stream.on("close", () => fail(/* @__PURE__ */ new Error("Output stream closed.")));
|
|
3319
|
+
}
|
|
3320
|
+
return new Promise((resolve, reject) => {
|
|
3321
|
+
if (failure || stream.destroyed) {
|
|
3322
|
+
reject(failure ?? fail(/* @__PURE__ */ new Error("Output stream is destroyed.")));
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
pending.add(reject);
|
|
3326
|
+
try {
|
|
3327
|
+
stream.write(`${line}\n`, (error) => {
|
|
3328
|
+
pending.delete(reject);
|
|
3329
|
+
if (error) reject(fail(error));
|
|
3330
|
+
else resolve();
|
|
3331
|
+
});
|
|
3332
|
+
} catch (error) {
|
|
3333
|
+
fail(error);
|
|
3334
|
+
}
|
|
3335
|
+
});
|
|
3336
|
+
};
|
|
3041
3337
|
};
|
|
3338
|
+
/** Node-only construction; keep streams and their errors outside the core. */
|
|
3339
|
+
const createNodeIO = (stdout, stderr) => ({
|
|
3340
|
+
stdout: writer(stdout),
|
|
3341
|
+
stderr: writer(stderr)
|
|
3342
|
+
});
|
|
3343
|
+
const nodeIO = createNodeIO(process.stdout, process.stderr);
|
|
3042
3344
|
//#endregion
|
|
3043
|
-
export {
|
|
3345
|
+
export { nodeFileSystem as A, isSeverity as C, asAbsPath as D, version as E, SiroError as F, UsageError as I, DEFAULT_SCAN_LIMITS as M, resolveScanLimits as N, asRelPath as O, ConfigError as P, isPM as S, SUPPORTED_NODE_RANGE as T, PROJECT_TYPES as _, DEFAULT_REPORTER_NAME as a, PMS as b, jsonReporter as c, overrideBindings as d, requireConfigKey as f, getByPath as g, defineRule as h, BUILTIN_REPORTER_NAMES as i, isNodeError as j, assertDirectory 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, isSupportedNodeVersion as w, SEVERITIES as x, isStableVersion as y };
|
|
3044
3346
|
|
|
3045
|
-
//# sourceMappingURL=node-io-
|
|
3347
|
+
//# sourceMappingURL=node-io-BpAHyeBG.mjs.map
|