@pho9ubenaa/siro 0.5.0 → 0.5.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 +19 -0
- package/README.md +5 -0
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +44 -30
- package/dist/index.mjs +1 -1
- package/dist/{node-io-CaqobxYp.mjs → node-io-BDHg4J2w.mjs} +1602 -1530
- package/package.json +1 -1
|
@@ -4,12 +4,12 @@ import { pathToFileURL } from "node:url";
|
|
|
4
4
|
import satisfies from "semver/functions/satisfies.js";
|
|
5
5
|
import * as vb from "valibot";
|
|
6
6
|
import { lt, parse } from "semver";
|
|
7
|
-
import
|
|
7
|
+
import valid from "semver/functions/valid.js";
|
|
8
|
+
import { GLOBSTAR, Minimatch, braceExpand } from "minimatch";
|
|
8
9
|
import ini from "ini";
|
|
9
10
|
import { parse as parse$1 } from "smol-toml";
|
|
10
11
|
import { parseDocument } from "yaml";
|
|
11
|
-
import
|
|
12
|
-
import valid from "semver/functions/valid.js";
|
|
12
|
+
import pc from "picocolors";
|
|
13
13
|
//#region src/shared/errors.ts
|
|
14
14
|
var SiroError = class extends Error {
|
|
15
15
|
exitCode;
|
|
@@ -48,15 +48,22 @@ const wrapCodecError = (filePath, fn) => {
|
|
|
48
48
|
};
|
|
49
49
|
//#endregion
|
|
50
50
|
//#region src/shared/paths.ts
|
|
51
|
+
const isRelPath = (value) => typeof value === "string" && value.length > 0 && !value.includes("\0") && !/^[\\/]/u.test(value) && !/^[a-z]:/iu.test(value) && !value.split(/[\\/]/u).includes("..");
|
|
52
|
+
const asRelPath = (value) => {
|
|
53
|
+
if (!isRelPath(value)) throw new TypeError("Expected a repository-relative path without parent traversal.");
|
|
54
|
+
return value;
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/adapters/node-paths.ts
|
|
51
58
|
const isAbsPath = (value) => typeof value === "string" && !value.includes("\0") && path.isAbsolute(value);
|
|
52
|
-
const isRelPath = (value) => typeof value === "string" && value.length > 0 && !value.includes("\0") && !path.posix.isAbsolute(value) && !path.win32.isAbsolute(value) && !/^[a-z]:/iu.test(value) && !value.split(/[\\/]/u).includes("..");
|
|
53
59
|
const asAbsPath = (value) => {
|
|
54
60
|
if (!isAbsPath(value)) throw new TypeError("Expected an absolute filesystem path.");
|
|
55
61
|
return value;
|
|
56
62
|
};
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
const nodePaths = {
|
|
64
|
+
isAbsolute: isAbsPath,
|
|
65
|
+
resolve: (root, relative) => asAbsPath(path.join(root, asRelPath(relative))),
|
|
66
|
+
normalizePattern: (pattern) => path.posix.normalize(pattern)
|
|
60
67
|
};
|
|
61
68
|
//#endregion
|
|
62
69
|
//#region src/adapters/node-errors.ts
|
|
@@ -108,16 +115,20 @@ const nodeFileSystem = {
|
|
|
108
115
|
}
|
|
109
116
|
}
|
|
110
117
|
};
|
|
111
|
-
const resolveIn = (root, relPath) =>
|
|
118
|
+
const resolveIn = (root, relPath) => nodePaths.resolve(root, relPath);
|
|
112
119
|
const assertDirectory = (root) => {
|
|
113
120
|
if (!statSync(root).isDirectory()) throw new UsageError("The lint target must be a directory.");
|
|
114
121
|
};
|
|
115
122
|
//#endregion
|
|
116
123
|
//#region package.json
|
|
117
|
-
var version$1 = "0.5.
|
|
124
|
+
var version$1 = "0.5.1";
|
|
125
|
+
var engines = { "node": "^22.18.0 || ^24.0.0" };
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/version.ts
|
|
128
|
+
const version = version$1;
|
|
129
|
+
const SUPPORTED_NODE_RANGE = engines.node;
|
|
118
130
|
//#endregion
|
|
119
|
-
//#region src/
|
|
120
|
-
const SUPPORTED_NODE_RANGE = { "node": "^22.18.0 || ^24.0.0" }.node;
|
|
131
|
+
//#region src/adapters/node-version.ts
|
|
121
132
|
const isSupportedNodeVersion = (version) => satisfies(version, SUPPORTED_NODE_RANGE);
|
|
122
133
|
//#endregion
|
|
123
134
|
//#region src/domain/entities/pms.ts
|
|
@@ -200,14 +211,16 @@ const toParsedConfig = (value) => {
|
|
|
200
211
|
throw new TypeError("Config root must be a mapping.");
|
|
201
212
|
};
|
|
202
213
|
//#endregion
|
|
203
|
-
//#region src/domain/entities/
|
|
204
|
-
const defineRule = (rule) => rule;
|
|
205
|
-
const isOptionalString = (value) => typeof value === "undefined" || typeof value === "string";
|
|
214
|
+
//#region src/domain/entities/config-file-ref.ts
|
|
206
215
|
const CONFIG_FILE_KINDS = new Set(CODEC_KINDS);
|
|
207
216
|
const isConfigFileRefShape = (value) => {
|
|
208
217
|
if (!isPlainRecord(value) || typeof value.kind !== "string" || !isRelPath(value.path)) return false;
|
|
209
218
|
return CONFIG_FILE_KINDS.has(value.kind);
|
|
210
219
|
};
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/domain/entities/rule.ts
|
|
222
|
+
const defineRule = (rule) => rule;
|
|
223
|
+
const isOptionalString = (value) => typeof value === "undefined" || typeof value === "string";
|
|
211
224
|
const isConfigValueShape = (value) => typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value);
|
|
212
225
|
const isDenseStringArray = (value) => Array.isArray(value) && Array.from(value).every((item) => typeof item === "string");
|
|
213
226
|
const isCheckStatusShape = (value) => {
|
|
@@ -252,7 +265,7 @@ const declaredPMVersion = (declaration) => {
|
|
|
252
265
|
return pm && isPM(pm) && isStableVersion(version) && extra.length === 0 ? { [pm]: version } : {};
|
|
253
266
|
};
|
|
254
267
|
//#endregion
|
|
255
|
-
//#region src/
|
|
268
|
+
//#region src/domain/services/parse-siro-config.ts
|
|
256
269
|
const RuleSettingSchema = vb.union([vb.picklist(SEVERITIES), vb.literal("off")]);
|
|
257
270
|
const ConfigSchema = vb.strictObject({
|
|
258
271
|
customRules: vb.optional(vb.array(vb.custom(isRuleShape, "must be a structurally valid rule"))),
|
|
@@ -315,149 +328,6 @@ const loadConfig = async (cwd, nodeVersion = process.versions.node) => {
|
|
|
315
328
|
return parseConfig(mod !== null && typeof mod === "object" && "default" in mod ? mod.default : mod, name);
|
|
316
329
|
};
|
|
317
330
|
//#endregion
|
|
318
|
-
//#region src/domain/services/filter.ts
|
|
319
|
-
const EXIT_SUCCESS = 0;
|
|
320
|
-
const EXIT_FAILURE = 1;
|
|
321
|
-
const meetsThreshold = (severity, threshold) => SEVERITY_RANK[severity] >= SEVERITY_RANK[threshold];
|
|
322
|
-
/** Keep only findings at or above `threshold`, recomputing the summary. */
|
|
323
|
-
const filterBySeverity = (result, threshold) => {
|
|
324
|
-
const findings = [];
|
|
325
|
-
const summary = {
|
|
326
|
-
error: 0,
|
|
327
|
-
info: 0,
|
|
328
|
-
warn: 0
|
|
329
|
-
};
|
|
330
|
-
for (const finding of result.findings) if (meetsThreshold(finding.severity, threshold)) {
|
|
331
|
-
findings.push(finding);
|
|
332
|
-
summary[finding.severity] += 1;
|
|
333
|
-
}
|
|
334
|
-
return {
|
|
335
|
-
findings,
|
|
336
|
-
summary
|
|
337
|
-
};
|
|
338
|
-
};
|
|
339
|
-
/**
|
|
340
|
-
* Exit code for a lint run. Non-zero when any finding meets `threshold`
|
|
341
|
-
* (default: only `error` fails the run).
|
|
342
|
-
*/
|
|
343
|
-
const exitCodeForLint = (result, threshold = "error") => {
|
|
344
|
-
if (result.findings.some((finding) => meetsThreshold(finding.severity, threshold))) return EXIT_FAILURE;
|
|
345
|
-
return EXIT_SUCCESS;
|
|
346
|
-
};
|
|
347
|
-
//#endregion
|
|
348
|
-
//#region src/adapters/reporters/github.ts
|
|
349
|
-
const COMMAND = {
|
|
350
|
-
error: "error",
|
|
351
|
-
info: "notice",
|
|
352
|
-
warn: "warning"
|
|
353
|
-
};
|
|
354
|
-
const escapeData = (raw) => raw.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
355
|
-
const escapeProp = (raw) => escapeData(raw).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
356
|
-
/** Emit GitHub Actions workflow commands (annotations on PRs). */
|
|
357
|
-
const githubReporter = {
|
|
358
|
-
format(result, io) {
|
|
359
|
-
for (const finding of result.findings) {
|
|
360
|
-
let file = "";
|
|
361
|
-
if (finding.file) file = `file=${escapeProp(finding.file)},`;
|
|
362
|
-
let body = `[${finding.pm}] ${finding.message}`;
|
|
363
|
-
if (finding.docs) body += ` (${finding.docs})`;
|
|
364
|
-
io.stdout(`::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`);
|
|
365
|
-
}
|
|
366
|
-
},
|
|
367
|
-
name: "github"
|
|
368
|
-
};
|
|
369
|
-
//#endregion
|
|
370
|
-
//#region src/version.ts
|
|
371
|
-
const version = version$1;
|
|
372
|
-
//#endregion
|
|
373
|
-
//#region src/adapters/reporters/json.ts
|
|
374
|
-
/**
|
|
375
|
-
* Versioned machine-readable output — the public contract consumed by
|
|
376
|
-
* external fixers (see docs/json-output.md). Bump `schemaVersion` on any
|
|
377
|
-
* breaking shape change and update that doc in the same commit.
|
|
378
|
-
*/
|
|
379
|
-
const SCHEMA_VERSION = 2;
|
|
380
|
-
const jsonReporter = {
|
|
381
|
-
format(result, io) {
|
|
382
|
-
io.stdout(JSON.stringify({
|
|
383
|
-
schemaVersion: SCHEMA_VERSION,
|
|
384
|
-
siroVersion: version,
|
|
385
|
-
findings: result.findings,
|
|
386
|
-
summary: result.summary
|
|
387
|
-
}, void 0, 2));
|
|
388
|
-
},
|
|
389
|
-
name: "json"
|
|
390
|
-
};
|
|
391
|
-
//#endregion
|
|
392
|
-
//#region src/adapters/reporters/pretty.ts
|
|
393
|
-
const GLYPH = {
|
|
394
|
-
error: "✖ error",
|
|
395
|
-
info: "ℹ info",
|
|
396
|
-
warn: "⚠ warn"
|
|
397
|
-
};
|
|
398
|
-
/**
|
|
399
|
-
* Decide colour at call time, not at import time.
|
|
400
|
-
*
|
|
401
|
-
* picocolors snapshots `isColorSupported` when imported, so a later
|
|
402
|
-
* `process.env.NO_COLOR = '1'` (or a different stdout) is ignored. The
|
|
403
|
-
* reporter is invoked per `siro lint` run from many contexts (TTY, CI
|
|
404
|
-
* with FORCE_COLOR, redirected output, …) — checking each call is the
|
|
405
|
-
* only way NO_COLOR can truly win, as https://no-color.org/ requires.
|
|
406
|
-
*/
|
|
407
|
-
const colorSupportedNow = () => {
|
|
408
|
-
const { env } = process;
|
|
409
|
-
if (typeof env.NO_COLOR !== "undefined" && env.NO_COLOR !== "") return false;
|
|
410
|
-
if ("FORCE_COLOR" in env) return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "";
|
|
411
|
-
return pc.isColorSupported;
|
|
412
|
-
};
|
|
413
|
-
const renderFinding = (finding, ctx) => {
|
|
414
|
-
let where = "";
|
|
415
|
-
if (finding.file) where = ctx.colors.dim(` (${finding.file})`);
|
|
416
|
-
ctx.io.stdout(`${ctx.tag[finding.severity](GLYPH[finding.severity])} [${finding.pm}] ${ctx.colors.bold(finding.ruleId)}${where}`);
|
|
417
|
-
ctx.io.stdout(` ${finding.message}`);
|
|
418
|
-
for (const step of finding.remediation?.kind === "manual" ? finding.remediation.steps : []) ctx.io.stdout(` ↳ ${step}`);
|
|
419
|
-
if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${finding.docs}`));
|
|
420
|
-
};
|
|
421
|
-
const buildRenderCtx = (io) => {
|
|
422
|
-
const colors = pc.createColors(colorSupportedNow());
|
|
423
|
-
return {
|
|
424
|
-
colors,
|
|
425
|
-
io,
|
|
426
|
-
tag: {
|
|
427
|
-
error: colors.red,
|
|
428
|
-
info: colors.cyan,
|
|
429
|
-
warn: colors.yellow
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
};
|
|
433
|
-
const prettyReporter = {
|
|
434
|
-
format(result, io) {
|
|
435
|
-
const ctx = buildRenderCtx(io);
|
|
436
|
-
if (result.findings.length === 0) {
|
|
437
|
-
io.stdout(ctx.colors.green("✔ No security best-practice issues found."));
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
for (const finding of result.findings) renderFinding(finding, ctx);
|
|
441
|
-
const { error, warn, info } = result.summary;
|
|
442
|
-
io.stdout("");
|
|
443
|
-
io.stdout(`Summary: ${error} error, ${warn} warn, ${info} info`);
|
|
444
|
-
},
|
|
445
|
-
name: "pretty"
|
|
446
|
-
};
|
|
447
|
-
//#endregion
|
|
448
|
-
//#region src/adapters/reporters/registry.ts
|
|
449
|
-
const BUILTINS = [
|
|
450
|
-
prettyReporter,
|
|
451
|
-
jsonReporter,
|
|
452
|
-
githubReporter
|
|
453
|
-
];
|
|
454
|
-
const DEFAULT_REPORTER_NAME = prettyReporter.name;
|
|
455
|
-
const JSON_REPORTER_NAME = jsonReporter.name;
|
|
456
|
-
/** Names of every built-in reporter, in stable display order. */
|
|
457
|
-
const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
|
|
458
|
-
/** Later registrations replace earlier reporters with the same name. */
|
|
459
|
-
const createRegistry = (extras = []) => new Map([...BUILTINS, ...extras].map((reporter) => [reporter.name, reporter]));
|
|
460
|
-
//#endregion
|
|
461
331
|
//#region src/domain/entities/config-files.ts
|
|
462
332
|
/** Known configuration locations and their parsers. */
|
|
463
333
|
const CONFIG_FILES = {
|
|
@@ -491,1304 +361,802 @@ const CONFIG_FILES = {
|
|
|
491
361
|
}
|
|
492
362
|
};
|
|
493
363
|
//#endregion
|
|
494
|
-
//#region src/domain/
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
"
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
364
|
+
//#region src/domain/setting-availability.ts
|
|
365
|
+
const { npmrc: npmrc$7, packageJson: packageJson$2, pnpmWorkspace: pnpmWorkspace$11, yarnrc: yarnrc$10, bunfig: bunfig$6 } = CONFIG_FILES;
|
|
366
|
+
const settingAvailability = [
|
|
367
|
+
{
|
|
368
|
+
pm: "npm",
|
|
369
|
+
file: npmrc$7,
|
|
370
|
+
keyPath: ["provenance"],
|
|
371
|
+
since: "9.5.0",
|
|
372
|
+
source: "https://github.com/npm/cli/releases/tag/v9.5.0"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
pm: "npm",
|
|
376
|
+
file: packageJson$2,
|
|
377
|
+
keyPath: ["publishConfig", "provenance"],
|
|
378
|
+
since: "9.5.0",
|
|
379
|
+
source: "https://github.com/npm/cli/releases/tag/v9.5.0"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
pm: "npm",
|
|
383
|
+
file: npmrc$7,
|
|
384
|
+
keyPath: ["min-release-age"],
|
|
385
|
+
since: "11.10.0",
|
|
386
|
+
source: "https://github.com/npm/cli/releases/tag/v11.10.0"
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
pm: "pnpm",
|
|
390
|
+
file: pnpmWorkspace$11,
|
|
391
|
+
keyPath: ["strictDepBuilds"],
|
|
392
|
+
since: "10.6.0",
|
|
393
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.6.0"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
pm: "pnpm",
|
|
397
|
+
file: pnpmWorkspace$11,
|
|
398
|
+
keyPath: ["dangerouslyAllowAllBuilds"],
|
|
399
|
+
since: "10.9.0",
|
|
400
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.9.0"
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
pm: "pnpm",
|
|
404
|
+
file: pnpmWorkspace$11,
|
|
405
|
+
keyPath: ["minimumReleaseAge"],
|
|
406
|
+
since: "10.16.0",
|
|
407
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.16.0"
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
pm: "pnpm",
|
|
411
|
+
file: pnpmWorkspace$11,
|
|
412
|
+
keyPath: ["minimumReleaseAgeExclude"],
|
|
413
|
+
since: "10.16.0",
|
|
414
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.16.0"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
pm: "pnpm",
|
|
418
|
+
file: pnpmWorkspace$11,
|
|
419
|
+
keyPath: ["trustPolicy"],
|
|
420
|
+
since: "10.21.0",
|
|
421
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.21.0"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
pm: "pnpm",
|
|
425
|
+
file: pnpmWorkspace$11,
|
|
426
|
+
keyPath: ["blockExoticSubdeps"],
|
|
427
|
+
since: "10.26.0",
|
|
428
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v10.26.0"
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
pm: "pnpm",
|
|
432
|
+
file: pnpmWorkspace$11,
|
|
433
|
+
keyPath: ["frozenStore"],
|
|
434
|
+
since: "11.7.0",
|
|
435
|
+
source: "https://github.com/pnpm/pnpm/releases/tag/v11.7.0"
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
pm: "yarn",
|
|
439
|
+
file: yarnrc$10,
|
|
440
|
+
keyPath: ["enableHardenedMode"],
|
|
441
|
+
since: "4.0.0",
|
|
442
|
+
source: "https://yarnpkg.com/blog/release/4.0"
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
pm: "yarn",
|
|
446
|
+
file: yarnrc$10,
|
|
447
|
+
keyPath: ["npmMinimalAgeGate"],
|
|
448
|
+
since: "4.10.0",
|
|
449
|
+
source: "https://github.com/yarnpkg/berry/releases/tag/@yarnpkg/cli/4.10.0"
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
pm: "yarn",
|
|
453
|
+
file: yarnrc$10,
|
|
454
|
+
keyPath: ["npmPreapprovedPackages"],
|
|
455
|
+
since: "4.10.0",
|
|
456
|
+
source: "https://github.com/yarnpkg/berry/releases/tag/@yarnpkg/cli/4.10.0"
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
pm: "bun",
|
|
460
|
+
file: bunfig$6,
|
|
461
|
+
keyPath: ["install", "minimumReleaseAge"],
|
|
462
|
+
since: "1.3.0",
|
|
463
|
+
source: "https://bun.com/blog/bun-v1.3#minimum-release-age"
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
pm: "bun",
|
|
467
|
+
file: bunfig$6,
|
|
468
|
+
keyPath: [
|
|
469
|
+
"install",
|
|
470
|
+
"security",
|
|
471
|
+
"scanner"
|
|
472
|
+
],
|
|
473
|
+
since: "1.2.21",
|
|
474
|
+
source: "https://bun.com/blog/bun-v1.2.21#security-scanner-api-for-bun-install"
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
pm: "deno",
|
|
478
|
+
file: npmrc$7,
|
|
479
|
+
keyPath: ["min-release-age"],
|
|
480
|
+
since: "2.8.1",
|
|
481
|
+
source: "https://github.com/denoland/deno/releases/tag/v2.8.1"
|
|
590
482
|
}
|
|
483
|
+
];
|
|
484
|
+
//#endregion
|
|
485
|
+
//#region src/domain/services/remediation-availability.ts
|
|
486
|
+
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
|
+
/** Undefined means the target version or the setting's introduction is unknown. */
|
|
488
|
+
const settingSupportedByTarget = (pm, version, target) => {
|
|
489
|
+
if (version === void 0) return void 0;
|
|
490
|
+
const availability = settingAvailability.find((setting) => setting.pm === pm && sameSetting(setting, target));
|
|
491
|
+
return availability === void 0 ? void 0 : !lt(version, availability.since);
|
|
591
492
|
};
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const
|
|
596
|
-
if (
|
|
597
|
-
const readText = (relPath) => fs.readText(resolveIn(root, relPath));
|
|
598
|
-
const exists = (relPath) => fs.exists(resolveIn(root, relPath));
|
|
599
|
-
const raw = readText(asRelPath("package.json"));
|
|
600
|
-
let packageJson = void 0;
|
|
601
|
-
if (typeof raw !== "undefined") packageJson = readPackageJson(raw);
|
|
493
|
+
/** Keep a proposal atomic and make known version prerequisites explicit. */
|
|
494
|
+
const guardRemediationAvailability = (pm, version, remediation, targets = remediation?.kind === "automatic" ? remediation.operations : []) => {
|
|
495
|
+
if (version === void 0 || remediation === void 0) return remediation;
|
|
496
|
+
const unsupported = settingAvailability.filter((setting) => setting.pm === pm && lt(version, setting.since) && targets.some((target) => sameSetting(setting, target)));
|
|
497
|
+
if (unsupported.length === 0) return remediation;
|
|
602
498
|
return {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
projectType,
|
|
606
|
-
readText,
|
|
607
|
-
root
|
|
499
|
+
kind: "manual",
|
|
500
|
+
steps: [`Target ${pm} ${version} does not support this proposal: ${unsupported.map((setting) => `${setting.file.path}#${setting.keyPath.join(".")} requires ${pm} >=${setting.since}`).join("; ")}. Verify and upgrade the target before applying the steps below, or choose a supported security control. Removing a setting alone does not provide its protection.`, ...remediation.kind === "manual" ? remediation.steps : remediation.operations.map((operation) => `After upgrading, set ${operation.file.path}#${operation.keyPath.join(".")} to ${JSON.stringify(operation.value)}.`)]
|
|
608
501
|
};
|
|
609
502
|
};
|
|
610
503
|
//#endregion
|
|
611
|
-
//#region src/
|
|
612
|
-
/**
|
|
613
|
-
const
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
attempted = true;
|
|
624
|
-
let current = ".";
|
|
625
|
-
for (const part of prefix) {
|
|
626
|
-
const child = resolveChild(current, part);
|
|
627
|
-
if (child === void 0) return void 0;
|
|
628
|
-
current = current === "." ? child : `${current}/${child}`;
|
|
629
|
-
}
|
|
630
|
-
resolved = current;
|
|
504
|
+
//#region src/domain/rules/remediation.ts
|
|
505
|
+
/** Scalar writes must not discard an existing container or replace a non-object parent. */
|
|
506
|
+
const proposeChanges = (config, operations) => {
|
|
507
|
+
for (const operation of operations) {
|
|
508
|
+
let current = config;
|
|
509
|
+
for (const key of operation.keyPath) {
|
|
510
|
+
if (current === void 0) break;
|
|
511
|
+
if (!isPlainRecord(current)) return {
|
|
512
|
+
kind: "manual",
|
|
513
|
+
steps: [`Review ${operation.file.path}: ${operation.keyPath.join(".")} has a non-object parent. Restructure it before setting the value to ${JSON.stringify(operation.value)}.`]
|
|
514
|
+
};
|
|
515
|
+
current = Object.hasOwn(current, key) ? current[key] : void 0;
|
|
631
516
|
}
|
|
632
|
-
return
|
|
633
|
-
|
|
634
|
-
|
|
517
|
+
if (current !== null && typeof current === "object") return {
|
|
518
|
+
kind: "manual",
|
|
519
|
+
steps: [`Review ${operation.file.path}: ${operation.keyPath.join(".")} contains nested settings. Preserve the required entries when correcting this value.`]
|
|
520
|
+
};
|
|
521
|
+
}
|
|
635
522
|
return {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const actual = resolve();
|
|
639
|
-
const rewritten = actual === void 0 ? void 0 : rewrite(directory, actual);
|
|
640
|
-
return rewritten !== void 0 && glob.matches(rewritten);
|
|
641
|
-
},
|
|
642
|
-
canDescend(directory) {
|
|
643
|
-
const actual = resolve();
|
|
644
|
-
if (actual === void 0) return false;
|
|
645
|
-
if (directory === "." || actual.startsWith(`${directory}/`)) return true;
|
|
646
|
-
const rewritten = rewrite(directory, actual);
|
|
647
|
-
return rewritten !== void 0 && glob.canDescend(rewritten);
|
|
648
|
-
}
|
|
523
|
+
kind: "automatic",
|
|
524
|
+
operations
|
|
649
525
|
};
|
|
650
526
|
};
|
|
651
527
|
//#endregion
|
|
652
|
-
//#region src/
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
windowsPathsNoEscape: true,
|
|
659
|
-
nonegate: true,
|
|
660
|
-
nocomment: true,
|
|
661
|
-
optimizationLevel: 2
|
|
662
|
-
};
|
|
663
|
-
const boundedBraceExpand = (pattern) => {
|
|
664
|
-
const alternatives = braceExpand(pattern, { braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT });
|
|
665
|
-
if (alternatives.length > MAX_WORKSPACE_GLOB_ALTERNATIVES) throw new ConfigError(`Workspace pattern expands beyond ${MAX_WORKSPACE_GLOB_ALTERNATIVES} alternatives.`);
|
|
666
|
-
return alternatives;
|
|
667
|
-
};
|
|
668
|
-
/** Expand with minimatch's own bounded brace semantics. */
|
|
669
|
-
const expandWorkspaceGlob = (pattern) => {
|
|
670
|
-
try {
|
|
671
|
-
return boundedBraceExpand(pattern);
|
|
672
|
-
} catch (error) {
|
|
673
|
-
if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
|
|
674
|
-
throw error;
|
|
675
|
-
}
|
|
676
|
-
};
|
|
677
|
-
/** Compile once so membership, exclusions, and traversal share glob semantics. */
|
|
678
|
-
const compileWorkspaceGlob = (pattern, options = defaultWorkspaceGlobOptions) => {
|
|
679
|
-
let matcher;
|
|
680
|
-
try {
|
|
681
|
-
if (!options.nobrace) boundedBraceExpand(pattern);
|
|
682
|
-
matcher = new Minimatch(pattern, {
|
|
683
|
-
...options,
|
|
684
|
-
braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT
|
|
685
|
-
});
|
|
686
|
-
} catch (error) {
|
|
687
|
-
if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
|
|
688
|
-
throw error;
|
|
528
|
+
//#region src/domain/rules/builders/require-config-key.ts
|
|
529
|
+
const overrideBindings = (rule, overrides) => ({
|
|
530
|
+
...rule,
|
|
531
|
+
bindings: {
|
|
532
|
+
...rule.bindings,
|
|
533
|
+
...overrides
|
|
689
534
|
}
|
|
535
|
+
});
|
|
536
|
+
const accepts = (spec, actual) => spec.accept ? spec.accept(actual) : actual === spec.value;
|
|
537
|
+
const checkKeyValue = (spec, config) => {
|
|
538
|
+
const actual = getByPath(config, spec.keyPath);
|
|
539
|
+
const coveredByDefault = actual === void 0 && spec.documentedDefault !== void 0 && spec.versionNote?.defaultSafeSince === void 0 && accepts(spec, spec.documentedDefault);
|
|
540
|
+
if (!coveredByDefault && accepts(spec, actual)) return { state: "ok" };
|
|
541
|
+
const severity = coveredByDefault ? spec.defaultSatisfiedSeverity ?? "info" : void 0;
|
|
542
|
+
if (severity === "off") return { state: "ok" };
|
|
690
543
|
return {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
let patternIndex = 0;
|
|
704
|
-
let valueIndex = 0;
|
|
705
|
-
let starIndex = -1;
|
|
706
|
-
let retryIndex = 0;
|
|
707
|
-
while (valueIndex < valueCharacters.length) {
|
|
708
|
-
const character = patternCharacters[patternIndex];
|
|
709
|
-
if (character === "?" || character === valueCharacters[valueIndex]) {
|
|
710
|
-
patternIndex += 1;
|
|
711
|
-
valueIndex += 1;
|
|
712
|
-
} else if (character === "*") {
|
|
713
|
-
starIndex = patternIndex;
|
|
714
|
-
retryIndex = valueIndex;
|
|
715
|
-
patternIndex += 1;
|
|
716
|
-
} else if (starIndex >= 0) {
|
|
717
|
-
patternIndex = starIndex + 1;
|
|
718
|
-
retryIndex += 1;
|
|
719
|
-
valueIndex = retryIndex;
|
|
720
|
-
} else return false;
|
|
721
|
-
}
|
|
722
|
-
while (patternCharacters[patternIndex] === "*") patternIndex += 1;
|
|
723
|
-
return patternIndex === patternCharacters.length;
|
|
544
|
+
state: "violation",
|
|
545
|
+
actual,
|
|
546
|
+
expected: spec.value,
|
|
547
|
+
message: spec.message,
|
|
548
|
+
...severity === void 0 ? {} : { severity },
|
|
549
|
+
remediation: proposeChanges(config, [{
|
|
550
|
+
file: spec.file,
|
|
551
|
+
keyPath: spec.keyPath,
|
|
552
|
+
op: "setKey",
|
|
553
|
+
value: spec.value
|
|
554
|
+
}])
|
|
555
|
+
};
|
|
724
556
|
};
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
noext: true,
|
|
749
|
-
nocase: true
|
|
750
|
-
});
|
|
751
|
-
return {
|
|
752
|
-
...matcher,
|
|
753
|
-
matches: (directory) => files.matches(directory === "." ? "package.json" : `${directory}/package.json`)
|
|
754
|
-
};
|
|
557
|
+
const buildBinding = (spec, pm, applies) => ({
|
|
558
|
+
check(ctx, config) {
|
|
559
|
+
if (typeof applies !== "undefined" && !applies(ctx)) return { state: "na" };
|
|
560
|
+
const status = checkKeyValue(spec, config);
|
|
561
|
+
return status.state === "violation" ? {
|
|
562
|
+
...status,
|
|
563
|
+
remediation: guardRemediationAvailability(pm, ctx.pmVersion, status.remediation, [spec])
|
|
564
|
+
} : status;
|
|
565
|
+
},
|
|
566
|
+
docs: spec.docs,
|
|
567
|
+
file: spec.file,
|
|
568
|
+
severity: spec.severity,
|
|
569
|
+
versionNote: spec.versionNote
|
|
570
|
+
});
|
|
571
|
+
/** Build a Rule from a per-PM table of {file, keyPath, value, message}. */
|
|
572
|
+
const requireConfigKey = (options) => {
|
|
573
|
+
const bindings = {};
|
|
574
|
+
for (const pm of PMS) {
|
|
575
|
+
const spec = options.bindings[pm];
|
|
576
|
+
if (typeof spec !== "undefined") {
|
|
577
|
+
if ("extraFix" in spec) throw new TypeError("extraFix is no longer supported; use a custom binding.");
|
|
578
|
+
bindings[pm] = buildBinding(spec, pm, options.applies);
|
|
579
|
+
}
|
|
755
580
|
}
|
|
756
|
-
if (pm !== "aube" || !excluded) return matcher;
|
|
757
|
-
const self = pattern.endsWith("/**") ? pattern.slice(0, -3) : void 0;
|
|
758
581
|
return {
|
|
759
|
-
|
|
760
|
-
|
|
582
|
+
bindings,
|
|
583
|
+
description: options.description,
|
|
584
|
+
docs: options.docs,
|
|
585
|
+
id: options.id,
|
|
586
|
+
projectTypes: options.projectTypes,
|
|
587
|
+
severity: options.severity,
|
|
588
|
+
title: options.title
|
|
761
589
|
};
|
|
762
590
|
};
|
|
763
591
|
//#endregion
|
|
764
|
-
//#region src/
|
|
765
|
-
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
return {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const npmPatterns = (patterns) => {
|
|
774
|
-
const positive = [];
|
|
775
|
-
let negative = [];
|
|
776
|
-
for (const raw of patterns) {
|
|
777
|
-
const parsed = splitNpmPattern(raw);
|
|
778
|
-
const pattern = parsed.pattern.replace(/^\.?\/+/u, "");
|
|
779
|
-
if (parsed.excluded) negative.push({
|
|
780
|
-
pattern,
|
|
781
|
-
glob: compileWorkspaceGlob(pattern, {})
|
|
782
|
-
});
|
|
783
|
-
else {
|
|
784
|
-
for (let index = 0; index < negative.length; index += 1) if (negative[index]?.glob.matches(pattern)) negative.splice(index, 1);
|
|
785
|
-
positive.push(pattern);
|
|
592
|
+
//#region src/domain/rules/builders/with-aube-paranoid.ts
|
|
593
|
+
const withAubeParanoid = (rule) => {
|
|
594
|
+
const { aube } = rule.bindings;
|
|
595
|
+
if (!aube) return rule;
|
|
596
|
+
return overrideBindings(rule, { aube: {
|
|
597
|
+
...aube,
|
|
598
|
+
check(ctx, config) {
|
|
599
|
+
if (getByPath(config, ["paranoid"]) === true) return { state: "ok" };
|
|
600
|
+
return aube.check(ctx, config);
|
|
786
601
|
}
|
|
787
|
-
}
|
|
788
|
-
return [...positive, ...negative.map(({ pattern }) => `!${pattern}`)];
|
|
602
|
+
} });
|
|
789
603
|
};
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region src/domain/rules/advisory-check.ts
|
|
606
|
+
const { aubeWorkspace: aubeWorkspace$8 } = CONFIG_FILES;
|
|
607
|
+
const advisoryCheck = withAubeParanoid(requireConfigKey({
|
|
608
|
+
bindings: { aube: {
|
|
609
|
+
accept: (value) => value === "on" || value === "required",
|
|
610
|
+
docs: "https://aube.jdx.dev/security.html",
|
|
611
|
+
documentedDefault: "on",
|
|
612
|
+
file: aubeWorkspace$8,
|
|
613
|
+
keyPath: ["advisoryCheck"],
|
|
614
|
+
message: "Set `advisoryCheck: on` (or `required`) in aube-workspace.yaml to check dependencies against known-malicious advisories.",
|
|
615
|
+
value: "on"
|
|
616
|
+
} },
|
|
617
|
+
description: "Query the OSV database for known-malicious packages during dependency resolution.",
|
|
618
|
+
docs: "https://aube.jdx.dev/security.html",
|
|
619
|
+
id: "advisory-check",
|
|
620
|
+
severity: "warn",
|
|
621
|
+
title: "Enable malicious-package advisory checks"
|
|
622
|
+
}));
|
|
623
|
+
//#endregion
|
|
624
|
+
//#region src/domain/rules/config-predicates.ts
|
|
625
|
+
const isNonBlankString = (value) => typeof value === "string" && value.trim().length > 0;
|
|
626
|
+
const isStringList = (value) => Array.isArray(value) && Array.from(value).every(isNonBlankString);
|
|
627
|
+
//#endregion
|
|
628
|
+
//#region src/domain/rules/approved-git-repos.ts
|
|
629
|
+
const { yarnrc: yarnrc$9 } = CONFIG_FILES;
|
|
630
|
+
const message = "Set `approvedGitRepositories: []` in .yarnrc.yml to block all git: protocol dependencies (or list approved repository globs).";
|
|
631
|
+
const approvedGitRepos = defineRule({
|
|
632
|
+
bindings: { yarn: {
|
|
633
|
+
check(_ctx, config) {
|
|
634
|
+
const value = getByPath(config, ["approvedGitRepositories"]);
|
|
635
|
+
if (isStringList(value)) return { state: "ok" };
|
|
636
|
+
return {
|
|
637
|
+
remediation: {
|
|
638
|
+
kind: "manual",
|
|
639
|
+
steps: ["Add `approvedGitRepositories: []` to .yarnrc.yml to block all git deps, or list specific approved repository URL globs."]
|
|
640
|
+
},
|
|
641
|
+
actual: value,
|
|
642
|
+
expected: "[]",
|
|
643
|
+
message,
|
|
644
|
+
state: "violation"
|
|
645
|
+
};
|
|
646
|
+
},
|
|
647
|
+
docs: "https://yarnpkg.com/configuration/yarnrc#approvedGitRepositories",
|
|
648
|
+
file: yarnrc$9,
|
|
649
|
+
versionNote: { configAvailableSince: "yarn 4.14.0" }
|
|
650
|
+
} },
|
|
651
|
+
description: "Restrict git: protocol dependencies to an explicit allowlist of approved repository URL patterns.",
|
|
652
|
+
docs: "https://yarnpkg.com/configuration/yarnrc#approvedGitRepositories",
|
|
653
|
+
id: "approved-git-repos",
|
|
654
|
+
severity: "warn",
|
|
655
|
+
title: "Approve git repository dependencies"
|
|
656
|
+
});
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/domain/rules/audit-suppression.ts
|
|
659
|
+
const { yarnrc: yarnrc$8 } = CONFIG_FILES;
|
|
660
|
+
const isNonEmptyArray = (value) => Array.isArray(value) && value.length > 0;
|
|
661
|
+
const SUPPRESSION_KEYS = ["npmAuditIgnoreAdvisories", "npmAuditExcludePackages"];
|
|
662
|
+
const checkSuppression = (config) => {
|
|
663
|
+
const present = SUPPRESSION_KEYS.filter((key) => isNonEmptyArray(getByPath(config, [key])));
|
|
664
|
+
const [first] = present;
|
|
665
|
+
if (typeof first === "undefined") return { state: "ok" };
|
|
666
|
+
return {
|
|
667
|
+
actual: getByPath(config, [first]),
|
|
668
|
+
message: `Review audit suppression in .yarnrc.yml (${present.join(", ")}). Broad glob patterns can silently hide future vulnerabilities.`,
|
|
669
|
+
state: "violation",
|
|
670
|
+
remediation: {
|
|
671
|
+
kind: "manual",
|
|
672
|
+
steps: ["Review entries in `npmAuditIgnoreAdvisories` and `npmAuditExcludePackages` — remove stale suppressions and overly broad glob patterns."]
|
|
814
673
|
}
|
|
815
|
-
return validate(value, "package.json#workspaces");
|
|
816
674
|
};
|
|
817
|
-
if (pm === "pnpm") return [validate(ctx.exists(CONFIG_FILES.pnpmWorkspace.path) ? parse(CONFIG_FILES.pnpmWorkspace).packages : void 0, "pnpm-workspace.yaml#packages")];
|
|
818
|
-
if (pm === "aube") {
|
|
819
|
-
const file = [CONFIG_FILES.aubeWorkspace, CONFIG_FILES.pnpmWorkspace].find((candidate) => ctx.exists(candidate.path));
|
|
820
|
-
return [file ? validate(parse(file).packages, `${file.path}#packages`) : packageDefinition()];
|
|
821
|
-
}
|
|
822
|
-
if (pm === "deno") return [validate(parse(CONFIG_FILES.denoJson).workspace, "deno.json#workspace", true), packageDefinition()];
|
|
823
|
-
return [packageDefinition()];
|
|
824
675
|
};
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
const
|
|
867
|
-
|
|
868
|
-
return parts.slice(0, wildcard < 0 ? parts.length : wildcard).join("/") || ".";
|
|
869
|
-
};
|
|
870
|
-
const relatedBases = (left, right) => left === "." || right === "." || left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
871
|
-
const denoPositive = pm === "deno" ? positive.filter((pattern) => /[*?]/u.test(pattern)).map((pattern) => ({
|
|
872
|
-
base: declaredBase(pattern),
|
|
873
|
-
scope: anchorWorkspacePrefix(declaredBase(pattern), {
|
|
874
|
-
matches: () => true,
|
|
875
|
-
canDescend: () => true
|
|
876
|
-
}, resolveChild)
|
|
877
|
-
})) : [];
|
|
878
|
-
const denoOrdered = pm === "deno" ? patterns.filter((pattern) => /[*?]/u.test(pattern) || pattern.startsWith("!")).map((raw) => {
|
|
879
|
-
const isExcluded = raw.startsWith("!");
|
|
880
|
-
const pattern = path.posix.normalize(raw.replace(/^!/u, "")).replace(/\/+$/u, "");
|
|
881
|
-
const glob = compile(pattern, isExcluded);
|
|
882
|
-
if (!isExcluded) return {
|
|
883
|
-
excluded: false,
|
|
884
|
-
glob
|
|
885
|
-
};
|
|
886
|
-
return {
|
|
887
|
-
excluded: true,
|
|
888
|
-
glob: {
|
|
889
|
-
...glob,
|
|
890
|
-
matches(directory) {
|
|
891
|
-
if (!/[*?]/u.test(pattern)) return directory === pattern;
|
|
892
|
-
if (!glob.matches(directory)) return false;
|
|
893
|
-
const applicable = denoPositive.filter((item) => item.scope.matches(directory)).map((item) => relatedBases(item.base, declaredBase(pattern)));
|
|
894
|
-
if (applicable.some(Boolean) && applicable.some((value) => !value)) throw new ConfigError("Deno workspace exclusion has ambiguous overlapping bases; use consistent prefix spelling or separate non-overlapping patterns.");
|
|
895
|
-
return applicable.some(Boolean);
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
};
|
|
899
|
-
}) : [];
|
|
900
|
-
const bunPatterns = pm === "bun" ? patterns.map((raw) => {
|
|
901
|
-
const isExcluded = raw.startsWith("!");
|
|
902
|
-
const pattern = path.posix.normalize(raw.replace(/^!/u, "")).replace(/\/+$/u, "");
|
|
903
|
-
const usesGlob = isExcluded || pattern.includes("*") || pattern.includes("?") || pattern.includes("{") || pattern.includes("[");
|
|
904
|
-
const glob = compileWorkspaceGlob(pattern, {
|
|
905
|
-
...defaultWorkspaceGlobOptions,
|
|
906
|
-
noext: true
|
|
907
|
-
});
|
|
908
|
-
const subtree = isExcluded && pattern.endsWith("/**") ? compileWorkspaceGlob(pattern.slice(0, -3), {
|
|
909
|
-
...defaultWorkspaceGlobOptions,
|
|
910
|
-
noext: true
|
|
911
|
-
}) : void 0;
|
|
676
|
+
const auditSuppression = defineRule({
|
|
677
|
+
bindings: { yarn: {
|
|
678
|
+
check(_ctx, config) {
|
|
679
|
+
return checkSuppression(config);
|
|
680
|
+
},
|
|
681
|
+
docs: "https://yarnpkg.com/configuration/yarnrc#npmAuditIgnoreAdvisories",
|
|
682
|
+
file: yarnrc$8
|
|
683
|
+
} },
|
|
684
|
+
description: "Flag audit advisory suppressions that may silently hide future vulnerabilities via broad glob patterns.",
|
|
685
|
+
docs: "https://yarnpkg.com/configuration/yarnrc#npmAuditIgnoreAdvisories",
|
|
686
|
+
id: "audit-suppression",
|
|
687
|
+
severity: "info",
|
|
688
|
+
title: "Review audit suppression entries"
|
|
689
|
+
});
|
|
690
|
+
//#endregion
|
|
691
|
+
//#region src/domain/rules/block-auto-install.ts
|
|
692
|
+
const { bunfig: bunfig$5 } = CONFIG_FILES;
|
|
693
|
+
const blockAutoInstall = requireConfigKey({
|
|
694
|
+
bindings: { bun: {
|
|
695
|
+
docs: "https://bun.sh/docs/runtime/bunfig#install-auto",
|
|
696
|
+
file: bunfig$5,
|
|
697
|
+
keyPath: ["install", "auto"],
|
|
698
|
+
message: "Set `auto = \"disable\"` under [install] in bunfig.toml to prevent auto-install from bypassing security guards.",
|
|
699
|
+
value: "disable",
|
|
700
|
+
accept: (value) => value === "disable" || value === false
|
|
701
|
+
} },
|
|
702
|
+
description: "Disable auto-install so dependencies are only installed through an explicit install step where security guards apply.",
|
|
703
|
+
docs: "https://bun.sh/docs/runtime/bunfig#install-auto",
|
|
704
|
+
id: "block-auto-install",
|
|
705
|
+
severity: "warn",
|
|
706
|
+
title: "Block automatic package installation"
|
|
707
|
+
});
|
|
708
|
+
//#endregion
|
|
709
|
+
//#region src/domain/rules/block-exotic-subdeps.ts
|
|
710
|
+
const { aubeWorkspace: aubeWorkspace$7, npmrc: npmrc$6, pnpmWorkspace: pnpmWorkspace$10 } = CONFIG_FILES;
|
|
711
|
+
const isRootOrNone = (value) => value === "root" || value === "none";
|
|
712
|
+
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.";
|
|
713
|
+
const npmBinding$1 = {
|
|
714
|
+
check(_ctx, config) {
|
|
715
|
+
const git = getByPath(config, ["allow-git"]);
|
|
716
|
+
const remote = getByPath(config, ["allow-remote"]);
|
|
717
|
+
const invalid = [git, remote].find((value) => value !== void 0 && !isRootOrNone(value));
|
|
718
|
+
if (invalid === void 0 && git !== void 0 && remote !== void 0) return { state: "ok" };
|
|
912
719
|
return {
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
720
|
+
state: "violation",
|
|
721
|
+
expected: "none",
|
|
722
|
+
...invalid === void 0 ? {} : { actual: invalid },
|
|
723
|
+
message: npmMessage,
|
|
724
|
+
remediation: proposeChanges(config, [{
|
|
725
|
+
file: npmrc$6,
|
|
726
|
+
keyPath: ["allow-git"],
|
|
727
|
+
op: "setKey",
|
|
728
|
+
value: "none"
|
|
729
|
+
}, {
|
|
730
|
+
file: npmrc$6,
|
|
731
|
+
keyPath: ["allow-remote"],
|
|
732
|
+
op: "setKey",
|
|
733
|
+
value: "none"
|
|
734
|
+
}])
|
|
917
735
|
};
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const denoLiteral = pm === "deno" ? positive.filter((pattern) => !/[*?]/u.test(pattern)).map((pattern) => compile(pattern)) : [];
|
|
923
|
-
if (definition.denoManifests && denoOrdered.findLast(({ glob }) => glob.matches("."))?.excluded === false) throw new ConfigError("deno.json#workspace: a Deno workspace cannot contain itself.");
|
|
924
|
-
if (positive.length === 0 || positive.every((pattern) => pattern === ".")) return [];
|
|
925
|
-
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
926
|
-
const result = [];
|
|
927
|
-
const pending = ["."];
|
|
928
|
-
while (pending.length > 0) {
|
|
929
|
-
const current = pending.pop();
|
|
930
|
-
if (current === void 0) break;
|
|
931
|
-
for (const name of readDirectories(current)) {
|
|
932
|
-
if (name === ".git" || name === "node_modules") continue;
|
|
933
|
-
const directory = asRelPath(current === "." ? name : `${current}/${name}`);
|
|
934
|
-
if (pm === "bun" && name === "CMakeFiles" && !bunLiterals.some(({ glob }) => glob.matches(directory) || glob.canDescend(directory))) continue;
|
|
935
|
-
const inVendor = skipVendor && (directory === "vendor" || directory.startsWith("vendor/"));
|
|
936
|
-
if (inVendor && !denoLiteral.some((pattern) => pattern.matches(directory) || pattern.canDescend(directory))) continue;
|
|
937
|
-
const isExcluded = pm !== "deno" && excluded.some(({ glob }) => glob.matches(directory) || glob.matches(`${directory}/`));
|
|
938
|
-
if (excluded.some(({ subtree }) => subtree?.matches(directory))) continue;
|
|
939
|
-
const lastDenoMatch = denoOrdered.findLast(({ glob }) => glob.matches(directory));
|
|
940
|
-
const lastBunMatch = bunOrdered.findLast(({ matches }) => matches(directory));
|
|
941
|
-
if (pm === "deno" ? denoLiteral.some((pattern) => pattern.matches(directory)) || !inVendor && lastDenoMatch?.excluded === false : pm === "bun" ? bunLiterals.some(({ glob }) => glob.matches(directory)) || lastBunMatch?.isExcluded === false : !isExcluded && included.some((pattern) => pattern.matches(directory))) result.push(directory);
|
|
942
|
-
if ((pm === "bun" ? bunTraversal : inVendor ? denoLiteral : included).some((pattern) => pattern.canDescend(directory))) pending.push(directory);
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
return result.sort();
|
|
736
|
+
},
|
|
737
|
+
docs: "https://docs.npmjs.com/cli/v12/using-npm/config#allow-git",
|
|
738
|
+
file: npmrc$6,
|
|
739
|
+
versionNote: { defaultSafeSince: "npm 12.0.0" }
|
|
946
740
|
};
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const explicitMember = definition.patterns.some((pattern) => !pattern.startsWith("!") && !/[*?]/u.test(pattern) && path.posix.normalize(pattern).replace(/\/+$/u, "") === directory);
|
|
968
|
-
if (!definition.denoManifests && !fs.exists(manifest)) {
|
|
969
|
-
if (pm === "deno" && explicitMember) throw new ConfigError("Declared npm workspace member has no package.json.");
|
|
970
|
-
return [];
|
|
971
|
-
}
|
|
972
|
-
if (pm === "deno" && !fs.exists(denoManifest) && fs.exists(denoJsonc)) throw new ConfigError("deno.jsonc is not supported; use strict deno.json.");
|
|
973
|
-
const memberCtx = createRepoContext(root, memberFs, projectType);
|
|
974
|
-
if (!definition.denoManifests && !memberCtx.packageJson) return [];
|
|
975
|
-
if (!memberCtx.packageJson && !memberCtx.exists(asRelPath("deno.json"))) {
|
|
976
|
-
if (definition.denoManifests && explicitMember) throw new ConfigError("Declared Deno member has no deno.json or package.json.");
|
|
977
|
-
return [];
|
|
978
|
-
}
|
|
979
|
-
if (pm === "deno") {
|
|
980
|
-
if (createConfigParser(codecFor, memberCtx)(CONFIG_FILES.denoJson).workspace !== void 0 || memberCtx.packageJson?.workspaces !== void 0) throw new ConfigError("Nested workspace declarations are not supported by Deno.");
|
|
741
|
+
const blockExoticSubdeps = overrideBindings(requireConfigKey({
|
|
742
|
+
bindings: {
|
|
743
|
+
aube: {
|
|
744
|
+
docs: "https://aube.jdx.dev/security.html",
|
|
745
|
+
documentedDefault: true,
|
|
746
|
+
file: aubeWorkspace$7,
|
|
747
|
+
keyPath: ["blockExoticSubdeps"],
|
|
748
|
+
message: "Set `blockExoticSubdeps: true` in aube-workspace.yaml to block git/tarball URLs in transitive dependencies.",
|
|
749
|
+
value: true
|
|
750
|
+
},
|
|
751
|
+
pnpm: {
|
|
752
|
+
docs: "https://pnpm.io/settings#blockexoticsubdeps",
|
|
753
|
+
documentedDefault: true,
|
|
754
|
+
file: pnpmWorkspace$10,
|
|
755
|
+
keyPath: ["blockExoticSubdeps"],
|
|
756
|
+
message: "Set `blockExoticSubdeps: true` in pnpm-workspace.yaml to block git/tarball URLs in transitive dependencies.",
|
|
757
|
+
value: true,
|
|
758
|
+
versionNote: {
|
|
759
|
+
configAvailableSince: "pnpm 10.26.0",
|
|
760
|
+
defaultSafeSince: "pnpm 10.26.0"
|
|
981
761
|
}
|
|
982
|
-
if (seen.has(directory)) return [];
|
|
983
|
-
seen.add(directory);
|
|
984
|
-
return [{
|
|
985
|
-
directory,
|
|
986
|
-
pm,
|
|
987
|
-
ctx: memberCtx
|
|
988
|
-
}];
|
|
989
|
-
} catch (error) {
|
|
990
|
-
if (error instanceof ConfigError) throw new ConfigError(`${directory}/${error.message}`);
|
|
991
|
-
throw error;
|
|
992
762
|
}
|
|
993
|
-
}
|
|
763
|
+
},
|
|
764
|
+
description: "Refuse to install transitive dependencies sourced from git or tarball URLs, which bypass registry integrity checking.",
|
|
765
|
+
docs: "https://pnpm.io/settings#blockexoticsubdeps",
|
|
766
|
+
id: "block-exotic-subdeps",
|
|
767
|
+
severity: "warn",
|
|
768
|
+
title: "Block exotic subdependencies"
|
|
769
|
+
}), { npm: npmBinding$1 });
|
|
770
|
+
//#endregion
|
|
771
|
+
//#region src/domain/rules/bun-security-scanner.ts
|
|
772
|
+
const { bunfig: bunfig$4 } = CONFIG_FILES;
|
|
773
|
+
const bunSecurityScanner = defineRule({
|
|
774
|
+
bindings: { bun: {
|
|
775
|
+
check(ctx, config) {
|
|
776
|
+
const scanner = getByPath(config, [
|
|
777
|
+
"install",
|
|
778
|
+
"security",
|
|
779
|
+
"scanner"
|
|
780
|
+
]);
|
|
781
|
+
if (isNonBlankString(scanner)) return { state: "ok" };
|
|
782
|
+
return {
|
|
783
|
+
remediation: guardRemediationAvailability("bun", ctx.pmVersion, {
|
|
784
|
+
kind: "manual",
|
|
785
|
+
steps: ["Add `[install.security] scanner = \"@socketsecurity/bun-security-scanner\"` (or another bun-compatible scanner) to bunfig.toml."]
|
|
786
|
+
}, [{
|
|
787
|
+
file: bunfig$4,
|
|
788
|
+
keyPath: [
|
|
789
|
+
"install",
|
|
790
|
+
"security",
|
|
791
|
+
"scanner"
|
|
792
|
+
]
|
|
793
|
+
}]),
|
|
794
|
+
actual: scanner,
|
|
795
|
+
message: "Configure `[install.security] scanner = \"...\"` in bunfig.toml (e.g. `@socketsecurity/bun-security-scanner`) to scan new packages on install.",
|
|
796
|
+
state: "violation"
|
|
797
|
+
};
|
|
798
|
+
},
|
|
799
|
+
docs: "https://bun.com/docs/pm/security-scanner-api",
|
|
800
|
+
file: bunfig$4,
|
|
801
|
+
versionNote: { configAvailableSince: "bun 1.2.21" }
|
|
802
|
+
} },
|
|
803
|
+
description: "Bun supports a Security Scanner API that intercepts new packages at install time (e.g. Socket Firewall).",
|
|
804
|
+
docs: "https://github.com/bodadotsh/npm-security-best-practices#preinstall-scanners",
|
|
805
|
+
id: "bun-security-scanner",
|
|
806
|
+
severity: "info",
|
|
807
|
+
title: "Enable a bun install-time security scanner"
|
|
994
808
|
});
|
|
995
809
|
//#endregion
|
|
996
|
-
//#region src/domain/
|
|
997
|
-
const {
|
|
998
|
-
const
|
|
999
|
-
{
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
810
|
+
//#region src/domain/rules/checksum-verification.ts
|
|
811
|
+
const { yarnrc: yarnrc$7 } = CONFIG_FILES;
|
|
812
|
+
const checksumVerification = requireConfigKey({
|
|
813
|
+
bindings: { yarn: {
|
|
814
|
+
docs: "https://yarnpkg.com/configuration/yarnrc#checksumBehavior",
|
|
815
|
+
documentedDefault: "throw",
|
|
816
|
+
file: yarnrc$7,
|
|
817
|
+
keyPath: ["checksumBehavior"],
|
|
818
|
+
message: "Set `checksumBehavior: throw` in .yarnrc.yml to reject packages whose checksum does not match the lockfile.",
|
|
819
|
+
value: "throw"
|
|
820
|
+
} },
|
|
821
|
+
description: "Throw on checksum mismatches so tampered or corrupted packages are never silently installed.",
|
|
822
|
+
docs: "https://yarnpkg.com/configuration/yarnrc#checksumBehavior",
|
|
823
|
+
id: "checksum-verification",
|
|
824
|
+
severity: "warn",
|
|
825
|
+
title: "Verify package checksums"
|
|
826
|
+
});
|
|
827
|
+
//#endregion
|
|
828
|
+
//#region src/domain/entities/signals.ts
|
|
829
|
+
const PM_SIGNALS = {
|
|
830
|
+
aube: {
|
|
831
|
+
configs: [CONFIG_FILES.aubeWorkspace.path],
|
|
832
|
+
lockfiles: ["aube-lock.yaml"],
|
|
833
|
+
reusesLockfiles: [
|
|
834
|
+
"pnpm-lock.yaml",
|
|
835
|
+
"package-lock.json",
|
|
836
|
+
"npm-shrinkwrap.json",
|
|
837
|
+
"yarn.lock",
|
|
838
|
+
"bun.lock"
|
|
839
|
+
]
|
|
1005
840
|
},
|
|
1006
|
-
{
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
keyPath: ["publishConfig", "provenance"],
|
|
1010
|
-
since: "9.5.0",
|
|
1011
|
-
source: "https://github.com/npm/cli/releases/tag/v9.5.0"
|
|
841
|
+
bun: {
|
|
842
|
+
configs: [CONFIG_FILES.bunfig.path],
|
|
843
|
+
lockfiles: ["bun.lock", "bun.lockb"]
|
|
1012
844
|
},
|
|
1013
|
-
{
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
keyPath: ["min-release-age"],
|
|
1017
|
-
since: "11.10.0",
|
|
1018
|
-
source: "https://github.com/npm/cli/releases/tag/v11.10.0"
|
|
845
|
+
deno: {
|
|
846
|
+
configs: [CONFIG_FILES.denoJson.path, "deno.jsonc"],
|
|
847
|
+
lockfiles: ["deno.lock"]
|
|
1019
848
|
},
|
|
1020
|
-
{
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
keyPath: ["strictDepBuilds"],
|
|
1024
|
-
since: "10.6.0",
|
|
1025
|
-
source: "https://github.com/pnpm/pnpm/releases/tag/v10.6.0"
|
|
849
|
+
npm: {
|
|
850
|
+
configs: [],
|
|
851
|
+
lockfiles: ["package-lock.json", "npm-shrinkwrap.json"]
|
|
1026
852
|
},
|
|
1027
|
-
{
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
keyPath: ["dangerouslyAllowAllBuilds"],
|
|
1031
|
-
since: "10.9.0",
|
|
1032
|
-
source: "https://github.com/pnpm/pnpm/releases/tag/v10.9.0"
|
|
1033
|
-
},
|
|
1034
|
-
{
|
|
1035
|
-
pm: "pnpm",
|
|
1036
|
-
file: pnpmWorkspace$11,
|
|
1037
|
-
keyPath: ["minimumReleaseAge"],
|
|
1038
|
-
since: "10.16.0",
|
|
1039
|
-
source: "https://github.com/pnpm/pnpm/releases/tag/v10.16.0"
|
|
1040
|
-
},
|
|
1041
|
-
{
|
|
1042
|
-
pm: "pnpm",
|
|
1043
|
-
file: pnpmWorkspace$11,
|
|
1044
|
-
keyPath: ["minimumReleaseAgeExclude"],
|
|
1045
|
-
since: "10.16.0",
|
|
1046
|
-
source: "https://github.com/pnpm/pnpm/releases/tag/v10.16.0"
|
|
1047
|
-
},
|
|
1048
|
-
{
|
|
1049
|
-
pm: "pnpm",
|
|
1050
|
-
file: pnpmWorkspace$11,
|
|
1051
|
-
keyPath: ["trustPolicy"],
|
|
1052
|
-
since: "10.21.0",
|
|
1053
|
-
source: "https://github.com/pnpm/pnpm/releases/tag/v10.21.0"
|
|
1054
|
-
},
|
|
1055
|
-
{
|
|
1056
|
-
pm: "pnpm",
|
|
1057
|
-
file: pnpmWorkspace$11,
|
|
1058
|
-
keyPath: ["blockExoticSubdeps"],
|
|
1059
|
-
since: "10.26.0",
|
|
1060
|
-
source: "https://github.com/pnpm/pnpm/releases/tag/v10.26.0"
|
|
1061
|
-
},
|
|
1062
|
-
{
|
|
1063
|
-
pm: "pnpm",
|
|
1064
|
-
file: pnpmWorkspace$11,
|
|
1065
|
-
keyPath: ["frozenStore"],
|
|
1066
|
-
since: "11.7.0",
|
|
1067
|
-
source: "https://github.com/pnpm/pnpm/releases/tag/v11.7.0"
|
|
1068
|
-
},
|
|
1069
|
-
{
|
|
1070
|
-
pm: "yarn",
|
|
1071
|
-
file: yarnrc$10,
|
|
1072
|
-
keyPath: ["enableHardenedMode"],
|
|
1073
|
-
since: "4.0.0",
|
|
1074
|
-
source: "https://yarnpkg.com/blog/release/4.0"
|
|
1075
|
-
},
|
|
1076
|
-
{
|
|
1077
|
-
pm: "yarn",
|
|
1078
|
-
file: yarnrc$10,
|
|
1079
|
-
keyPath: ["npmMinimalAgeGate"],
|
|
1080
|
-
since: "4.10.0",
|
|
1081
|
-
source: "https://github.com/yarnpkg/berry/releases/tag/@yarnpkg/cli/4.10.0"
|
|
1082
|
-
},
|
|
1083
|
-
{
|
|
1084
|
-
pm: "yarn",
|
|
1085
|
-
file: yarnrc$10,
|
|
1086
|
-
keyPath: ["npmPreapprovedPackages"],
|
|
1087
|
-
since: "4.10.0",
|
|
1088
|
-
source: "https://github.com/yarnpkg/berry/releases/tag/@yarnpkg/cli/4.10.0"
|
|
1089
|
-
},
|
|
1090
|
-
{
|
|
1091
|
-
pm: "bun",
|
|
1092
|
-
file: bunfig$6,
|
|
1093
|
-
keyPath: ["install", "minimumReleaseAge"],
|
|
1094
|
-
since: "1.3.0",
|
|
1095
|
-
source: "https://bun.com/blog/bun-v1.3#minimum-release-age"
|
|
1096
|
-
},
|
|
1097
|
-
{
|
|
1098
|
-
pm: "bun",
|
|
1099
|
-
file: bunfig$6,
|
|
1100
|
-
keyPath: [
|
|
1101
|
-
"install",
|
|
1102
|
-
"security",
|
|
1103
|
-
"scanner"
|
|
1104
|
-
],
|
|
1105
|
-
since: "1.2.21",
|
|
1106
|
-
source: "https://bun.com/blog/bun-v1.2.21#security-scanner-api-for-bun-install"
|
|
853
|
+
pnpm: {
|
|
854
|
+
configs: [CONFIG_FILES.pnpmWorkspace.path],
|
|
855
|
+
lockfiles: ["pnpm-lock.yaml"]
|
|
1107
856
|
},
|
|
1108
|
-
{
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
keyPath: ["min-release-age"],
|
|
1112
|
-
since: "2.8.1",
|
|
1113
|
-
source: "https://github.com/denoland/deno/releases/tag/v2.8.1"
|
|
857
|
+
yarn: {
|
|
858
|
+
configs: [CONFIG_FILES.yarnrc.path],
|
|
859
|
+
lockfiles: ["yarn.lock"]
|
|
1114
860
|
}
|
|
1115
|
-
|
|
861
|
+
};
|
|
1116
862
|
//#endregion
|
|
1117
|
-
//#region src/domain/
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
863
|
+
//#region src/domain/rules/commit-lockfile.ts
|
|
864
|
+
const LOCKFILE_DOCS = {
|
|
865
|
+
aube: "https://github.com/aubepkg/aube/blob/main/docs/package-manager/lockfiles.md",
|
|
866
|
+
bun: "https://bun.com/docs/install/lockfile",
|
|
867
|
+
deno: "https://docs.deno.com/runtime/fundamentals/modules/#integrity-checking-and-lock-files",
|
|
868
|
+
npm: "https://docs.npmjs.com/cli/v12/configuring-npm/package-lock-json",
|
|
869
|
+
pnpm: "https://pnpm.io/git#lockfiles",
|
|
870
|
+
yarn: "https://yarnpkg.com/getting-started/qa#should-lockfiles-be-committed-to-the-repository"
|
|
1124
871
|
};
|
|
1125
|
-
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
const
|
|
1129
|
-
|
|
872
|
+
const LOCKFILE_VERSION_NOTES = { deno: { configAvailableSince: "deno 1.28.0" } };
|
|
873
|
+
const lockfileBinding = (pm) => {
|
|
874
|
+
const { lockfiles, reusesLockfiles } = PM_SIGNALS[pm];
|
|
875
|
+
const [primary] = lockfiles;
|
|
876
|
+
const accepted = [...lockfiles, ...reusesLockfiles ?? []].filter((lockfile) => pm !== "npm" || lockfile === primary);
|
|
1130
877
|
return {
|
|
1131
|
-
|
|
1132
|
-
|
|
878
|
+
check(ctx) {
|
|
879
|
+
if (pm === "aube" && ctx.exists(asRelPath("bun.lockb")) && !ctx.exists(asRelPath("bun.lock"))) return {
|
|
880
|
+
state: "violation",
|
|
881
|
+
message: "Aube cannot read bun.lockb, even when another lockfile exists.",
|
|
882
|
+
remediation: {
|
|
883
|
+
kind: "manual",
|
|
884
|
+
steps: ["Run `bun install --save-text-lockfile` to generate bun.lock, then commit it."]
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
if (accepted.some((lf) => ctx.exists(asRelPath(lf)))) return { state: "ok" };
|
|
888
|
+
return {
|
|
889
|
+
remediation: {
|
|
890
|
+
kind: "manual",
|
|
891
|
+
steps: [`Install dependencies to generate ${primary}, then commit it.`]
|
|
892
|
+
},
|
|
893
|
+
message: `No lockfile found. Generate and commit ${primary}.`,
|
|
894
|
+
state: "violation"
|
|
895
|
+
};
|
|
896
|
+
},
|
|
897
|
+
docs: LOCKFILE_DOCS[pm],
|
|
898
|
+
versionNote: LOCKFILE_VERSION_NOTES[pm]
|
|
1133
899
|
};
|
|
1134
900
|
};
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
901
|
+
const denoBinding$1 = {
|
|
902
|
+
file: CONFIG_FILES.denoJson,
|
|
903
|
+
docs: LOCKFILE_DOCS.deno,
|
|
904
|
+
versionNote: LOCKFILE_VERSION_NOTES.deno,
|
|
905
|
+
check(ctx, config) {
|
|
906
|
+
const lock = getByPath(config, ["lock"]);
|
|
907
|
+
if (lock != null && typeof lock !== "boolean" && typeof lock !== "string" && !(isPlainRecord(lock) && (lock.path == null || typeof lock.path === "string") && (lock.frozen == null || typeof lock.frozen === "boolean"))) return {
|
|
908
|
+
state: "violation",
|
|
909
|
+
message: "Invalid Deno lock setting.",
|
|
910
|
+
remediation: {
|
|
1144
911
|
kind: "manual",
|
|
1145
|
-
steps: [
|
|
1146
|
-
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
912
|
+
steps: ["Use a boolean, a lockfile path, or an object with optional path and frozen fields."]
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
const selected = typeof lock === "string" ? lock : getByPath(config, ["lock", "path"]);
|
|
916
|
+
const lockPath = selected == null ? asRelPath("deno.lock") : selected;
|
|
917
|
+
if (lock !== false && isRelPath(lockPath) && ctx.exists(lockPath)) return { state: "ok" };
|
|
918
|
+
return {
|
|
919
|
+
state: "violation",
|
|
920
|
+
message: lock === false ? "Enable the Deno lockfile; `lock: false` disables it even when deno.lock exists." : `No readable project lockfile found at ${String(lockPath)}.`,
|
|
921
|
+
remediation: {
|
|
922
|
+
kind: "manual",
|
|
923
|
+
steps: ["Enable lockfile use in deno.json and generate and commit the configured lockfile within the project."]
|
|
924
|
+
}
|
|
1152
925
|
};
|
|
1153
926
|
}
|
|
1154
|
-
return {
|
|
1155
|
-
kind: "automatic",
|
|
1156
|
-
operations
|
|
1157
|
-
};
|
|
1158
927
|
};
|
|
1159
|
-
|
|
1160
|
-
//#region src/domain/rules/builders/require-config-key.ts
|
|
1161
|
-
const overrideBindings = (rule, overrides) => ({
|
|
1162
|
-
...rule,
|
|
928
|
+
const commitLockfile = defineRule({
|
|
1163
929
|
bindings: {
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
930
|
+
aube: lockfileBinding("aube"),
|
|
931
|
+
bun: lockfileBinding("bun"),
|
|
932
|
+
deno: denoBinding$1,
|
|
933
|
+
npm: lockfileBinding("npm"),
|
|
934
|
+
pnpm: lockfileBinding("pnpm"),
|
|
935
|
+
yarn: lockfileBinding("yarn")
|
|
936
|
+
},
|
|
937
|
+
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#2-include-lockfiles",
|
|
939
|
+
id: "commit-lockfile",
|
|
940
|
+
severity: "error",
|
|
941
|
+
title: "Commit a lockfile"
|
|
1167
942
|
});
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1174
|
-
if (
|
|
943
|
+
//#endregion
|
|
944
|
+
//#region src/domain/rules/dependency-overrides.ts
|
|
945
|
+
const { aubeWorkspace: aubeWorkspace$6, pnpmWorkspace: pnpmWorkspace$9 } = CONFIG_FILES;
|
|
946
|
+
const isNonEmptyObject$2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
|
|
947
|
+
const checkOverrides = (config, file) => {
|
|
948
|
+
const value = getByPath(config, ["overrides"]);
|
|
949
|
+
if (!isNonEmptyObject$2(value)) return { state: "ok" };
|
|
1175
950
|
return {
|
|
951
|
+
actual: value,
|
|
952
|
+
message: `Review \`overrides\` in ${file}. Overrides can replace transitive dependencies with arbitrary versions or forks.`,
|
|
1176
953
|
state: "violation",
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
remediation: proposeChanges(config, [{
|
|
1182
|
-
file: spec.file,
|
|
1183
|
-
keyPath: spec.keyPath,
|
|
1184
|
-
op: "setKey",
|
|
1185
|
-
value: spec.value
|
|
1186
|
-
}])
|
|
954
|
+
remediation: {
|
|
955
|
+
kind: "manual",
|
|
956
|
+
steps: ["Review each entry in `overrides` — verify pinned versions address a known CVE and are not redirecting to untrusted packages."]
|
|
957
|
+
}
|
|
1187
958
|
};
|
|
1188
959
|
};
|
|
1189
|
-
const
|
|
1190
|
-
check(
|
|
1191
|
-
|
|
1192
|
-
const status = checkKeyValue(spec, config);
|
|
1193
|
-
return status.state === "violation" ? {
|
|
1194
|
-
...status,
|
|
1195
|
-
remediation: guardRemediationAvailability(pm, ctx.pmVersion, status.remediation, [spec])
|
|
1196
|
-
} : status;
|
|
960
|
+
const makeBinding = (file, fileName, docs) => ({
|
|
961
|
+
check(_ctx, config) {
|
|
962
|
+
return checkOverrides(config, fileName);
|
|
1197
963
|
},
|
|
1198
|
-
docs
|
|
1199
|
-
file
|
|
1200
|
-
|
|
1201
|
-
|
|
964
|
+
docs,
|
|
965
|
+
file
|
|
966
|
+
});
|
|
967
|
+
const dependencyOverrides = defineRule({
|
|
968
|
+
bindings: {
|
|
969
|
+
aube: makeBinding(aubeWorkspace$6, "aube-workspace.yaml", "https://aube.sh/settings/"),
|
|
970
|
+
pnpm: makeBinding(pnpmWorkspace$9, "pnpm-workspace.yaml", "https://pnpm.io/settings#overrides")
|
|
971
|
+
},
|
|
972
|
+
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",
|
|
974
|
+
id: "dependency-overrides",
|
|
975
|
+
severity: "info",
|
|
976
|
+
title: "Review dependency overrides"
|
|
1202
977
|
});
|
|
1203
|
-
/** Build a Rule from a per-PM table of {file, keyPath, value, message}. */
|
|
1204
|
-
const requireConfigKey = (options) => {
|
|
1205
|
-
const bindings = {};
|
|
1206
|
-
for (const pm of PMS) {
|
|
1207
|
-
const spec = options.bindings[pm];
|
|
1208
|
-
if (typeof spec !== "undefined") {
|
|
1209
|
-
if ("extraFix" in spec) throw new TypeError("extraFix is no longer supported; use a custom binding.");
|
|
1210
|
-
bindings[pm] = buildBinding(spec, pm, options.applies);
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
return {
|
|
1214
|
-
bindings,
|
|
1215
|
-
description: options.description,
|
|
1216
|
-
docs: options.docs,
|
|
1217
|
-
id: options.id,
|
|
1218
|
-
projectTypes: options.projectTypes,
|
|
1219
|
-
severity: options.severity,
|
|
1220
|
-
title: options.title
|
|
1221
|
-
};
|
|
1222
|
-
};
|
|
1223
|
-
//#endregion
|
|
1224
|
-
//#region src/domain/rules/builders/with-aube-paranoid.ts
|
|
1225
|
-
const withAubeParanoid = (rule) => {
|
|
1226
|
-
const { aube } = rule.bindings;
|
|
1227
|
-
if (!aube) return rule;
|
|
1228
|
-
return overrideBindings(rule, { aube: {
|
|
1229
|
-
...aube,
|
|
1230
|
-
check(ctx, config) {
|
|
1231
|
-
if (getByPath(config, ["paranoid"]) === true) return { state: "ok" };
|
|
1232
|
-
return aube.check(ctx, config);
|
|
1233
|
-
}
|
|
1234
|
-
} });
|
|
1235
|
-
};
|
|
1236
|
-
//#endregion
|
|
1237
|
-
//#region src/domain/rules/advisory-check.ts
|
|
1238
|
-
const { aubeWorkspace: aubeWorkspace$8 } = CONFIG_FILES;
|
|
1239
|
-
const advisoryCheck = withAubeParanoid(requireConfigKey({
|
|
1240
|
-
bindings: { aube: {
|
|
1241
|
-
accept: (value) => value === "on" || value === "required",
|
|
1242
|
-
docs: "https://aube.jdx.dev/security.html",
|
|
1243
|
-
documentedDefault: "on",
|
|
1244
|
-
file: aubeWorkspace$8,
|
|
1245
|
-
keyPath: ["advisoryCheck"],
|
|
1246
|
-
message: "Set `advisoryCheck: on` (or `required`) in aube-workspace.yaml to check dependencies against known-malicious advisories.",
|
|
1247
|
-
value: "on"
|
|
1248
|
-
} },
|
|
1249
|
-
description: "Query the OSV database for known-malicious packages during dependency resolution.",
|
|
1250
|
-
docs: "https://aube.jdx.dev/security.html",
|
|
1251
|
-
id: "advisory-check",
|
|
1252
|
-
severity: "warn",
|
|
1253
|
-
title: "Enable malicious-package advisory checks"
|
|
1254
|
-
}));
|
|
1255
|
-
//#endregion
|
|
1256
|
-
//#region src/domain/rules/config-predicates.ts
|
|
1257
|
-
const isNonBlankString = (value) => typeof value === "string" && value.trim().length > 0;
|
|
1258
|
-
const isStringList = (value) => Array.isArray(value) && Array.from(value).every(isNonBlankString);
|
|
1259
978
|
//#endregion
|
|
1260
|
-
//#region src/domain/rules/
|
|
1261
|
-
const { yarnrc: yarnrc$
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
979
|
+
//#region src/domain/rules/disable-lifecycle-scripts.ts
|
|
980
|
+
const { npmrc: npmrc$5, pnpmWorkspace: pnpmWorkspace$8, yarnrc: yarnrc$6, aubeWorkspace: aubeWorkspace$5, bunfig: bunfig$3 } = CONFIG_FILES;
|
|
981
|
+
const pnpmBinding$4 = {
|
|
982
|
+
check(ctx, config) {
|
|
983
|
+
if (getByPath(config, ["ignoreScripts"]) === true) return { state: "ok" };
|
|
984
|
+
const bypass = getByPath(config, ["dangerouslyAllowAllBuilds"]);
|
|
985
|
+
const strict = getByPath(config, ["strictDepBuilds"]);
|
|
986
|
+
if (bypass === true) {
|
|
987
|
+
const bypassTarget = {
|
|
988
|
+
file: pnpmWorkspace$8,
|
|
989
|
+
keyPath: ["dangerouslyAllowAllBuilds"]
|
|
990
|
+
};
|
|
991
|
+
if (settingSupportedByTarget("pnpm", ctx.pmVersion, bypassTarget) === false) {
|
|
992
|
+
const proposal = {
|
|
993
|
+
kind: "manual",
|
|
994
|
+
steps: ["Remove `dangerouslyAllowAllBuilds: true` before upgrading to pnpm 10.9.0 or newer; the declared target ignores it, but a later version would activate the bypass.", ...strict === true ? [] : ["Set `strictDepBuilds: true` in pnpm-workspace.yaml to enable lifecycle-script gating for the declared target."]]
|
|
995
|
+
};
|
|
996
|
+
return {
|
|
997
|
+
state: "violation",
|
|
998
|
+
actual: bypass,
|
|
999
|
+
expected: false,
|
|
1000
|
+
message: "The declared target ignores this future-version bypass. Removing the ignored bypass alone does not provide protection against lifecycle scripts, and leaving it configured would disable gating after an upgrade.",
|
|
1001
|
+
remediation: strict === true ? proposal : guardRemediationAvailability("pnpm", ctx.pmVersion, proposal, [{
|
|
1002
|
+
file: pnpmWorkspace$8,
|
|
1003
|
+
keyPath: ["strictDepBuilds"]
|
|
1004
|
+
}])
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const proposal = {
|
|
1008
|
+
kind: "manual",
|
|
1009
|
+
steps: ["After upgrading, remove `dangerouslyAllowAllBuilds: true` (or set it to false) and configure lifecycle-script gating."]
|
|
1010
|
+
};
|
|
1011
|
+
const guarded = guardRemediationAvailability("pnpm", ctx.pmVersion, proposal, [{
|
|
1012
|
+
file: pnpmWorkspace$8,
|
|
1013
|
+
keyPath: ["strictDepBuilds"]
|
|
1014
|
+
}, {
|
|
1015
|
+
file: pnpmWorkspace$8,
|
|
1016
|
+
keyPath: ["dangerouslyAllowAllBuilds"]
|
|
1017
|
+
}]);
|
|
1018
|
+
if (guarded !== proposal) return {
|
|
1019
|
+
state: "violation",
|
|
1020
|
+
actual: bypass,
|
|
1021
|
+
expected: false,
|
|
1022
|
+
message: "The target cannot use the proposed YAML lifecycle-script controls. Removing an unsupported bypass alone does not provide protection.",
|
|
1023
|
+
remediation: guarded
|
|
1024
|
+
};
|
|
1268
1025
|
return {
|
|
1026
|
+
actual: bypass,
|
|
1027
|
+
expected: false,
|
|
1269
1028
|
remediation: {
|
|
1270
1029
|
kind: "manual",
|
|
1271
|
-
steps: ["
|
|
1030
|
+
steps: ["Remove `dangerouslyAllowAllBuilds: true` from pnpm-workspace.yaml (or set it to `false`). Setting `strictDepBuilds: true` alone has no effect while the bypass remains."]
|
|
1272
1031
|
},
|
|
1273
|
-
|
|
1274
|
-
expected: "[]",
|
|
1275
|
-
message,
|
|
1032
|
+
message: "`dangerouslyAllowAllBuilds: true` in pnpm-workspace.yaml bypasses strictDepBuilds — remove it (or set it to false) to restore lifecycle-script gating.",
|
|
1276
1033
|
state: "violation"
|
|
1277
1034
|
};
|
|
1278
|
-
},
|
|
1279
|
-
docs: "https://yarnpkg.com/configuration/yarnrc#approvedGitRepositories",
|
|
1280
|
-
file: yarnrc$9,
|
|
1281
|
-
versionNote: { configAvailableSince: "yarn 4.14.0" }
|
|
1282
|
-
} },
|
|
1283
|
-
description: "Restrict git: protocol dependencies to an explicit allowlist of approved repository URL patterns.",
|
|
1284
|
-
docs: "https://yarnpkg.com/configuration/yarnrc#approvedGitRepositories",
|
|
1285
|
-
id: "approved-git-repos",
|
|
1286
|
-
severity: "warn",
|
|
1287
|
-
title: "Approve git repository dependencies"
|
|
1288
|
-
});
|
|
1289
|
-
//#endregion
|
|
1290
|
-
//#region src/domain/rules/audit-suppression.ts
|
|
1291
|
-
const { yarnrc: yarnrc$8 } = CONFIG_FILES;
|
|
1292
|
-
const isNonEmptyArray = (value) => Array.isArray(value) && value.length > 0;
|
|
1293
|
-
const SUPPRESSION_KEYS = ["npmAuditIgnoreAdvisories", "npmAuditExcludePackages"];
|
|
1294
|
-
const checkSuppression = (config) => {
|
|
1295
|
-
const present = SUPPRESSION_KEYS.filter((key) => isNonEmptyArray(getByPath(config, [key])));
|
|
1296
|
-
const [first] = present;
|
|
1297
|
-
if (typeof first === "undefined") return { state: "ok" };
|
|
1298
|
-
return {
|
|
1299
|
-
actual: getByPath(config, [first]),
|
|
1300
|
-
message: `Review audit suppression in .yarnrc.yml (${present.join(", ")}). Broad glob patterns can silently hide future vulnerabilities.`,
|
|
1301
|
-
state: "violation",
|
|
1302
|
-
remediation: {
|
|
1303
|
-
kind: "manual",
|
|
1304
|
-
steps: ["Review entries in `npmAuditIgnoreAdvisories` and `npmAuditExcludePackages` — remove stale suppressions and overly broad glob patterns."]
|
|
1305
1035
|
}
|
|
1306
|
-
|
|
1307
|
-
};
|
|
1308
|
-
const auditSuppression = defineRule({
|
|
1309
|
-
bindings: { yarn: {
|
|
1310
|
-
check(_ctx, config) {
|
|
1311
|
-
return checkSuppression(config);
|
|
1312
|
-
},
|
|
1313
|
-
docs: "https://yarnpkg.com/configuration/yarnrc#npmAuditIgnoreAdvisories",
|
|
1314
|
-
file: yarnrc$8
|
|
1315
|
-
} },
|
|
1316
|
-
description: "Flag audit advisory suppressions that may silently hide future vulnerabilities via broad glob patterns.",
|
|
1317
|
-
docs: "https://yarnpkg.com/configuration/yarnrc#npmAuditIgnoreAdvisories",
|
|
1318
|
-
id: "audit-suppression",
|
|
1319
|
-
severity: "info",
|
|
1320
|
-
title: "Review audit suppression entries"
|
|
1321
|
-
});
|
|
1322
|
-
//#endregion
|
|
1323
|
-
//#region src/domain/rules/block-auto-install.ts
|
|
1324
|
-
const { bunfig: bunfig$5 } = CONFIG_FILES;
|
|
1325
|
-
const blockAutoInstall = requireConfigKey({
|
|
1326
|
-
bindings: { bun: {
|
|
1327
|
-
docs: "https://bun.sh/docs/runtime/bunfig#install-auto",
|
|
1328
|
-
file: bunfig$5,
|
|
1329
|
-
keyPath: ["install", "auto"],
|
|
1330
|
-
message: "Set `auto = \"disable\"` under [install] in bunfig.toml to prevent auto-install from bypassing security guards.",
|
|
1331
|
-
value: "disable",
|
|
1332
|
-
accept: (value) => value === "disable" || value === false
|
|
1333
|
-
} },
|
|
1334
|
-
description: "Disable auto-install so dependencies are only installed through an explicit install step where security guards apply.",
|
|
1335
|
-
docs: "https://bun.sh/docs/runtime/bunfig#install-auto",
|
|
1336
|
-
id: "block-auto-install",
|
|
1337
|
-
severity: "warn",
|
|
1338
|
-
title: "Block automatic package installation"
|
|
1339
|
-
});
|
|
1340
|
-
//#endregion
|
|
1341
|
-
//#region src/domain/rules/block-exotic-subdeps.ts
|
|
1342
|
-
const { aubeWorkspace: aubeWorkspace$7, npmrc: npmrc$6, pnpmWorkspace: pnpmWorkspace$10 } = CONFIG_FILES;
|
|
1343
|
-
const isRootOrNone = (value) => value === "root" || value === "none";
|
|
1344
|
-
const npmMessage = "Set `allow-git=none` and `allow-remote=none` in .npmrc to block git/tarball URLs; use `root` only when direct URL dependencies are required.";
|
|
1345
|
-
const npmBinding$2 = {
|
|
1346
|
-
check(_ctx, config) {
|
|
1347
|
-
const git = getByPath(config, ["allow-git"]);
|
|
1348
|
-
const remote = getByPath(config, ["allow-remote"]);
|
|
1349
|
-
const invalid = [git, remote].find((value) => value !== void 0 && !isRootOrNone(value));
|
|
1350
|
-
if (invalid === void 0 && git !== void 0 && remote !== void 0) return { state: "ok" };
|
|
1036
|
+
if (strict === true) return { state: "ok" };
|
|
1351
1037
|
return {
|
|
1352
1038
|
state: "violation",
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
message:
|
|
1356
|
-
remediation: proposeChanges(config, [{
|
|
1357
|
-
file:
|
|
1358
|
-
keyPath: ["
|
|
1359
|
-
op: "setKey",
|
|
1360
|
-
value: "none"
|
|
1361
|
-
}, {
|
|
1362
|
-
file: npmrc$6,
|
|
1363
|
-
keyPath: ["allow-remote"],
|
|
1039
|
+
actual: strict,
|
|
1040
|
+
expected: true,
|
|
1041
|
+
message: strict === void 0 ? "Set `strictDepBuilds: true` in pnpm-workspace.yaml to pin lifecycle-script gating across versions." : "Set `strictDepBuilds: true` in pnpm-workspace.yaml to block silent skips of un-approved dep builds.",
|
|
1042
|
+
remediation: guardRemediationAvailability("pnpm", ctx.pmVersion, proposeChanges(config, [{
|
|
1043
|
+
file: pnpmWorkspace$8,
|
|
1044
|
+
keyPath: ["strictDepBuilds"],
|
|
1364
1045
|
op: "setKey",
|
|
1365
|
-
value:
|
|
1046
|
+
value: true
|
|
1047
|
+
}]), [{
|
|
1048
|
+
file: pnpmWorkspace$8,
|
|
1049
|
+
keyPath: ["strictDepBuilds"]
|
|
1366
1050
|
}])
|
|
1367
1051
|
};
|
|
1368
1052
|
},
|
|
1369
|
-
docs: "https://
|
|
1370
|
-
file:
|
|
1371
|
-
versionNote: {
|
|
1053
|
+
docs: "https://pnpm.io/settings#strictdepbuilds",
|
|
1054
|
+
file: pnpmWorkspace$8,
|
|
1055
|
+
versionNote: {
|
|
1056
|
+
configAvailableSince: "pnpm 10.3.0",
|
|
1057
|
+
note: "pnpm-workspace.yaml settings require pnpm 10.6.0",
|
|
1058
|
+
defaultSafeSince: "pnpm 11.0.0"
|
|
1059
|
+
}
|
|
1372
1060
|
};
|
|
1373
|
-
const
|
|
1374
|
-
bindings: {
|
|
1375
|
-
aube: {
|
|
1376
|
-
docs: "https://aube.jdx.dev/security.html",
|
|
1377
|
-
documentedDefault: true,
|
|
1378
|
-
file: aubeWorkspace$7,
|
|
1379
|
-
keyPath: ["blockExoticSubdeps"],
|
|
1380
|
-
message: "Set `blockExoticSubdeps: true` in aube-workspace.yaml to block git/tarball URLs in transitive dependencies.",
|
|
1381
|
-
value: true
|
|
1382
|
-
},
|
|
1383
|
-
pnpm: {
|
|
1384
|
-
docs: "https://pnpm.io/settings#blockexoticsubdeps",
|
|
1385
|
-
documentedDefault: true,
|
|
1386
|
-
file: pnpmWorkspace$10,
|
|
1387
|
-
keyPath: ["blockExoticSubdeps"],
|
|
1388
|
-
message: "Set `blockExoticSubdeps: true` in pnpm-workspace.yaml to block git/tarball URLs in transitive dependencies.",
|
|
1389
|
-
value: true,
|
|
1390
|
-
versionNote: {
|
|
1391
|
-
configAvailableSince: "pnpm 10.26.0",
|
|
1392
|
-
defaultSafeSince: "pnpm 10.26.0"
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
},
|
|
1396
|
-
description: "Refuse to install transitive dependencies sourced from git or tarball URLs, which bypass registry integrity checking.",
|
|
1397
|
-
docs: "https://pnpm.io/settings#blockexoticsubdeps",
|
|
1398
|
-
id: "block-exotic-subdeps",
|
|
1399
|
-
severity: "warn",
|
|
1400
|
-
title: "Block exotic subdependencies"
|
|
1401
|
-
}), { npm: npmBinding$2 });
|
|
1402
|
-
//#endregion
|
|
1403
|
-
//#region src/domain/rules/bun-security-scanner.ts
|
|
1404
|
-
const { bunfig: bunfig$4 } = CONFIG_FILES;
|
|
1405
|
-
const bunSecurityScanner = defineRule({
|
|
1406
|
-
bindings: { bun: {
|
|
1407
|
-
check(ctx, config) {
|
|
1408
|
-
const scanner = getByPath(config, [
|
|
1409
|
-
"install",
|
|
1410
|
-
"security",
|
|
1411
|
-
"scanner"
|
|
1412
|
-
]);
|
|
1413
|
-
if (isNonBlankString(scanner)) return { state: "ok" };
|
|
1414
|
-
return {
|
|
1415
|
-
remediation: guardRemediationAvailability("bun", ctx.pmVersion, {
|
|
1416
|
-
kind: "manual",
|
|
1417
|
-
steps: ["Add `[install.security] scanner = \"@socketsecurity/bun-security-scanner\"` (or another bun-compatible scanner) to bunfig.toml."]
|
|
1418
|
-
}, [{
|
|
1419
|
-
file: bunfig$4,
|
|
1420
|
-
keyPath: [
|
|
1421
|
-
"install",
|
|
1422
|
-
"security",
|
|
1423
|
-
"scanner"
|
|
1424
|
-
]
|
|
1425
|
-
}]),
|
|
1426
|
-
actual: scanner,
|
|
1427
|
-
message: "Configure `[install.security] scanner = \"...\"` in bunfig.toml (e.g. `@socketsecurity/bun-security-scanner`) to scan new packages on install.",
|
|
1428
|
-
state: "violation"
|
|
1429
|
-
};
|
|
1430
|
-
},
|
|
1431
|
-
docs: "https://bun.com/docs/pm/security-scanner-api",
|
|
1432
|
-
file: bunfig$4,
|
|
1433
|
-
versionNote: { configAvailableSince: "bun 1.2.21" }
|
|
1434
|
-
} },
|
|
1435
|
-
description: "Bun supports a Security Scanner API that intercepts new packages at install time (e.g. Socket Firewall).",
|
|
1436
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#preinstall-scanners",
|
|
1437
|
-
id: "bun-security-scanner",
|
|
1438
|
-
severity: "info",
|
|
1439
|
-
title: "Enable a bun install-time security scanner"
|
|
1440
|
-
});
|
|
1441
|
-
//#endregion
|
|
1442
|
-
//#region src/domain/rules/checksum-verification.ts
|
|
1443
|
-
const { yarnrc: yarnrc$7 } = CONFIG_FILES;
|
|
1444
|
-
const checksumVerification = requireConfigKey({
|
|
1445
|
-
bindings: { yarn: {
|
|
1446
|
-
docs: "https://yarnpkg.com/configuration/yarnrc#checksumBehavior",
|
|
1447
|
-
documentedDefault: "throw",
|
|
1448
|
-
file: yarnrc$7,
|
|
1449
|
-
keyPath: ["checksumBehavior"],
|
|
1450
|
-
message: "Set `checksumBehavior: throw` in .yarnrc.yml to reject packages whose checksum does not match the lockfile.",
|
|
1451
|
-
value: "throw"
|
|
1452
|
-
} },
|
|
1453
|
-
description: "Throw on checksum mismatches so tampered or corrupted packages are never silently installed.",
|
|
1454
|
-
docs: "https://yarnpkg.com/configuration/yarnrc#checksumBehavior",
|
|
1455
|
-
id: "checksum-verification",
|
|
1456
|
-
severity: "warn",
|
|
1457
|
-
title: "Verify package checksums"
|
|
1458
|
-
});
|
|
1459
|
-
//#endregion
|
|
1460
|
-
//#region src/domain/entities/signals.ts
|
|
1461
|
-
const PM_SIGNALS = {
|
|
1462
|
-
aube: {
|
|
1463
|
-
configs: [CONFIG_FILES.aubeWorkspace.path],
|
|
1464
|
-
lockfiles: ["aube-lock.yaml"],
|
|
1465
|
-
reusesLockfiles: [
|
|
1466
|
-
"pnpm-lock.yaml",
|
|
1467
|
-
"package-lock.json",
|
|
1468
|
-
"npm-shrinkwrap.json",
|
|
1469
|
-
"yarn.lock",
|
|
1470
|
-
"bun.lock"
|
|
1471
|
-
]
|
|
1472
|
-
},
|
|
1473
|
-
bun: {
|
|
1474
|
-
configs: [CONFIG_FILES.bunfig.path],
|
|
1475
|
-
lockfiles: ["bun.lock", "bun.lockb"]
|
|
1476
|
-
},
|
|
1477
|
-
deno: {
|
|
1478
|
-
configs: [CONFIG_FILES.denoJson.path, "deno.jsonc"],
|
|
1479
|
-
lockfiles: ["deno.lock"]
|
|
1480
|
-
},
|
|
1481
|
-
npm: {
|
|
1482
|
-
configs: [],
|
|
1483
|
-
lockfiles: ["package-lock.json", "npm-shrinkwrap.json"]
|
|
1484
|
-
},
|
|
1485
|
-
pnpm: {
|
|
1486
|
-
configs: [CONFIG_FILES.pnpmWorkspace.path],
|
|
1487
|
-
lockfiles: ["pnpm-lock.yaml"]
|
|
1488
|
-
},
|
|
1489
|
-
yarn: {
|
|
1490
|
-
configs: [CONFIG_FILES.yarnrc.path],
|
|
1491
|
-
lockfiles: ["yarn.lock"]
|
|
1492
|
-
}
|
|
1493
|
-
};
|
|
1494
|
-
//#endregion
|
|
1495
|
-
//#region src/domain/rules/commit-lockfile.ts
|
|
1496
|
-
const LOCKFILE_DOCS = {
|
|
1497
|
-
aube: "https://github.com/aubepkg/aube/blob/main/docs/package-manager/lockfiles.md",
|
|
1498
|
-
bun: "https://bun.com/docs/install/lockfile",
|
|
1499
|
-
deno: "https://docs.deno.com/runtime/fundamentals/modules/#integrity-checking-and-lock-files",
|
|
1500
|
-
npm: "https://docs.npmjs.com/cli/v12/configuring-npm/package-lock-json",
|
|
1501
|
-
pnpm: "https://pnpm.io/git#lockfiles",
|
|
1502
|
-
yarn: "https://yarnpkg.com/getting-started/qa#should-lockfiles-be-committed-to-the-repository"
|
|
1503
|
-
};
|
|
1504
|
-
const LOCKFILE_VERSION_NOTES = { deno: { configAvailableSince: "deno 1.28.0" } };
|
|
1505
|
-
const lockfileBinding = (pm) => {
|
|
1506
|
-
const { lockfiles, reusesLockfiles } = PM_SIGNALS[pm];
|
|
1507
|
-
const [primary] = lockfiles;
|
|
1508
|
-
const accepted = [...lockfiles, ...reusesLockfiles ?? []].filter((lockfile) => pm !== "npm" || lockfile === primary);
|
|
1509
|
-
return {
|
|
1510
|
-
check(ctx) {
|
|
1511
|
-
if (pm === "aube" && ctx.exists(asRelPath("bun.lockb")) && !ctx.exists(asRelPath("bun.lock"))) return {
|
|
1512
|
-
state: "violation",
|
|
1513
|
-
message: "Aube cannot read bun.lockb, even when another lockfile exists.",
|
|
1514
|
-
remediation: {
|
|
1515
|
-
kind: "manual",
|
|
1516
|
-
steps: ["Run `bun install --save-text-lockfile` to generate bun.lock, then commit it."]
|
|
1517
|
-
}
|
|
1518
|
-
};
|
|
1519
|
-
if (accepted.some((lf) => ctx.exists(asRelPath(lf)))) return { state: "ok" };
|
|
1520
|
-
return {
|
|
1521
|
-
remediation: {
|
|
1522
|
-
kind: "manual",
|
|
1523
|
-
steps: [`Install dependencies to generate ${primary}, then commit it.`]
|
|
1524
|
-
},
|
|
1525
|
-
message: `No lockfile found. Generate and commit ${primary}.`,
|
|
1526
|
-
state: "violation"
|
|
1527
|
-
};
|
|
1528
|
-
},
|
|
1529
|
-
docs: LOCKFILE_DOCS[pm],
|
|
1530
|
-
versionNote: LOCKFILE_VERSION_NOTES[pm]
|
|
1531
|
-
};
|
|
1532
|
-
};
|
|
1533
|
-
const denoBinding$2 = {
|
|
1534
|
-
file: CONFIG_FILES.denoJson,
|
|
1535
|
-
docs: LOCKFILE_DOCS.deno,
|
|
1536
|
-
versionNote: LOCKFILE_VERSION_NOTES.deno,
|
|
1061
|
+
const aubeBinding$2 = {
|
|
1537
1062
|
check(ctx, config) {
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1063
|
+
const npmConfig = ctx.readConfig(npmrc$5);
|
|
1064
|
+
const jail = getByPath(config, ["jailBuilds"]);
|
|
1065
|
+
const strict = getByPath(npmConfig, ["strictDepBuilds"]);
|
|
1066
|
+
if (jail === true && strict === true) return { state: "ok" };
|
|
1067
|
+
const violations = [];
|
|
1068
|
+
if (jail !== true) violations.push({
|
|
1540
1069
|
state: "violation",
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1070
|
+
file: aubeWorkspace$5.path,
|
|
1071
|
+
actual: jail,
|
|
1072
|
+
expected: true,
|
|
1073
|
+
message: "Set `jailBuilds: true` in aube-workspace.yaml to sandbox approved builds.",
|
|
1074
|
+
remediation: proposeChanges(config, [{
|
|
1075
|
+
file: aubeWorkspace$5,
|
|
1076
|
+
keyPath: ["jailBuilds"],
|
|
1077
|
+
op: "setKey",
|
|
1078
|
+
value: true
|
|
1079
|
+
}])
|
|
1080
|
+
});
|
|
1081
|
+
if (strict !== true) violations.push({
|
|
1082
|
+
state: "violation",
|
|
1083
|
+
file: npmrc$5.path,
|
|
1084
|
+
actual: strict,
|
|
1085
|
+
expected: true,
|
|
1086
|
+
message: "Set `strictDepBuilds=true` in .npmrc to reject unreviewed lifecycle scripts.",
|
|
1087
|
+
remediation: proposeChanges(npmConfig, [{
|
|
1088
|
+
file: npmrc$5,
|
|
1089
|
+
keyPath: ["strictDepBuilds"],
|
|
1090
|
+
op: "setKey",
|
|
1091
|
+
value: true
|
|
1092
|
+
}])
|
|
1093
|
+
});
|
|
1094
|
+
const [first, ...rest] = violations;
|
|
1095
|
+
return !first ? { state: "ok" } : rest.length === 0 ? first : {
|
|
1096
|
+
state: "violations",
|
|
1097
|
+
violations: [first, ...rest]
|
|
1546
1098
|
};
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1099
|
+
},
|
|
1100
|
+
docs: "https://aube.jdx.dev/security.html",
|
|
1101
|
+
file: aubeWorkspace$5
|
|
1102
|
+
};
|
|
1103
|
+
const bunMessage = "Set `ignoreScripts = true` under [install] in bunfig.toml — or set `\"trustedDependencies\": []` in package.json — to opt out of the curated allow-list (postinstall is already blocked for untrusted packages by default).";
|
|
1104
|
+
const bunBinding = {
|
|
1105
|
+
check(ctx, config) {
|
|
1106
|
+
const ignoreScripts = getByPath(config, ["install", "ignoreScripts"]);
|
|
1107
|
+
if (ignoreScripts === true) return { state: "ok" };
|
|
1108
|
+
const trusted = ctx.packageJson?.trustedDependencies;
|
|
1109
|
+
if (typeof trusted !== "undefined" && trusted.length === 0) return { state: "ok" };
|
|
1550
1110
|
return {
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
}
|
|
1111
|
+
remediation: proposeChanges(config, [{
|
|
1112
|
+
file: bunfig$3,
|
|
1113
|
+
keyPath: ["install", "ignoreScripts"],
|
|
1114
|
+
op: "setKey",
|
|
1115
|
+
value: true
|
|
1116
|
+
}]),
|
|
1117
|
+
actual: ignoreScripts,
|
|
1118
|
+
expected: true,
|
|
1119
|
+
message: bunMessage,
|
|
1120
|
+
state: "violation"
|
|
1557
1121
|
};
|
|
1558
|
-
}
|
|
1122
|
+
},
|
|
1123
|
+
docs: "https://bun.com/docs/pm/lifecycle",
|
|
1124
|
+
file: bunfig$3,
|
|
1125
|
+
severity: "info",
|
|
1126
|
+
versionNote: { configAvailableSince: "bun 1.2.0" }
|
|
1559
1127
|
};
|
|
1560
|
-
const
|
|
1128
|
+
const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigKey({
|
|
1561
1129
|
bindings: {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1130
|
+
npm: {
|
|
1131
|
+
docs: "https://docs.npmjs.com/cli/v11/using-npm/config#ignore-scripts",
|
|
1132
|
+
file: npmrc$5,
|
|
1133
|
+
keyPath: ["ignore-scripts"],
|
|
1134
|
+
message: "Set `ignore-scripts=true` in .npmrc to block dependency lifecycle scripts.",
|
|
1135
|
+
value: true
|
|
1136
|
+
},
|
|
1137
|
+
yarn: {
|
|
1138
|
+
docs: "https://yarnpkg.com/configuration/yarnrc#enableScripts",
|
|
1139
|
+
documentedDefault: false,
|
|
1140
|
+
file: yarnrc$6,
|
|
1141
|
+
keyPath: ["enableScripts"],
|
|
1142
|
+
message: "Set `enableScripts: false` in .yarnrc.yml to pin the policy across versions.",
|
|
1143
|
+
value: false,
|
|
1144
|
+
versionNote: {
|
|
1145
|
+
configAvailableSince: "yarn 2.0.0",
|
|
1146
|
+
defaultSafeSince: "yarn 4.14.0"
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1568
1149
|
},
|
|
1569
|
-
description: "
|
|
1570
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#
|
|
1571
|
-
id: "
|
|
1150
|
+
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#3-disable-lifecycle-scripts",
|
|
1152
|
+
id: "disable-lifecycle-scripts",
|
|
1572
1153
|
severity: "error",
|
|
1573
|
-
title: "
|
|
1574
|
-
})
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
const checkOverrides = (config, file) => {
|
|
1580
|
-
const value = getByPath(config, ["overrides"]);
|
|
1581
|
-
if (!isNonEmptyObject$2(value)) return { state: "ok" };
|
|
1582
|
-
return {
|
|
1583
|
-
actual: value,
|
|
1584
|
-
message: `Review \`overrides\` in ${file}. Overrides can replace transitive dependencies with arbitrary versions or forks.`,
|
|
1585
|
-
state: "violation",
|
|
1586
|
-
remediation: {
|
|
1587
|
-
kind: "manual",
|
|
1588
|
-
steps: ["Review each entry in `overrides` — verify pinned versions address a known CVE and are not redirecting to untrusted packages."]
|
|
1589
|
-
}
|
|
1590
|
-
};
|
|
1591
|
-
};
|
|
1592
|
-
const makeBinding = (file, fileName, docs) => ({
|
|
1593
|
-
check(_ctx, config) {
|
|
1594
|
-
return checkOverrides(config, fileName);
|
|
1595
|
-
},
|
|
1596
|
-
docs,
|
|
1597
|
-
file
|
|
1598
|
-
});
|
|
1599
|
-
const dependencyOverrides = defineRule({
|
|
1600
|
-
bindings: {
|
|
1601
|
-
aube: makeBinding(aubeWorkspace$6, "aube-workspace.yaml", "https://aube.sh/settings/"),
|
|
1602
|
-
pnpm: makeBinding(pnpmWorkspace$9, "pnpm-workspace.yaml", "https://pnpm.io/settings#overrides")
|
|
1603
|
-
},
|
|
1604
|
-
description: "Flag dependency overrides that can replace transitive packages with arbitrary versions or forks — a supply-chain injection vector.",
|
|
1605
|
-
docs: "https://pnpm.io/settings#overrides",
|
|
1606
|
-
id: "dependency-overrides",
|
|
1607
|
-
severity: "info",
|
|
1608
|
-
title: "Review dependency overrides"
|
|
1609
|
-
});
|
|
1610
|
-
//#endregion
|
|
1611
|
-
//#region src/domain/rules/disable-lifecycle-scripts.ts
|
|
1612
|
-
const { npmrc: npmrc$5, pnpmWorkspace: pnpmWorkspace$8, yarnrc: yarnrc$6, aubeWorkspace: aubeWorkspace$5, bunfig: bunfig$3 } = CONFIG_FILES;
|
|
1613
|
-
const pnpmBinding$4 = {
|
|
1614
|
-
check(ctx, config) {
|
|
1615
|
-
if (getByPath(config, ["ignoreScripts"]) === true) return { state: "ok" };
|
|
1616
|
-
const bypass = getByPath(config, ["dangerouslyAllowAllBuilds"]);
|
|
1617
|
-
const strict = getByPath(config, ["strictDepBuilds"]);
|
|
1618
|
-
if (bypass === true) {
|
|
1619
|
-
const bypassTarget = {
|
|
1620
|
-
file: pnpmWorkspace$8,
|
|
1621
|
-
keyPath: ["dangerouslyAllowAllBuilds"]
|
|
1622
|
-
};
|
|
1623
|
-
if (settingSupportedByTarget("pnpm", ctx.pmVersion, bypassTarget) === false) {
|
|
1624
|
-
const proposal = {
|
|
1625
|
-
kind: "manual",
|
|
1626
|
-
steps: ["Remove `dangerouslyAllowAllBuilds: true` before upgrading to pnpm 10.9.0 or newer; the declared target ignores it, but a later version would activate the bypass.", ...strict === true ? [] : ["Set `strictDepBuilds: true` in pnpm-workspace.yaml to enable lifecycle-script gating for the declared target."]]
|
|
1627
|
-
};
|
|
1628
|
-
return {
|
|
1629
|
-
state: "violation",
|
|
1630
|
-
actual: bypass,
|
|
1631
|
-
expected: false,
|
|
1632
|
-
message: "The declared target ignores this future-version bypass. Removing the ignored bypass alone does not provide protection against lifecycle scripts, and leaving it configured would disable gating after an upgrade.",
|
|
1633
|
-
remediation: strict === true ? proposal : guardRemediationAvailability("pnpm", ctx.pmVersion, proposal, [{
|
|
1634
|
-
file: pnpmWorkspace$8,
|
|
1635
|
-
keyPath: ["strictDepBuilds"]
|
|
1636
|
-
}])
|
|
1637
|
-
};
|
|
1638
|
-
}
|
|
1639
|
-
const proposal = {
|
|
1640
|
-
kind: "manual",
|
|
1641
|
-
steps: ["After upgrading, remove `dangerouslyAllowAllBuilds: true` (or set it to false) and configure lifecycle-script gating."]
|
|
1642
|
-
};
|
|
1643
|
-
const guarded = guardRemediationAvailability("pnpm", ctx.pmVersion, proposal, [{
|
|
1644
|
-
file: pnpmWorkspace$8,
|
|
1645
|
-
keyPath: ["strictDepBuilds"]
|
|
1646
|
-
}, {
|
|
1647
|
-
file: pnpmWorkspace$8,
|
|
1648
|
-
keyPath: ["dangerouslyAllowAllBuilds"]
|
|
1649
|
-
}]);
|
|
1650
|
-
if (guarded !== proposal) return {
|
|
1651
|
-
state: "violation",
|
|
1652
|
-
actual: bypass,
|
|
1653
|
-
expected: false,
|
|
1654
|
-
message: "The target cannot use the proposed YAML lifecycle-script controls. Removing an unsupported bypass alone does not provide protection.",
|
|
1655
|
-
remediation: guarded
|
|
1656
|
-
};
|
|
1657
|
-
return {
|
|
1658
|
-
actual: bypass,
|
|
1659
|
-
expected: false,
|
|
1660
|
-
remediation: {
|
|
1661
|
-
kind: "manual",
|
|
1662
|
-
steps: ["Remove `dangerouslyAllowAllBuilds: true` from pnpm-workspace.yaml (or set it to `false`). Setting `strictDepBuilds: true` alone has no effect while the bypass remains."]
|
|
1663
|
-
},
|
|
1664
|
-
message: "`dangerouslyAllowAllBuilds: true` in pnpm-workspace.yaml bypasses strictDepBuilds — remove it (or set it to false) to restore lifecycle-script gating.",
|
|
1665
|
-
state: "violation"
|
|
1666
|
-
};
|
|
1667
|
-
}
|
|
1668
|
-
if (strict === true) return { state: "ok" };
|
|
1669
|
-
return {
|
|
1670
|
-
state: "violation",
|
|
1671
|
-
actual: strict,
|
|
1672
|
-
expected: true,
|
|
1673
|
-
message: strict === void 0 ? "Set `strictDepBuilds: true` in pnpm-workspace.yaml to pin lifecycle-script gating across versions." : "Set `strictDepBuilds: true` in pnpm-workspace.yaml to block silent skips of un-approved dep builds.",
|
|
1674
|
-
remediation: guardRemediationAvailability("pnpm", ctx.pmVersion, proposeChanges(config, [{
|
|
1675
|
-
file: pnpmWorkspace$8,
|
|
1676
|
-
keyPath: ["strictDepBuilds"],
|
|
1677
|
-
op: "setKey",
|
|
1678
|
-
value: true
|
|
1679
|
-
}]), [{
|
|
1680
|
-
file: pnpmWorkspace$8,
|
|
1681
|
-
keyPath: ["strictDepBuilds"]
|
|
1682
|
-
}])
|
|
1683
|
-
};
|
|
1684
|
-
},
|
|
1685
|
-
docs: "https://pnpm.io/settings#strictdepbuilds",
|
|
1686
|
-
file: pnpmWorkspace$8,
|
|
1687
|
-
versionNote: {
|
|
1688
|
-
configAvailableSince: "pnpm 10.3.0",
|
|
1689
|
-
note: "pnpm-workspace.yaml settings require pnpm 10.6.0",
|
|
1690
|
-
defaultSafeSince: "pnpm 11.0.0"
|
|
1691
|
-
}
|
|
1692
|
-
};
|
|
1693
|
-
const aubeBinding$2 = {
|
|
1694
|
-
check(ctx, config) {
|
|
1695
|
-
const npmConfig = ctx.readConfig(npmrc$5);
|
|
1696
|
-
const jail = getByPath(config, ["jailBuilds"]);
|
|
1697
|
-
const strict = getByPath(npmConfig, ["strictDepBuilds"]);
|
|
1698
|
-
if (jail === true && strict === true) return { state: "ok" };
|
|
1699
|
-
const violations = [];
|
|
1700
|
-
if (jail !== true) violations.push({
|
|
1701
|
-
state: "violation",
|
|
1702
|
-
file: aubeWorkspace$5.path,
|
|
1703
|
-
actual: jail,
|
|
1704
|
-
expected: true,
|
|
1705
|
-
message: "Set `jailBuilds: true` in aube-workspace.yaml to sandbox approved builds.",
|
|
1706
|
-
remediation: proposeChanges(config, [{
|
|
1707
|
-
file: aubeWorkspace$5,
|
|
1708
|
-
keyPath: ["jailBuilds"],
|
|
1709
|
-
op: "setKey",
|
|
1710
|
-
value: true
|
|
1711
|
-
}])
|
|
1712
|
-
});
|
|
1713
|
-
if (strict !== true) violations.push({
|
|
1714
|
-
state: "violation",
|
|
1715
|
-
file: npmrc$5.path,
|
|
1716
|
-
actual: strict,
|
|
1717
|
-
expected: true,
|
|
1718
|
-
message: "Set `strictDepBuilds=true` in .npmrc to reject unreviewed lifecycle scripts.",
|
|
1719
|
-
remediation: proposeChanges(npmConfig, [{
|
|
1720
|
-
file: npmrc$5,
|
|
1721
|
-
keyPath: ["strictDepBuilds"],
|
|
1722
|
-
op: "setKey",
|
|
1723
|
-
value: true
|
|
1724
|
-
}])
|
|
1725
|
-
});
|
|
1726
|
-
const [first, ...rest] = violations;
|
|
1727
|
-
return !first ? { state: "ok" } : rest.length === 0 ? first : {
|
|
1728
|
-
state: "violations",
|
|
1729
|
-
violations: [first, ...rest]
|
|
1730
|
-
};
|
|
1731
|
-
},
|
|
1732
|
-
docs: "https://aube.jdx.dev/security.html",
|
|
1733
|
-
file: aubeWorkspace$5
|
|
1734
|
-
};
|
|
1735
|
-
const bunMessage = "Set `ignoreScripts = true` under [install] in bunfig.toml — or set `\"trustedDependencies\": []` in package.json — to opt out of the curated allow-list (postinstall is already blocked for untrusted packages by default).";
|
|
1736
|
-
const bunBinding = {
|
|
1737
|
-
check(ctx, config) {
|
|
1738
|
-
const ignoreScripts = getByPath(config, ["install", "ignoreScripts"]);
|
|
1739
|
-
if (ignoreScripts === true) return { state: "ok" };
|
|
1740
|
-
const trusted = ctx.packageJson?.trustedDependencies;
|
|
1741
|
-
if (typeof trusted !== "undefined" && trusted.length === 0) return { state: "ok" };
|
|
1742
|
-
return {
|
|
1743
|
-
remediation: proposeChanges(config, [{
|
|
1744
|
-
file: bunfig$3,
|
|
1745
|
-
keyPath: ["install", "ignoreScripts"],
|
|
1746
|
-
op: "setKey",
|
|
1747
|
-
value: true
|
|
1748
|
-
}]),
|
|
1749
|
-
actual: ignoreScripts,
|
|
1750
|
-
expected: true,
|
|
1751
|
-
message: bunMessage,
|
|
1752
|
-
state: "violation"
|
|
1753
|
-
};
|
|
1754
|
-
},
|
|
1755
|
-
docs: "https://bun.com/docs/pm/lifecycle",
|
|
1756
|
-
file: bunfig$3,
|
|
1757
|
-
severity: "info",
|
|
1758
|
-
versionNote: { configAvailableSince: "bun 1.2.0" }
|
|
1759
|
-
};
|
|
1760
|
-
const disableLifecycleScripts = withAubeParanoid(overrideBindings(requireConfigKey({
|
|
1761
|
-
bindings: {
|
|
1762
|
-
npm: {
|
|
1763
|
-
docs: "https://docs.npmjs.com/cli/v11/using-npm/config#ignore-scripts",
|
|
1764
|
-
file: npmrc$5,
|
|
1765
|
-
keyPath: ["ignore-scripts"],
|
|
1766
|
-
message: "Set `ignore-scripts=true` in .npmrc to block dependency lifecycle scripts.",
|
|
1767
|
-
value: true
|
|
1768
|
-
},
|
|
1769
|
-
yarn: {
|
|
1770
|
-
docs: "https://yarnpkg.com/configuration/yarnrc#enableScripts",
|
|
1771
|
-
documentedDefault: false,
|
|
1772
|
-
file: yarnrc$6,
|
|
1773
|
-
keyPath: ["enableScripts"],
|
|
1774
|
-
message: "Set `enableScripts: false` in .yarnrc.yml to pin the policy across versions.",
|
|
1775
|
-
value: false,
|
|
1776
|
-
versionNote: {
|
|
1777
|
-
configAvailableSince: "yarn 2.0.0",
|
|
1778
|
-
defaultSafeSince: "yarn 4.14.0"
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
},
|
|
1782
|
-
description: "Malicious postinstall scripts are a primary supply-chain attack vector. Prevent automatic execution of dependency lifecycle scripts.",
|
|
1783
|
-
docs: "https://github.com/bodadotsh/npm-security-best-practices#3-disable-lifecycle-scripts",
|
|
1784
|
-
id: "disable-lifecycle-scripts",
|
|
1785
|
-
severity: "error",
|
|
1786
|
-
title: "Disable dependency lifecycle scripts"
|
|
1787
|
-
}), {
|
|
1788
|
-
aube: aubeBinding$2,
|
|
1789
|
-
bun: bunBinding,
|
|
1790
|
-
pnpm: pnpmBinding$4
|
|
1791
|
-
}));
|
|
1154
|
+
title: "Disable dependency lifecycle scripts"
|
|
1155
|
+
}), {
|
|
1156
|
+
aube: aubeBinding$2,
|
|
1157
|
+
bun: bunBinding,
|
|
1158
|
+
pnpm: pnpmBinding$4
|
|
1159
|
+
}));
|
|
1792
1160
|
//#endregion
|
|
1793
1161
|
//#region src/domain/rules/enforce-strict-ssl.ts
|
|
1794
1162
|
const { npmrc: npmrc$4, yarnrc: yarnrc$5 } = CONFIG_FILES;
|
|
@@ -2021,19 +1389,9 @@ const hardenedMode = requireConfigKey({
|
|
|
2021
1389
|
title: "Enable hardened mode (yarn)"
|
|
2022
1390
|
});
|
|
2023
1391
|
//#endregion
|
|
2024
|
-
//#region src/domain/
|
|
2025
|
-
const HOURS_PER_DAY = 24;
|
|
2026
|
-
const MINUTES_PER_HOUR = 60;
|
|
2027
|
-
const SECONDS_PER_MINUTE = 60;
|
|
2028
|
-
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
|
|
2029
|
-
const SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE;
|
|
2030
|
-
const RECOMMENDED_RELEASE_AGE_DAYS = 3;
|
|
2031
|
-
const RECOMMENDED_RELEASE_AGE_MINUTES = RECOMMENDED_RELEASE_AGE_DAYS * MINUTES_PER_DAY;
|
|
2032
|
-
const RECOMMENDED_RELEASE_AGE_SECONDS = RECOMMENDED_RELEASE_AGE_DAYS * SECONDS_PER_DAY;
|
|
2033
|
-
const DOCUMENTED_DEFAULT_MINUTES = MINUTES_PER_DAY;
|
|
2034
|
-
const { npmrc: npmrc$3, pnpmWorkspace: pnpmWorkspace$5, yarnrc: yarnrc$2, bunfig: bunfig$1, denoJson: denoJson$1, aubeWorkspace: aubeWorkspace$4 } = CONFIG_FILES;
|
|
1392
|
+
//#region src/domain/services/deno-release-age.ts
|
|
2035
1393
|
const DENO_DURATION = /^\+?P(?:\+?\d+[Ww]|(?:\d+[Dd])*(?:T(?:\d+[HhMm]|\d+(?:\.\d+)?[Ss])+)?)$/u;
|
|
2036
|
-
const DENO_MIN_TIMESTAMP = Date.
|
|
1394
|
+
const DENO_MIN_TIMESTAMP = Date.UTC(-262143, 0, 1);
|
|
2037
1395
|
const DENO_UNIT_SECONDS = {
|
|
2038
1396
|
w: 604800,
|
|
2039
1397
|
d: 86400,
|
|
@@ -2044,33 +1402,49 @@ const DENO_UNIT_SECONDS = {
|
|
|
2044
1402
|
const DENO_DATE = /^\d{4}-\d{2}-\d{2}$/u;
|
|
2045
1403
|
const DENO_TIMESTAMP = /^\d{4}-\d{2}-\d{2}[Tt ](?:[01]\d|2[0-3]):[0-5]\d:(?:[0-5]\d|60)(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/u;
|
|
2046
1404
|
const DENO_OFFSET_TIMESTAMP = /^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d(?::(?:[0-5]\d|60))?[+-](?:[01]\d|2[0-3]):?[0-5]\d$/u;
|
|
2047
|
-
const
|
|
2048
|
-
const
|
|
2049
|
-
const isPositiveDenoSeconds = (seconds) => seconds > 0 && Number.isFinite(seconds) &&
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
let seconds = 0;
|
|
2056
|
-
for (const [, integer, fraction, unit] of value.matchAll(/(\d+)(?:\.(\d+))?([WDHMS])/giu)) {
|
|
2057
|
-
const amount = Number(integer) + Number(`0.${(fraction ?? "").slice(0, 9) || "0"}`);
|
|
2058
|
-
seconds += amount * (DENO_UNIT_SECONDS[unit?.toLowerCase() ?? ""] ?? 0);
|
|
2059
|
-
}
|
|
2060
|
-
return isPositiveDenoSeconds(seconds);
|
|
1405
|
+
const DENO_DURATION_TOKEN = /(\d+)(?:\.(\d+))?([WDHMS])/giu;
|
|
1406
|
+
const LEAP_SECOND = /:60(?=\.|Z|z|[+-])/u;
|
|
1407
|
+
const isPositiveDenoSeconds = (seconds, now) => seconds > 0 && Number.isFinite(seconds) && now - seconds * 1e3 >= DENO_MIN_TIMESTAMP;
|
|
1408
|
+
const durationSeconds = (value) => {
|
|
1409
|
+
let seconds = 0;
|
|
1410
|
+
for (const [, integer, fraction, unit] of value.matchAll(DENO_DURATION_TOKEN)) {
|
|
1411
|
+
const amount = Number(integer) + Number(`0.${(fraction ?? "").slice(0, 9) || "0"}`);
|
|
1412
|
+
seconds += amount * (DENO_UNIT_SECONDS[unit?.toLowerCase() ?? ""] ?? 0);
|
|
2061
1413
|
}
|
|
1414
|
+
return seconds;
|
|
1415
|
+
};
|
|
1416
|
+
/** Match Deno's minimumDependencyAge value grammar and active-cutoff semantics. */
|
|
1417
|
+
const isActiveDenoReleaseAge = (value, now, parse) => {
|
|
1418
|
+
if (typeof value === "number") return Number.isSafeInteger(value) && isPositiveDenoSeconds(value * 60, now);
|
|
1419
|
+
if (typeof value !== "string") return false;
|
|
1420
|
+
if (/^\d+$/u.test(value)) return Number.isSafeInteger(Number(value)) && isPositiveDenoSeconds(Number(value) * 60, now);
|
|
1421
|
+
if (DENO_DURATION.test(value)) return isPositiveDenoSeconds(durationSeconds(value), now);
|
|
2062
1422
|
if (!DENO_DATE.test(value) && !DENO_TIMESTAMP.test(value) && !DENO_OFFSET_TIMESTAMP.test(value)) return false;
|
|
2063
1423
|
const date = value.slice(0, 10);
|
|
2064
1424
|
const midnight = /* @__PURE__ */ new Date(`${date}T00:00:00Z`);
|
|
2065
|
-
const leapSecond =
|
|
2066
|
-
const timestamp =
|
|
2067
|
-
return !Number.isNaN(midnight.valueOf()) && midnight.toISOString().slice(0, 10) === date && timestamp <
|
|
1425
|
+
const leapSecond = LEAP_SECOND.test(value);
|
|
1426
|
+
const timestamp = parse(value.replace(LEAP_SECOND, ":59")) + (leapSecond ? 1e3 : 0);
|
|
1427
|
+
return !Number.isNaN(midnight.valueOf()) && midnight.toISOString().slice(0, 10) === date && timestamp < now;
|
|
2068
1428
|
};
|
|
2069
|
-
|
|
2070
|
-
|
|
1429
|
+
//#endregion
|
|
1430
|
+
//#region src/domain/rules/minimum-release-age.ts
|
|
1431
|
+
const HOURS_PER_DAY = 24;
|
|
1432
|
+
const MINUTES_PER_HOUR = 60;
|
|
1433
|
+
const SECONDS_PER_MINUTE = 60;
|
|
1434
|
+
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
|
|
1435
|
+
const SECONDS_PER_DAY = MINUTES_PER_DAY * SECONDS_PER_MINUTE;
|
|
1436
|
+
const RECOMMENDED_RELEASE_AGE_DAYS = 3;
|
|
1437
|
+
const RECOMMENDED_RELEASE_AGE_MINUTES = RECOMMENDED_RELEASE_AGE_DAYS * MINUTES_PER_DAY;
|
|
1438
|
+
const RECOMMENDED_RELEASE_AGE_SECONDS = RECOMMENDED_RELEASE_AGE_DAYS * SECONDS_PER_DAY;
|
|
1439
|
+
const DOCUMENTED_DEFAULT_MINUTES = MINUTES_PER_DAY;
|
|
1440
|
+
const { npmrc: npmrc$3, pnpmWorkspace: pnpmWorkspace$5, yarnrc: yarnrc$2, bunfig: bunfig$1, denoJson: denoJson$1, aubeWorkspace: aubeWorkspace$4 } = CONFIG_FILES;
|
|
1441
|
+
const isPositiveNumber = (value) => typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
1442
|
+
const isPositiveYarnDuration = (value) => isPositiveNumber(value) || typeof value === "string" && /^\d*\.?\d+(?:ms|s|m|h|d|w)?$/u.test(value) && Number.isFinite(Number.parseFloat(value)) && Number.parseFloat(value) > 0;
|
|
1443
|
+
const isNonDisabledDenoReleaseAge = (value, now, parse) => {
|
|
1444
|
+
if (!isPlainRecord(value)) return isActiveDenoReleaseAge(value, now, parse);
|
|
2071
1445
|
if (Object.keys(value).some((key) => key !== "age" && key !== "exclude")) return false;
|
|
2072
1446
|
if (value.exclude !== void 0 && !isStringList(value.exclude)) return false;
|
|
2073
|
-
return value.age == null ||
|
|
1447
|
+
return value.age == null || isActiveDenoReleaseAge(value.age, now, parse);
|
|
2074
1448
|
};
|
|
2075
1449
|
const denoAgeUsesFallback = (value) => {
|
|
2076
1450
|
if (value === void 0 || value === null) return true;
|
|
@@ -2080,7 +1454,7 @@ const denoAgeUsesFallback = (value) => {
|
|
|
2080
1454
|
return value.age == null;
|
|
2081
1455
|
};
|
|
2082
1456
|
const isPositiveDenoNpmrcDays = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
|
|
2083
|
-
const
|
|
1457
|
+
const baseRule$1 = requireConfigKey({
|
|
2084
1458
|
bindings: {
|
|
2085
1459
|
aube: {
|
|
2086
1460
|
accept: isPositiveNumber,
|
|
@@ -2132,102 +1506,105 @@ const minimumReleaseAge = overrideBindings(requireConfigKey({
|
|
|
2132
1506
|
id: "minimum-release-age",
|
|
2133
1507
|
severity: "warn",
|
|
2134
1508
|
title: "Set a minimum release age"
|
|
2135
|
-
})
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
return {
|
|
2146
|
-
state: "violation",
|
|
2147
|
-
actual,
|
|
2148
|
-
expected: "a date in the past",
|
|
2149
|
-
message: "Use a valid past before cutoff, or remove before and set min-release-age.",
|
|
2150
|
-
remediation: {
|
|
2151
|
-
kind: "manual",
|
|
2152
|
-
steps: ["In .npmrc, set before to a valid past date. A future or disabled before overrides min-release-age in this file.", ...guardRemediationAvailability("npm", ctx.pmVersion, {
|
|
2153
|
-
kind: "manual",
|
|
2154
|
-
steps: [`Alternatively, remove before and set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days.`]
|
|
2155
|
-
}, [{
|
|
2156
|
-
file: npmrc$3,
|
|
2157
|
-
keyPath: ["min-release-age"]
|
|
2158
|
-
}])?.steps ?? []]
|
|
2159
|
-
}
|
|
2160
|
-
};
|
|
2161
|
-
}
|
|
2162
|
-
const actual = getByPath(config, ["min-release-age"]);
|
|
2163
|
-
const age = typeof actual === "number" || typeof actual === "string" ? Number(actual) : NaN;
|
|
2164
|
-
const cutoff = (/* @__PURE__ */ new Date(now - SECONDS_PER_DAY * 1e3 * age)).valueOf();
|
|
2165
|
-
if (age > 0 && Number.isFinite(cutoff) && cutoff < now) return { state: "ok" };
|
|
1509
|
+
});
|
|
1510
|
+
const createNpmBinding = (time) => ({
|
|
1511
|
+
file: npmrc$3,
|
|
1512
|
+
docs: "https://docs.npmjs.com/cli/v12/using-npm/config#min-release-age",
|
|
1513
|
+
versionNote: { note: "min-release-age available since npm 11.10.0" },
|
|
1514
|
+
check(ctx, config) {
|
|
1515
|
+
const now = time.now();
|
|
1516
|
+
if (Object.hasOwn(config, "before")) {
|
|
1517
|
+
const actual = config.before;
|
|
1518
|
+
if ((typeof actual === "string" || typeof actual === "number") && time.parse(String(actual)) < now) return { state: "ok" };
|
|
2166
1519
|
return {
|
|
2167
1520
|
state: "violation",
|
|
2168
1521
|
actual,
|
|
2169
|
-
expected:
|
|
2170
|
-
message:
|
|
2171
|
-
remediation:
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
1522
|
+
expected: "a date in the past",
|
|
1523
|
+
message: "Use a valid past before cutoff, or remove before and set min-release-age.",
|
|
1524
|
+
remediation: {
|
|
1525
|
+
kind: "manual",
|
|
1526
|
+
steps: ["In .npmrc, set before to a valid past date. A future or disabled before overrides min-release-age in this file.", ...guardRemediationAvailability("npm", ctx.pmVersion, {
|
|
1527
|
+
kind: "manual",
|
|
1528
|
+
steps: [`Alternatively, remove before and set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days.`]
|
|
1529
|
+
}, [{
|
|
1530
|
+
file: npmrc$3,
|
|
1531
|
+
keyPath: ["min-release-age"]
|
|
1532
|
+
}])?.steps ?? []]
|
|
1533
|
+
}
|
|
2180
1534
|
};
|
|
2181
1535
|
}
|
|
1536
|
+
const actual = getByPath(config, ["min-release-age"]);
|
|
1537
|
+
const age = typeof actual === "number" || typeof actual === "string" ? Number(actual) : NaN;
|
|
1538
|
+
const cutoff = (/* @__PURE__ */ new Date(now - SECONDS_PER_DAY * 1e3 * age)).valueOf();
|
|
1539
|
+
if (age > 0 && Number.isFinite(cutoff) && cutoff < now) return { state: "ok" };
|
|
1540
|
+
return {
|
|
1541
|
+
state: "violation",
|
|
1542
|
+
actual,
|
|
1543
|
+
expected: RECOMMENDED_RELEASE_AGE_DAYS,
|
|
1544
|
+
message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days to quarantine brand-new releases.`,
|
|
1545
|
+
remediation: guardRemediationAvailability("npm", ctx.pmVersion, proposeChanges(config, [{
|
|
1546
|
+
file: npmrc$3,
|
|
1547
|
+
op: "setKey",
|
|
1548
|
+
keyPath: ["min-release-age"],
|
|
1549
|
+
value: RECOMMENDED_RELEASE_AGE_DAYS
|
|
1550
|
+
}]), [{
|
|
1551
|
+
file: npmrc$3,
|
|
1552
|
+
keyPath: ["min-release-age"]
|
|
1553
|
+
}])
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
const createDenoBinding = (time) => ({
|
|
1558
|
+
file: denoJson$1,
|
|
1559
|
+
docs: "https://docs.deno.com/runtime/reference/deno_json/",
|
|
1560
|
+
versionNote: {
|
|
1561
|
+
defaultSafeSince: "deno 2.9.0 (1440 minutes)",
|
|
1562
|
+
note: "object age may be omitted; project .npmrc fallback available since deno 2.8.1"
|
|
2182
1563
|
},
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
check(ctx, config) {
|
|
2191
|
-
const actual = getByPath(config, ["minimumDependencyAge"]);
|
|
2192
|
-
if (denoAgeUsesFallback(actual)) {
|
|
2193
|
-
const npmrcConfig = ctx.readConfig(npmrc$3);
|
|
2194
|
-
const npmrcAge = getByPath(npmrcConfig, ["min-release-age"]);
|
|
2195
|
-
if (isPositiveDenoNpmrcDays(npmrcAge)) return { state: "ok" };
|
|
2196
|
-
if (npmrcAge === 0) return {
|
|
2197
|
-
state: "violation",
|
|
2198
|
-
actual: npmrcAge,
|
|
2199
|
-
expected: RECOMMENDED_RELEASE_AGE_DAYS,
|
|
2200
|
-
file: npmrc$3.path,
|
|
2201
|
-
message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days in .npmrc, or set minimumDependencyAge in deno.json.`,
|
|
2202
|
-
remediation: proposeChanges(npmrcConfig, [{
|
|
2203
|
-
file: npmrc$3,
|
|
2204
|
-
op: "setKey",
|
|
2205
|
-
keyPath: ["min-release-age"],
|
|
2206
|
-
value: RECOMMENDED_RELEASE_AGE_DAYS
|
|
2207
|
-
}])
|
|
2208
|
-
};
|
|
2209
|
-
}
|
|
2210
|
-
if (isNonDisabledDenoDuration(actual)) return { state: "ok" };
|
|
2211
|
-
const objectAge = isPlainRecord(actual);
|
|
2212
|
-
const invalidObject = objectAge && (Object.keys(actual).some((key) => key !== "age" && key !== "exclude") || actual.exclude !== void 0 && !isStringList(actual.exclude));
|
|
2213
|
-
return {
|
|
1564
|
+
check(ctx, config) {
|
|
1565
|
+
const actual = getByPath(config, ["minimumDependencyAge"]);
|
|
1566
|
+
if (denoAgeUsesFallback(actual)) {
|
|
1567
|
+
const npmrcConfig = ctx.readConfig(npmrc$3);
|
|
1568
|
+
const npmrcAge = getByPath(npmrcConfig, ["min-release-age"]);
|
|
1569
|
+
if (isPositiveDenoNpmrcDays(npmrcAge)) return { state: "ok" };
|
|
1570
|
+
if (npmrcAge === 0) return {
|
|
2214
1571
|
state: "violation",
|
|
2215
|
-
actual,
|
|
2216
|
-
expected:
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
} : proposeChanges(config, [{
|
|
2222
|
-
file: denoJson$1,
|
|
1572
|
+
actual: npmrcAge,
|
|
1573
|
+
expected: RECOMMENDED_RELEASE_AGE_DAYS,
|
|
1574
|
+
file: npmrc$3.path,
|
|
1575
|
+
message: `Set min-release-age to ~${RECOMMENDED_RELEASE_AGE_DAYS} days in .npmrc, or set minimumDependencyAge in deno.json.`,
|
|
1576
|
+
remediation: proposeChanges(npmrcConfig, [{
|
|
1577
|
+
file: npmrc$3,
|
|
2223
1578
|
op: "setKey",
|
|
2224
|
-
keyPath:
|
|
2225
|
-
value:
|
|
1579
|
+
keyPath: ["min-release-age"],
|
|
1580
|
+
value: RECOMMENDED_RELEASE_AGE_DAYS
|
|
2226
1581
|
}])
|
|
2227
1582
|
};
|
|
2228
1583
|
}
|
|
1584
|
+
if (isNonDisabledDenoReleaseAge(actual, time.now(), time.parse)) return { state: "ok" };
|
|
1585
|
+
const objectAge = isPlainRecord(actual);
|
|
1586
|
+
const invalidObject = objectAge && (Object.keys(actual).some((key) => key !== "age" && key !== "exclude") || actual.exclude !== void 0 && !isStringList(actual.exclude));
|
|
1587
|
+
return {
|
|
1588
|
+
state: "violation",
|
|
1589
|
+
actual,
|
|
1590
|
+
expected: "P3D",
|
|
1591
|
+
message: `Set minimumDependencyAge (e.g. "P3D" for a ${RECOMMENDED_RELEASE_AGE_DAYS}-day cooldown) in deno.json.`,
|
|
1592
|
+
remediation: invalidObject ? {
|
|
1593
|
+
kind: "manual",
|
|
1594
|
+
steps: ["Use only age and exclude in minimumDependencyAge. Make exclude a list of package names and set age to a positive supported duration."]
|
|
1595
|
+
} : proposeChanges(config, [{
|
|
1596
|
+
file: denoJson$1,
|
|
1597
|
+
op: "setKey",
|
|
1598
|
+
keyPath: objectAge ? ["minimumDependencyAge", "age"] : ["minimumDependencyAge"],
|
|
1599
|
+
value: "P3D"
|
|
1600
|
+
}])
|
|
1601
|
+
};
|
|
2229
1602
|
}
|
|
2230
1603
|
});
|
|
1604
|
+
const createMinimumReleaseAge = (time) => overrideBindings(baseRule$1, {
|
|
1605
|
+
npm: createNpmBinding(time),
|
|
1606
|
+
deno: createDenoBinding(time)
|
|
1607
|
+
});
|
|
2231
1608
|
//#endregion
|
|
2232
1609
|
//#region src/domain/rules/named-registries.ts
|
|
2233
1610
|
const { pnpmWorkspace: pnpmWorkspace$4 } = CONFIG_FILES;
|
|
@@ -2308,7 +1685,8 @@ const REGISTRY_VERSION = /^(?:npm|jsr):(?:@[^/@]+\/)?[^/@]+@(?<version>[^/]*)/u;
|
|
|
2308
1685
|
const isUnpinnedRegistryImport = (specifier) => {
|
|
2309
1686
|
if (!specifier.startsWith("npm:") && !specifier.startsWith("jsr:")) return false;
|
|
2310
1687
|
const version = REGISTRY_VERSION.exec(specifier)?.groups?.version;
|
|
2311
|
-
|
|
1688
|
+
const normalized = version?.startsWith("=") ? version.slice(1) : version;
|
|
1689
|
+
return normalized === void 0 || valid(normalized) === null;
|
|
2312
1690
|
};
|
|
2313
1691
|
const collectUnpinnedImports = (imports) => {
|
|
2314
1692
|
const offenders = [];
|
|
@@ -2685,9 +2063,23 @@ const bindingFor = (pm) => {
|
|
|
2685
2063
|
} : { state: "ok" };
|
|
2686
2064
|
} };
|
|
2687
2065
|
};
|
|
2066
|
+
const unsupportedSettings = defineRule({
|
|
2067
|
+
id: "unsupported-settings",
|
|
2068
|
+
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. Workspace findings retain their member directory. Only the coverage table below is checked. Unknown targets and unlisted settings are not evaluated for availability.",
|
|
2070
|
+
severity: "error",
|
|
2071
|
+
docs: "https://github.com/pHo9UBenaA/siro/blob/main/docs/rules.md#unsupported-settings--error",
|
|
2072
|
+
bindings: {
|
|
2073
|
+
npm: bindingFor("npm"),
|
|
2074
|
+
pnpm: bindingFor("pnpm"),
|
|
2075
|
+
yarn: bindingFor("yarn"),
|
|
2076
|
+
bun: bindingFor("bun"),
|
|
2077
|
+
deno: bindingFor("deno")
|
|
2078
|
+
}
|
|
2079
|
+
});
|
|
2688
2080
|
//#endregion
|
|
2689
2081
|
//#region src/domain/builtin-rules.ts
|
|
2690
|
-
const
|
|
2082
|
+
const createBuiltinRules = (time) => [
|
|
2691
2083
|
advisoryCheck,
|
|
2692
2084
|
approvedGitRepos,
|
|
2693
2085
|
auditSuppression,
|
|
@@ -2703,7 +2095,7 @@ const rules = [
|
|
|
2703
2095
|
frozenLockfile,
|
|
2704
2096
|
frozenStore,
|
|
2705
2097
|
hardenedMode,
|
|
2706
|
-
|
|
2098
|
+
createMinimumReleaseAge(time),
|
|
2707
2099
|
namedRegistries,
|
|
2708
2100
|
paranoidMode,
|
|
2709
2101
|
patchedDependencies,
|
|
@@ -2715,21 +2107,448 @@ const rules = [
|
|
|
2715
2107
|
strictReleaseAge,
|
|
2716
2108
|
strictStoreIntegrity,
|
|
2717
2109
|
trustPolicy,
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2110
|
+
unsupportedSettings
|
|
2111
|
+
];
|
|
2112
|
+
//#endregion
|
|
2113
|
+
//#region src/composition/rules.ts
|
|
2114
|
+
/** Read time at evaluation, never at module initialization. */
|
|
2115
|
+
const rules = createBuiltinRules({
|
|
2116
|
+
now: () => Date.now(),
|
|
2117
|
+
parse: (value) => Date.parse(value)
|
|
2118
|
+
});
|
|
2119
|
+
//#endregion
|
|
2120
|
+
//#region src/adapters/workspace-globs.ts
|
|
2121
|
+
const MAX_WORKSPACE_GLOB_ALTERNATIVES = 8192;
|
|
2122
|
+
const BRACE_EXPANSION_PROBE_LIMIT = 8193;
|
|
2123
|
+
const boundedBraceExpand = (pattern) => {
|
|
2124
|
+
const alternatives = braceExpand(pattern, { braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT });
|
|
2125
|
+
if (alternatives.length > MAX_WORKSPACE_GLOB_ALTERNATIVES) throw new ConfigError(`Workspace pattern expands beyond ${MAX_WORKSPACE_GLOB_ALTERNATIVES} alternatives.`);
|
|
2126
|
+
return alternatives;
|
|
2127
|
+
};
|
|
2128
|
+
/** Expand with minimatch's own bounded brace semantics. */
|
|
2129
|
+
const expandWorkspaceGlob = (pattern) => {
|
|
2130
|
+
try {
|
|
2131
|
+
return boundedBraceExpand(pattern);
|
|
2132
|
+
} catch (error) {
|
|
2133
|
+
if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
|
|
2134
|
+
throw error;
|
|
2135
|
+
}
|
|
2136
|
+
};
|
|
2137
|
+
/** Engine choices live here; the application supplies only PM matching policy. */
|
|
2138
|
+
const engineOptions = (options) => {
|
|
2139
|
+
if (options.kind === "declaration") return {};
|
|
2140
|
+
return {
|
|
2141
|
+
platform: "linux",
|
|
2142
|
+
windowsPathsNoEscape: true,
|
|
2143
|
+
nonegate: true,
|
|
2144
|
+
nocomment: !options.hashComments,
|
|
2145
|
+
nocase: options.caseInsensitive,
|
|
2146
|
+
dot: options.includeDotDirectories ?? false,
|
|
2147
|
+
nobrace: options.syntax === "wildcards",
|
|
2148
|
+
noext: options.syntax === "wildcards" || options.extendedPatterns === false,
|
|
2149
|
+
optimizationLevel: options.syntax === "shell" ? 2 : 1
|
|
2150
|
+
};
|
|
2151
|
+
};
|
|
2152
|
+
/** Compile once so membership, exclusions, and traversal share glob semantics. */
|
|
2153
|
+
const compileWorkspaceGlob = (pattern, options) => {
|
|
2154
|
+
let matcher;
|
|
2155
|
+
try {
|
|
2156
|
+
const settings = engineOptions(options);
|
|
2157
|
+
if (!settings.nobrace) boundedBraceExpand(pattern);
|
|
2158
|
+
matcher = new Minimatch(options.kind === "directory" && options.syntax === "wildcards" ? pattern.replace(/[[\]]/gu, (character) => character === "[" ? "[[]" : "[]]") : pattern, {
|
|
2159
|
+
...settings,
|
|
2160
|
+
braceExpandMax: BRACE_EXPANSION_PROBE_LIMIT
|
|
2161
|
+
});
|
|
2162
|
+
} catch (error) {
|
|
2163
|
+
if (error instanceof TypeError) throw new ConfigError(`Invalid workspace pattern: ${error.message}.`);
|
|
2164
|
+
throw error;
|
|
2165
|
+
}
|
|
2166
|
+
return {
|
|
2167
|
+
matches: (directory) => matcher.match(directory),
|
|
2168
|
+
canDescend(directory) {
|
|
2169
|
+
const parts = directory.split("/");
|
|
2170
|
+
return matcher.set.some((alternative) => (alternative.includes(GLOBSTAR) || parts.length < alternative.length) && matcher.matchOne(parts, alternative, true));
|
|
2171
|
+
}
|
|
2172
|
+
};
|
|
2173
|
+
};
|
|
2174
|
+
const minimatchGlobs = {
|
|
2175
|
+
expand: expandWorkspaceGlob,
|
|
2176
|
+
compile: compileWorkspaceGlob
|
|
2177
|
+
};
|
|
2178
|
+
//#endregion
|
|
2179
|
+
//#region src/domain/services/parse-config-file.ts
|
|
2180
|
+
const EMPTY_FILE = Object.freeze({});
|
|
2181
|
+
/** Create a fresh parser and cache for one lint command. */
|
|
2182
|
+
const createConfigParser = (codecFor, ctx) => {
|
|
2183
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2184
|
+
return (file) => {
|
|
2185
|
+
if (file === void 0) return EMPTY_FILE;
|
|
2186
|
+
const cacheKey = `${file.kind}:${file.path}`;
|
|
2187
|
+
const cached = cache.get(cacheKey);
|
|
2188
|
+
if (cached !== void 0) return cached;
|
|
2189
|
+
const text = ctx.readText(file.path);
|
|
2190
|
+
if (text === void 0) {
|
|
2191
|
+
cache.set(cacheKey, EMPTY_FILE);
|
|
2192
|
+
return EMPTY_FILE;
|
|
2193
|
+
}
|
|
2194
|
+
const parsed = wrapCodecError(file.path, () => codecFor(file.kind).parse(text));
|
|
2195
|
+
cache.set(cacheKey, parsed);
|
|
2196
|
+
return parsed;
|
|
2197
|
+
};
|
|
2198
|
+
};
|
|
2199
|
+
//#endregion
|
|
2200
|
+
//#region src/application/workspace-pattern.ts
|
|
2201
|
+
/** Detect the basic * and ? markers shared across workspace dialects. */
|
|
2202
|
+
const hasBasicWorkspaceWildcard = (pattern) => pattern.includes("*") || pattern.includes("?");
|
|
2203
|
+
/** Workspace declarations use POSIX separators regardless of the host platform. */
|
|
2204
|
+
const stripTrailingWorkspaceSlashes = (pattern) => {
|
|
2205
|
+
let end = pattern.length;
|
|
2206
|
+
while (end > 0 && pattern[end - 1] === "/") end -= 1;
|
|
2207
|
+
return pattern.slice(0, end);
|
|
2208
|
+
};
|
|
2209
|
+
//#endregion
|
|
2210
|
+
//#region src/application/workspace-prefix.ts
|
|
2211
|
+
/** Anchor Deno's native literal prefix before applying its glob suffix. */
|
|
2212
|
+
const anchorWorkspacePrefix = (pattern, glob, resolveChild) => {
|
|
2213
|
+
const parts = pattern.split("/");
|
|
2214
|
+
const firstWildcard = parts.findIndex(hasBasicWorkspaceWildcard);
|
|
2215
|
+
const prefix = parts.slice(0, firstWildcard < 0 ? parts.length : firstWildcard);
|
|
2216
|
+
if (prefix.length === 0 || pattern === ".") return glob;
|
|
2217
|
+
const declared = prefix.join("/");
|
|
2218
|
+
let resolved;
|
|
2219
|
+
let attempted = false;
|
|
2220
|
+
const resolve = () => {
|
|
2221
|
+
if (!attempted) {
|
|
2222
|
+
attempted = true;
|
|
2223
|
+
let current = ".";
|
|
2224
|
+
for (const part of prefix) {
|
|
2225
|
+
const child = resolveChild(current, part);
|
|
2226
|
+
if (child === void 0) return void 0;
|
|
2227
|
+
current = current === "." ? child : `${current}/${child}`;
|
|
2228
|
+
}
|
|
2229
|
+
resolved = current;
|
|
2230
|
+
}
|
|
2231
|
+
return resolved;
|
|
2232
|
+
};
|
|
2233
|
+
const rewrite = (directory, actual) => directory === actual ? declared : directory.startsWith(`${actual}/`) ? declared + directory.slice(actual.length) : void 0;
|
|
2234
|
+
return {
|
|
2235
|
+
matches(directory) {
|
|
2236
|
+
if (directory === ".") return false;
|
|
2237
|
+
const actual = resolve();
|
|
2238
|
+
const rewritten = actual === void 0 ? void 0 : rewrite(directory, actual);
|
|
2239
|
+
return rewritten !== void 0 && glob.matches(rewritten);
|
|
2240
|
+
},
|
|
2241
|
+
canDescend(directory) {
|
|
2242
|
+
const actual = resolve();
|
|
2243
|
+
if (actual === void 0) return false;
|
|
2244
|
+
if (directory === "." || actual.startsWith(`${directory}/`)) return true;
|
|
2245
|
+
const rewritten = rewrite(directory, actual);
|
|
2246
|
+
return rewritten !== void 0 && glob.canDescend(rewritten);
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
};
|
|
2250
|
+
//#endregion
|
|
2251
|
+
//#region src/application/workspace-glob-policy.ts
|
|
2252
|
+
const workspaceGlobOptions = (caseInsensitive) => ({
|
|
2253
|
+
kind: "directory",
|
|
2254
|
+
syntax: "shell",
|
|
2255
|
+
caseInsensitive
|
|
2256
|
+
});
|
|
2257
|
+
//#endregion
|
|
2258
|
+
//#region src/application/workspace-dialects.ts
|
|
2259
|
+
const containsUnsupportedAubeGlobSyntax = (pattern) => /[[\]{}()]/u.test(pattern);
|
|
2260
|
+
const matchesCrossPathGlob = (pattern, value) => {
|
|
2261
|
+
const patternCharacters = [...pattern];
|
|
2262
|
+
const valueCharacters = [...value];
|
|
2263
|
+
let patternIndex = 0;
|
|
2264
|
+
let valueIndex = 0;
|
|
2265
|
+
let starIndex = -1;
|
|
2266
|
+
let retryIndex = 0;
|
|
2267
|
+
while (valueIndex < valueCharacters.length) {
|
|
2268
|
+
const character = patternCharacters[patternIndex];
|
|
2269
|
+
if (character === "?" || character === valueCharacters[valueIndex]) {
|
|
2270
|
+
patternIndex += 1;
|
|
2271
|
+
valueIndex += 1;
|
|
2272
|
+
} else if (character === "*") {
|
|
2273
|
+
starIndex = patternIndex;
|
|
2274
|
+
retryIndex = valueIndex;
|
|
2275
|
+
patternIndex += 1;
|
|
2276
|
+
} else if (starIndex >= 0) {
|
|
2277
|
+
patternIndex = starIndex + 1;
|
|
2278
|
+
retryIndex += 1;
|
|
2279
|
+
valueIndex = retryIndex;
|
|
2280
|
+
} else return false;
|
|
2281
|
+
}
|
|
2282
|
+
while (patternCharacters[patternIndex] === "*") patternIndex += 1;
|
|
2283
|
+
return patternIndex === patternCharacters.length;
|
|
2284
|
+
};
|
|
2285
|
+
/** The bounded glob subset verified against the recorded Deno/Aube sources. */
|
|
2286
|
+
const compileAdditionalWorkspaceGlob = (pattern, pm, excluded, caseInsensitive, globs) => {
|
|
2287
|
+
const parts = pattern.split("/");
|
|
2288
|
+
if (parts.some((part) => part.includes("**") && part !== "**")) throw new ConfigError(`${pm}: ** must occupy an entire workspace path component.`);
|
|
2289
|
+
if (pm === "aube" && (containsUnsupportedAubeGlobSyntax(pattern) || pattern.includes("**") && (parts.at(-1) !== "**" || hasBasicWorkspaceWildcard(parts.slice(0, -1).join("/"))))) throw new ConfigError("Aube workspace inspection supports literals, * and ?, and a trailing ** after a literal prefix; other glob forms are not yet supported.");
|
|
2290
|
+
const matcher = globs.compile(pattern, {
|
|
2291
|
+
kind: "directory",
|
|
2292
|
+
syntax: "wildcards",
|
|
2293
|
+
includeDotDirectories: pm === "aube",
|
|
2294
|
+
caseInsensitive: pm === "deno" && (hasBasicWorkspaceWildcard(pattern) || caseInsensitive)
|
|
2295
|
+
});
|
|
2296
|
+
if (pm === "deno" && hasBasicWorkspaceWildcard(pattern)) {
|
|
2297
|
+
const files = globs.compile(`${pattern}/package.json`, {
|
|
2298
|
+
kind: "directory",
|
|
2299
|
+
syntax: "wildcards",
|
|
2300
|
+
caseInsensitive: true
|
|
2301
|
+
});
|
|
2302
|
+
return {
|
|
2303
|
+
...matcher,
|
|
2304
|
+
matches: (directory) => files.matches(directory === "." ? "package.json" : `${directory}/package.json`)
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
if (pm !== "aube" || !excluded) return matcher;
|
|
2308
|
+
const self = pattern.endsWith("/**") ? pattern.slice(0, -3) : void 0;
|
|
2309
|
+
return {
|
|
2310
|
+
...matcher,
|
|
2311
|
+
matches: (directory) => matchesCrossPathGlob(pattern, directory) || directory === self
|
|
2312
|
+
};
|
|
2313
|
+
};
|
|
2314
|
+
//#endregion
|
|
2315
|
+
//#region src/application/workspaces.ts
|
|
2316
|
+
/** npm cancels an earlier exclusion when a later positive pattern matches it. */
|
|
2317
|
+
const splitNpmPattern = (raw) => {
|
|
2318
|
+
const prefix = /^!+/u.exec(raw)?.[0] ?? "";
|
|
2319
|
+
return {
|
|
2320
|
+
excluded: prefix.length % 2 === 1,
|
|
2321
|
+
pattern: raw.slice(prefix.length)
|
|
2322
|
+
};
|
|
2323
|
+
};
|
|
2324
|
+
const npmPatterns = (patterns, globs) => {
|
|
2325
|
+
const positive = [];
|
|
2326
|
+
let negative = [];
|
|
2327
|
+
for (const raw of patterns) {
|
|
2328
|
+
const parsed = splitNpmPattern(raw);
|
|
2329
|
+
const pattern = parsed.pattern.replace(/^\.?\/+/u, "");
|
|
2330
|
+
if (parsed.excluded) negative.push({
|
|
2331
|
+
pattern,
|
|
2332
|
+
glob: globs.compile(pattern, { kind: "declaration" })
|
|
2333
|
+
});
|
|
2334
|
+
else {
|
|
2335
|
+
for (let index = 0; index < negative.length; index += 1) if (negative[index]?.glob.matches(pattern)) negative.splice(index, 1);
|
|
2336
|
+
positive.push(pattern);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
return [...positive, ...negative.map(({ pattern }) => `!${pattern}`)];
|
|
2340
|
+
};
|
|
2341
|
+
/** Read declaration sources separately so Deno's two manifest sets stay distinct. */
|
|
2342
|
+
const workspaceDefinitions = (ctx, pm, dependencies) => {
|
|
2343
|
+
const { codecFor, paths, globs } = dependencies;
|
|
2344
|
+
const parse = createConfigParser(codecFor, ctx);
|
|
2345
|
+
const validate = (value, source, denoManifests = false) => {
|
|
2346
|
+
if (value === void 0) return {
|
|
2347
|
+
patterns: [],
|
|
2348
|
+
denoManifests
|
|
2349
|
+
};
|
|
2350
|
+
if (!Array.isArray(value) || !Array.from(value).every((item) => typeof item === "string")) throw new ConfigError(`${source}: expected an array of directory patterns.`);
|
|
2351
|
+
for (const pattern of value) {
|
|
2352
|
+
const positive = pm === "npm" ? splitNpmPattern(pattern).pattern : pattern.startsWith("!") ? pattern.slice(1) : pattern;
|
|
2353
|
+
if ((pm === "deno" || pm === "aube" ? [positive] : globs.expand(positive)).some((alternative) => !isRelPath(alternative) || alternative.includes("\\") || alternative.startsWith("!"))) throw new ConfigError(`${source}: use relative directory patterns without parent traversal: ${JSON.stringify(pattern)}.`);
|
|
2354
|
+
if (pm === "deno" && denoManifests && !pattern.startsWith("!") && paths.normalizePattern(positive) === ".") throw new ConfigError(`${source}: a Deno workspace cannot contain itself.`);
|
|
2355
|
+
}
|
|
2356
|
+
return {
|
|
2357
|
+
patterns: pm === "npm" ? npmPatterns(value, globs) : value,
|
|
2358
|
+
denoManifests
|
|
2359
|
+
};
|
|
2360
|
+
};
|
|
2361
|
+
const packageDefinition = () => {
|
|
2362
|
+
let value = ctx.packageJson?.workspaces;
|
|
2363
|
+
if (isPlainRecord(value)) {
|
|
2364
|
+
value = value.packages;
|
|
2365
|
+
if (value === void 0) throw new ConfigError("package.json#workspaces: expected a packages array.");
|
|
2366
|
+
}
|
|
2367
|
+
return validate(value, "package.json#workspaces");
|
|
2368
|
+
};
|
|
2369
|
+
if (pm === "pnpm") return [validate(ctx.exists(CONFIG_FILES.pnpmWorkspace.path) ? parse(CONFIG_FILES.pnpmWorkspace).packages : void 0, "pnpm-workspace.yaml#packages")];
|
|
2370
|
+
if (pm === "aube") {
|
|
2371
|
+
const file = [CONFIG_FILES.aubeWorkspace, CONFIG_FILES.pnpmWorkspace].find((candidate) => ctx.exists(candidate.path));
|
|
2372
|
+
return [file ? validate(parse(file).packages, `${file.path}#packages`) : packageDefinition()];
|
|
2373
|
+
}
|
|
2374
|
+
if (pm === "deno") return [validate(parse(CONFIG_FILES.denoJson).workspace, "deno.json#workspace", true), packageDefinition()];
|
|
2375
|
+
return [packageDefinition()];
|
|
2376
|
+
};
|
|
2377
|
+
/** Traverse only ordinary directories that can fall under a positive pattern. */
|
|
2378
|
+
const workspaceDirectories = (ctx, fs, definition, pm, dependencies) => {
|
|
2379
|
+
const { codecFor, paths, globs, caseInsensitiveGlobs } = dependencies;
|
|
2380
|
+
const defaultWorkspaceGlobOptions = workspaceGlobOptions(caseInsensitiveGlobs);
|
|
2381
|
+
const { patterns } = definition;
|
|
2382
|
+
const skipVendor = pm === "deno" && createConfigParser(codecFor, ctx)(CONFIG_FILES.denoJson).vendor === true;
|
|
2383
|
+
const positive = patterns.filter((pattern) => !pattern.startsWith("!")).map((pattern) => stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern)));
|
|
2384
|
+
const negative = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => stripTrailingWorkspaceSlashes(paths.normalizePattern(pattern.slice(1))));
|
|
2385
|
+
const directoryCache = /* @__PURE__ */ new Map();
|
|
2386
|
+
const readDirectories = (directory) => {
|
|
2387
|
+
const cached = directoryCache.get(directory);
|
|
2388
|
+
if (cached) return cached;
|
|
2389
|
+
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
2390
|
+
const names = fs.readDirectories(paths.resolve(ctx.root, asRelPath(directory)));
|
|
2391
|
+
if (!Array.isArray(names)) throw new ConfigError("FileSystem.readDirectories must return an array of directory names.");
|
|
2392
|
+
for (const name of Array.from(names)) if (!isRelPath(name) || name === "." || name === ".." || /[\\/]/u.test(name)) throw new ConfigError("FileSystem.readDirectories returned an invalid child directory name.");
|
|
2393
|
+
const result = [...new Set(names)].sort();
|
|
2394
|
+
directoryCache.set(directory, result);
|
|
2395
|
+
return result;
|
|
2396
|
+
};
|
|
2397
|
+
const resolveChild = (parent, name) => {
|
|
2398
|
+
if (name === ".git" || name === "node_modules") return void 0;
|
|
2399
|
+
const names = readDirectories(parent);
|
|
2400
|
+
const resolved = names.includes(name) ? name : fs.resolveDirectory?.(paths.resolve(ctx.root, asRelPath(parent)), name);
|
|
2401
|
+
if (resolved !== void 0 && !names.includes(resolved)) throw new ConfigError("FileSystem.resolveDirectory must return an enumerated ordinary child directory name.");
|
|
2402
|
+
return resolved === ".git" || resolved === "node_modules" ? void 0 : resolved;
|
|
2403
|
+
};
|
|
2404
|
+
const compile = (pattern, excluded = false) => {
|
|
2405
|
+
if (pm !== "deno" && pm !== "aube") return globs.compile(pattern, pm === "npm" ? {
|
|
2406
|
+
...defaultWorkspaceGlobOptions,
|
|
2407
|
+
hashComments: true
|
|
2408
|
+
} : defaultWorkspaceGlobOptions);
|
|
2409
|
+
const glob = compileAdditionalWorkspaceGlob(pattern, pm, excluded, caseInsensitiveGlobs, globs);
|
|
2410
|
+
return pm === "deno" && !excluded ? anchorWorkspacePrefix(pattern, glob, resolveChild) : glob;
|
|
2411
|
+
};
|
|
2412
|
+
const included = positive.map((pattern) => compile(pattern));
|
|
2413
|
+
const excluded = negative.map((pattern) => {
|
|
2414
|
+
return {
|
|
2415
|
+
glob: compile(pattern, true),
|
|
2416
|
+
subtree: pm === "deno" || pm === "aube" || pm === "bun" ? void 0 : pattern === "**" ? { matches: () => true } : pattern.endsWith("/**") ? compile(pattern.slice(0, -3), true) : void 0
|
|
2417
|
+
};
|
|
2418
|
+
});
|
|
2419
|
+
const declaredBase = (pattern) => {
|
|
2420
|
+
const parts = pattern.split("/");
|
|
2421
|
+
const wildcard = parts.findIndex(hasBasicWorkspaceWildcard);
|
|
2422
|
+
return parts.slice(0, wildcard < 0 ? parts.length : wildcard).join("/") || ".";
|
|
2423
|
+
};
|
|
2424
|
+
const relatedBases = (left, right) => left === "." || right === "." || left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
2425
|
+
const denoPositive = pm === "deno" ? positive.filter(hasBasicWorkspaceWildcard).map((pattern) => ({
|
|
2426
|
+
base: declaredBase(pattern),
|
|
2427
|
+
scope: anchorWorkspacePrefix(declaredBase(pattern), {
|
|
2428
|
+
matches: () => true,
|
|
2429
|
+
canDescend: () => true
|
|
2430
|
+
}, resolveChild)
|
|
2431
|
+
})) : [];
|
|
2432
|
+
const denoOrdered = pm === "deno" ? patterns.filter((pattern) => hasBasicWorkspaceWildcard(pattern) || pattern.startsWith("!")).map((raw) => {
|
|
2433
|
+
const isExcluded = raw.startsWith("!");
|
|
2434
|
+
const pattern = stripTrailingWorkspaceSlashes(paths.normalizePattern(isExcluded ? raw.slice(1) : raw));
|
|
2435
|
+
const glob = compile(pattern, isExcluded);
|
|
2436
|
+
if (!isExcluded) return {
|
|
2437
|
+
excluded: false,
|
|
2438
|
+
glob
|
|
2439
|
+
};
|
|
2440
|
+
return {
|
|
2441
|
+
excluded: true,
|
|
2442
|
+
glob: {
|
|
2443
|
+
...glob,
|
|
2444
|
+
matches(directory) {
|
|
2445
|
+
if (!hasBasicWorkspaceWildcard(pattern)) return directory === pattern;
|
|
2446
|
+
if (!glob.matches(directory)) return false;
|
|
2447
|
+
const applicable = denoPositive.filter((item) => item.scope.matches(directory)).map((item) => relatedBases(item.base, declaredBase(pattern)));
|
|
2448
|
+
if (applicable.some(Boolean) && applicable.some((value) => !value)) throw new ConfigError("Deno workspace exclusion has ambiguous overlapping bases; use consistent prefix spelling or separate non-overlapping patterns.");
|
|
2449
|
+
return applicable.some(Boolean);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
};
|
|
2453
|
+
}) : [];
|
|
2454
|
+
const bunPatterns = pm === "bun" ? patterns.map((raw) => {
|
|
2455
|
+
const isExcluded = raw.startsWith("!");
|
|
2456
|
+
const pattern = stripTrailingWorkspaceSlashes(paths.normalizePattern(isExcluded ? raw.slice(1) : raw));
|
|
2457
|
+
const usesGlob = isExcluded || hasBasicWorkspaceWildcard(pattern) || pattern.includes("{") || pattern.includes("[");
|
|
2458
|
+
const glob = globs.compile(pattern, {
|
|
2459
|
+
...defaultWorkspaceGlobOptions,
|
|
2460
|
+
extendedPatterns: false
|
|
2461
|
+
});
|
|
2462
|
+
const subtree = isExcluded && pattern.endsWith("/**") ? globs.compile(pattern.slice(0, -3), {
|
|
2463
|
+
...defaultWorkspaceGlobOptions,
|
|
2464
|
+
extendedPatterns: false
|
|
2465
|
+
}) : void 0;
|
|
2466
|
+
return {
|
|
2467
|
+
isExcluded,
|
|
2468
|
+
usesGlob,
|
|
2469
|
+
glob,
|
|
2470
|
+
matches: (directory) => glob.matches(directory) || glob.matches(`${directory}/`) || subtree?.matches(directory) === true
|
|
2471
|
+
};
|
|
2472
|
+
}) : [];
|
|
2473
|
+
const bunOrdered = bunPatterns.filter(({ usesGlob }) => usesGlob);
|
|
2474
|
+
const bunLiterals = bunPatterns.filter(({ isExcluded, usesGlob }) => !isExcluded && !usesGlob);
|
|
2475
|
+
const bunTraversal = bunPatterns.filter(({ isExcluded }) => !isExcluded).map(({ glob }) => glob);
|
|
2476
|
+
const denoLiteral = pm === "deno" ? positive.filter((pattern) => !hasBasicWorkspaceWildcard(pattern)).map((pattern) => compile(pattern)) : [];
|
|
2477
|
+
if (definition.denoManifests && denoOrdered.findLast(({ glob }) => glob.matches("."))?.excluded === false) throw new ConfigError("deno.json#workspace: a Deno workspace cannot contain itself.");
|
|
2478
|
+
if (positive.length === 0 || positive.every((pattern) => pattern === ".")) return [];
|
|
2479
|
+
if (!fs.readDirectories) throw new UsageError("Workspace discovery requires FileSystem.readDirectories; no host filesystem fallback is used.");
|
|
2480
|
+
const result = [];
|
|
2481
|
+
const pending = ["."];
|
|
2482
|
+
while (pending.length > 0) {
|
|
2483
|
+
const current = pending.pop();
|
|
2484
|
+
if (current === void 0) break;
|
|
2485
|
+
for (const name of readDirectories(current)) {
|
|
2486
|
+
if (name === ".git" || name === "node_modules") continue;
|
|
2487
|
+
const directory = asRelPath(current === "." ? name : `${current}/${name}`);
|
|
2488
|
+
if (pm === "bun" && name === "CMakeFiles" && !bunLiterals.some(({ glob }) => glob.matches(directory) || glob.canDescend(directory))) continue;
|
|
2489
|
+
const inVendor = skipVendor && (directory === "vendor" || directory.startsWith("vendor/"));
|
|
2490
|
+
if (inVendor && !denoLiteral.some((pattern) => pattern.matches(directory) || pattern.canDescend(directory))) continue;
|
|
2491
|
+
const isExcluded = pm !== "deno" && excluded.some(({ glob }) => glob.matches(directory) || glob.matches(`${directory}/`));
|
|
2492
|
+
if (excluded.some(({ subtree }) => subtree?.matches(directory))) continue;
|
|
2493
|
+
const lastDenoMatch = denoOrdered.findLast(({ glob }) => glob.matches(directory));
|
|
2494
|
+
const lastBunMatch = bunOrdered.findLast(({ matches }) => matches(directory));
|
|
2495
|
+
if (pm === "deno" ? denoLiteral.some((pattern) => pattern.matches(directory)) || !inVendor && lastDenoMatch?.excluded === false : pm === "bun" ? bunLiterals.some(({ glob }) => glob.matches(directory)) || lastBunMatch?.isExcluded === false : !isExcluded && included.some((pattern) => pattern.matches(directory))) result.push(directory);
|
|
2496
|
+
if ((pm === "bun" ? bunTraversal : inVendor ? denoLiteral : included).some((pattern) => pattern.canDescend(directory))) pending.push(directory);
|
|
2730
2497
|
}
|
|
2731
|
-
}
|
|
2732
|
-
|
|
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
|
+
};
|
|
2733
2552
|
//#endregion
|
|
2734
2553
|
//#region src/domain/services/apply-config.ts
|
|
2735
2554
|
/** Select active rules and keep user severity choices as explicit runtime data. */
|
|
@@ -2875,32 +2694,32 @@ const runLint = (opts) => {
|
|
|
2875
2694
|
//#endregion
|
|
2876
2695
|
//#region src/application/lint.ts
|
|
2877
2696
|
/** Evaluate a repository without reporting or executing configuration files. */
|
|
2878
|
-
const prepareLint = (options) => {
|
|
2879
|
-
|
|
2697
|
+
const prepareLint = (options, dependencies) => {
|
|
2698
|
+
const { paths, codecFor, createRepoContext } = dependencies;
|
|
2699
|
+
if (!options || !paths.isAbsolute(options.cwd)) throw new UsageError("cwd must be an absolute filesystem path.");
|
|
2880
2700
|
if ("customRules" in options || "reporters" in options) throw new UsageError("Pass customRules and reporters inside the config option.");
|
|
2881
2701
|
if (options.pm !== void 0 && !isPM(options.pm)) throw new UsageError(`Unknown package manager: ${String(options.pm)}`);
|
|
2882
2702
|
if (options.projectType !== void 0 && !isProjectType(options.projectType)) throw new UsageError(`Unknown project type: ${String(options.projectType)}`);
|
|
2883
2703
|
if (options.pmVersion !== void 0 && (!options.pm || !isStableVersion(options.pmVersion))) throw new UsageError("pmVersion / --pm-version requires pm / --pm and an exact stable version such as 10.16.0.");
|
|
2884
2704
|
if (options.workspaces !== void 0 && typeof options.workspaces !== "boolean") throw new UsageError("workspaces must be a boolean.");
|
|
2885
2705
|
const config = options.config === void 0 ? void 0 : parseConfig(options.config);
|
|
2886
|
-
const
|
|
2706
|
+
const fs = options.fs === void 0 ? dependencies.fileSystem : options.fs;
|
|
2707
|
+
const ctx = createRepoContext(options.cwd, fs, options.projectType ?? config?.projectType);
|
|
2887
2708
|
const pms = resolvePMs(ctx, {
|
|
2888
2709
|
allowed: config?.pms,
|
|
2889
2710
|
pmOverride: options.pm
|
|
2890
2711
|
});
|
|
2891
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.");
|
|
2892
|
-
const configured = applyConfig(rules, config);
|
|
2893
|
-
const pmVersions = {
|
|
2894
|
-
...declaredPMVersion(ctx.packageJson?.packageManager),
|
|
2895
|
-
...config?.pmVersions,
|
|
2896
|
-
...options.pm && options.pmVersion ? { [options.pm]: options.pmVersion } : {}
|
|
2897
|
-
};
|
|
2898
|
-
const fs = options.fs ?? nodeFileSystem;
|
|
2713
|
+
const configured = applyConfig(dependencies.rules, config);
|
|
2899
2714
|
return {
|
|
2900
2715
|
ctx,
|
|
2901
2716
|
pms,
|
|
2902
|
-
pmVersions
|
|
2903
|
-
|
|
2717
|
+
pmVersions: {
|
|
2718
|
+
...declaredPMVersion(ctx.packageJson?.packageManager),
|
|
2719
|
+
...config?.pmVersions,
|
|
2720
|
+
...options.pm && options.pmVersion ? { [options.pm]: options.pmVersion } : {}
|
|
2721
|
+
},
|
|
2722
|
+
members: options.workspaces ? collectWorkspaceMembers(ctx, fs, pms, options.projectType ?? config?.projectType, dependencies) : [],
|
|
2904
2723
|
ruleSet: configured.rules,
|
|
2905
2724
|
severityOverrides: configured.severityOverrides,
|
|
2906
2725
|
codecFor,
|
|
@@ -2944,16 +2763,46 @@ const runPreparedLint = (prepared) => {
|
|
|
2944
2763
|
summary
|
|
2945
2764
|
};
|
|
2946
2765
|
};
|
|
2947
|
-
const lint = (options) => runPreparedLint(prepareLint(options));
|
|
2766
|
+
const lint$1 = (options, dependencies) => runPreparedLint(prepareLint(options, dependencies));
|
|
2767
|
+
//#endregion
|
|
2768
|
+
//#region src/domain/services/filter.ts
|
|
2769
|
+
const EXIT_SUCCESS = 0;
|
|
2770
|
+
const EXIT_FAILURE = 1;
|
|
2771
|
+
const meetsThreshold = (severity, threshold) => SEVERITY_RANK[severity] >= SEVERITY_RANK[threshold];
|
|
2772
|
+
/** Keep only findings at or above `threshold`, recomputing the summary. */
|
|
2773
|
+
const filterBySeverity = (result, threshold) => {
|
|
2774
|
+
const findings = [];
|
|
2775
|
+
const summary = {
|
|
2776
|
+
error: 0,
|
|
2777
|
+
info: 0,
|
|
2778
|
+
warn: 0
|
|
2779
|
+
};
|
|
2780
|
+
for (const finding of result.findings) if (meetsThreshold(finding.severity, threshold)) {
|
|
2781
|
+
findings.push(finding);
|
|
2782
|
+
summary[finding.severity] += 1;
|
|
2783
|
+
}
|
|
2784
|
+
return {
|
|
2785
|
+
findings,
|
|
2786
|
+
summary
|
|
2787
|
+
};
|
|
2788
|
+
};
|
|
2789
|
+
/**
|
|
2790
|
+
* Exit code for a lint run. Non-zero when any finding meets `threshold`
|
|
2791
|
+
* (default: only `error` fails the run).
|
|
2792
|
+
*/
|
|
2793
|
+
const exitCodeForLint = (result, threshold = "error") => {
|
|
2794
|
+
if (result.findings.some((finding) => meetsThreshold(finding.severity, threshold))) return EXIT_FAILURE;
|
|
2795
|
+
return EXIT_SUCCESS;
|
|
2796
|
+
};
|
|
2948
2797
|
//#endregion
|
|
2949
2798
|
//#region src/application/commands/lint.ts
|
|
2950
2799
|
/** Evaluate and report. Executable config loading belongs to the CLI adapter. */
|
|
2951
|
-
const lintCommand = async (options, io) => {
|
|
2800
|
+
const lintCommand$1 = async (options, io, dependencies, reporters) => {
|
|
2952
2801
|
if (!options) throw new UsageError("Lint options are required.");
|
|
2953
2802
|
if (options.severity !== void 0 && !isSeverity(options.severity)) throw new UsageError(`Invalid severity: ${String(options.severity)}`);
|
|
2954
|
-
const prepared = prepareLint(options);
|
|
2955
|
-
const registry = createRegistry(prepared.reporters);
|
|
2956
|
-
const selection = options.reporter ??
|
|
2803
|
+
const prepared = prepareLint(options, dependencies);
|
|
2804
|
+
const registry = reporters.createRegistry(prepared.reporters);
|
|
2805
|
+
const selection = options.reporter ?? reporters.defaultName;
|
|
2957
2806
|
const reporter = typeof selection === "string" ? registry.get(selection) : selection;
|
|
2958
2807
|
if (!isReporterShape(reporter)) throw new UsageError(`${typeof selection === "string" ? "Unknown" : "Invalid"} reporter: ${String(selection)} (available: ${[...registry.keys()].join(", ")})`);
|
|
2959
2808
|
const result = runPreparedLint(prepared);
|
|
@@ -2962,12 +2811,235 @@ const lintCommand = async (options, io) => {
|
|
|
2962
2811
|
return exitCode;
|
|
2963
2812
|
};
|
|
2964
2813
|
//#endregion
|
|
2814
|
+
//#region src/domain/schemas/package-json.ts
|
|
2815
|
+
const PackageJsonSchema = vb.looseObject({
|
|
2816
|
+
files: vb.optional(vb.array(vb.string())),
|
|
2817
|
+
name: vb.optional(vb.string()),
|
|
2818
|
+
packageManager: vb.optional(vb.string()),
|
|
2819
|
+
private: vb.optional(vb.boolean()),
|
|
2820
|
+
publishConfig: vb.optional(vb.looseObject({ access: vb.optional(vb.picklist([
|
|
2821
|
+
"public",
|
|
2822
|
+
"restricted",
|
|
2823
|
+
"private"
|
|
2824
|
+
])) })),
|
|
2825
|
+
trustedDependencies: vb.optional(vb.array(vb.string()))
|
|
2826
|
+
});
|
|
2827
|
+
const parsePackageJson = (value) => {
|
|
2828
|
+
if (!isPlainRecord(value)) throw new ConfigError("package.json: expected an object at the root.");
|
|
2829
|
+
const result = vb.safeParse(PackageJsonSchema, value);
|
|
2830
|
+
if (result.success) return result.output;
|
|
2831
|
+
const issue = result.issues[0];
|
|
2832
|
+
const field = vb.getDotPath(issue);
|
|
2833
|
+
throw new ConfigError(`package.json: ${field ? `${field}: ` : ""}${issue.message}`);
|
|
2834
|
+
};
|
|
2835
|
+
//#endregion
|
|
2836
|
+
//#region src/adapters/repo-context.ts
|
|
2837
|
+
const tryParseJson = (text) => {
|
|
2838
|
+
try {
|
|
2839
|
+
return JSON.parse(text);
|
|
2840
|
+
} catch (error) {
|
|
2841
|
+
let errMsg = String(error);
|
|
2842
|
+
if (error instanceof Error) errMsg = error.message;
|
|
2843
|
+
throw new ConfigError(`package.json: invalid JSON — ${errMsg}`);
|
|
2844
|
+
}
|
|
2845
|
+
};
|
|
2846
|
+
const readPackageJson = (raw) => {
|
|
2847
|
+
return parsePackageJson(tryParseJson(raw.trim()));
|
|
2848
|
+
};
|
|
2849
|
+
const createRepoContext = (root, fs = nodeFileSystem, projectType) => {
|
|
2850
|
+
if (fs === nodeFileSystem) assertDirectory(root);
|
|
2851
|
+
const readText = (relPath) => fs.readText(resolveIn(root, relPath));
|
|
2852
|
+
const exists = (relPath) => fs.exists(resolveIn(root, relPath));
|
|
2853
|
+
const raw = readText(asRelPath("package.json"));
|
|
2854
|
+
let packageJson = void 0;
|
|
2855
|
+
if (typeof raw !== "undefined") packageJson = readPackageJson(raw);
|
|
2856
|
+
return {
|
|
2857
|
+
exists,
|
|
2858
|
+
packageJson,
|
|
2859
|
+
projectType,
|
|
2860
|
+
readText,
|
|
2861
|
+
root
|
|
2862
|
+
};
|
|
2863
|
+
};
|
|
2864
|
+
//#endregion
|
|
2865
|
+
//#region src/adapters/codecs/ini.ts
|
|
2866
|
+
const INTEGER_PATTERN = /^-?\d+$/u;
|
|
2867
|
+
const coerceBoolean = (str) => {
|
|
2868
|
+
if (str === "true") return true;
|
|
2869
|
+
if (str === "false") return false;
|
|
2870
|
+
};
|
|
2871
|
+
const coerceInteger = (str) => {
|
|
2872
|
+
if (INTEGER_PATTERN.test(str)) return Number(str);
|
|
2873
|
+
};
|
|
2874
|
+
const coerce = (raw) => {
|
|
2875
|
+
if (typeof raw !== "string") return raw;
|
|
2876
|
+
return coerceBoolean(raw) ?? coerceInteger(raw) ?? raw;
|
|
2877
|
+
};
|
|
2878
|
+
//#endregion
|
|
2879
|
+
//#region src/adapters/codecs/store.ts
|
|
2880
|
+
const CODECS = {
|
|
2881
|
+
json: { parse(text) {
|
|
2882
|
+
const trimmed = text.trim();
|
|
2883
|
+
return toParsedConfig(JSON.parse(trimmed));
|
|
2884
|
+
} },
|
|
2885
|
+
npmrc: { parse(text) {
|
|
2886
|
+
const raw = toParsedConfig(ini.parse(text));
|
|
2887
|
+
const config = {};
|
|
2888
|
+
for (const [key, value] of Object.entries(raw)) if (Array.isArray(value)) config[key] = value.map((elem) => coerce(elem));
|
|
2889
|
+
else if (typeof value === "object" && value !== null) config[key] = toParsedConfig(value);
|
|
2890
|
+
else config[key] = coerce(value);
|
|
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];
|
|
2909
|
+
//#endregion
|
|
2910
|
+
//#region src/adapters/reporters/github.ts
|
|
2911
|
+
const COMMAND = {
|
|
2912
|
+
error: "error",
|
|
2913
|
+
info: "notice",
|
|
2914
|
+
warn: "warning"
|
|
2915
|
+
};
|
|
2916
|
+
const escapeData = (raw) => raw.replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
|
|
2917
|
+
const escapeProp = (raw) => escapeData(raw).replaceAll(":", "%3A").replaceAll(",", "%2C");
|
|
2918
|
+
/** Emit GitHub Actions workflow commands (annotations on PRs). */
|
|
2919
|
+
const githubReporter = {
|
|
2920
|
+
format(result, io) {
|
|
2921
|
+
for (const finding of result.findings) {
|
|
2922
|
+
let file = "";
|
|
2923
|
+
if (finding.file) file = `file=${escapeProp(finding.file)},`;
|
|
2924
|
+
let body = `[${finding.pm}] ${finding.message}`;
|
|
2925
|
+
if (finding.docs) body += ` (${finding.docs})`;
|
|
2926
|
+
io.stdout(`::${COMMAND[finding.severity]} ${file}title=${escapeProp(finding.ruleId)}::${escapeData(body)}`);
|
|
2927
|
+
}
|
|
2928
|
+
},
|
|
2929
|
+
name: "github"
|
|
2930
|
+
};
|
|
2931
|
+
//#endregion
|
|
2932
|
+
//#region src/adapters/reporters/json.ts
|
|
2933
|
+
/**
|
|
2934
|
+
* Versioned machine-readable output — the public contract consumed by
|
|
2935
|
+
* external fixers (see docs/json-output.md). Bump `schemaVersion` on any
|
|
2936
|
+
* breaking shape change and update that doc in the same commit.
|
|
2937
|
+
*/
|
|
2938
|
+
const SCHEMA_VERSION = 2;
|
|
2939
|
+
const jsonReporter = {
|
|
2940
|
+
format(result, io) {
|
|
2941
|
+
io.stdout(JSON.stringify({
|
|
2942
|
+
schemaVersion: SCHEMA_VERSION,
|
|
2943
|
+
siroVersion: version,
|
|
2944
|
+
findings: result.findings,
|
|
2945
|
+
summary: result.summary
|
|
2946
|
+
}, void 0, 2));
|
|
2947
|
+
},
|
|
2948
|
+
name: "json"
|
|
2949
|
+
};
|
|
2950
|
+
//#endregion
|
|
2951
|
+
//#region src/adapters/reporters/pretty.ts
|
|
2952
|
+
const GLYPH = {
|
|
2953
|
+
error: "✖ error",
|
|
2954
|
+
info: "ℹ info",
|
|
2955
|
+
warn: "⚠ warn"
|
|
2956
|
+
};
|
|
2957
|
+
/**
|
|
2958
|
+
* Decide colour at call time, not at import time.
|
|
2959
|
+
*
|
|
2960
|
+
* picocolors snapshots `isColorSupported` when imported, so a later
|
|
2961
|
+
* `process.env.NO_COLOR = '1'` (or a different stdout) is ignored. The
|
|
2962
|
+
* reporter is invoked per `siro lint` run from many contexts (TTY, CI
|
|
2963
|
+
* with FORCE_COLOR, redirected output, …) — checking each call is the
|
|
2964
|
+
* only way NO_COLOR can truly win, as https://no-color.org/ requires.
|
|
2965
|
+
*/
|
|
2966
|
+
const colorSupportedNow = () => {
|
|
2967
|
+
const { env } = process;
|
|
2968
|
+
if (typeof env.NO_COLOR !== "undefined" && env.NO_COLOR !== "") return false;
|
|
2969
|
+
if ("FORCE_COLOR" in env) return env.FORCE_COLOR !== "0" && env.FORCE_COLOR !== "";
|
|
2970
|
+
return pc.isColorSupported;
|
|
2971
|
+
};
|
|
2972
|
+
const renderFinding = (finding, ctx) => {
|
|
2973
|
+
let where = "";
|
|
2974
|
+
if (finding.file) where = ctx.colors.dim(` (${finding.file})`);
|
|
2975
|
+
ctx.io.stdout(`${ctx.tag[finding.severity](GLYPH[finding.severity])} [${finding.pm}] ${ctx.colors.bold(finding.ruleId)}${where}`);
|
|
2976
|
+
ctx.io.stdout(` ${finding.message}`);
|
|
2977
|
+
for (const step of finding.remediation?.kind === "manual" ? finding.remediation.steps : []) ctx.io.stdout(` ↳ ${step}`);
|
|
2978
|
+
if (finding.docs) ctx.io.stdout(ctx.colors.dim(` → ${finding.docs}`));
|
|
2979
|
+
};
|
|
2980
|
+
const buildRenderCtx = (io) => {
|
|
2981
|
+
const colors = pc.createColors(colorSupportedNow());
|
|
2982
|
+
return {
|
|
2983
|
+
colors,
|
|
2984
|
+
io,
|
|
2985
|
+
tag: {
|
|
2986
|
+
error: colors.red,
|
|
2987
|
+
info: colors.cyan,
|
|
2988
|
+
warn: colors.yellow
|
|
2989
|
+
}
|
|
2990
|
+
};
|
|
2991
|
+
};
|
|
2992
|
+
const prettyReporter = {
|
|
2993
|
+
format(result, io) {
|
|
2994
|
+
const ctx = buildRenderCtx(io);
|
|
2995
|
+
if (result.findings.length === 0) {
|
|
2996
|
+
io.stdout(ctx.colors.green("✔ No security best-practice issues found."));
|
|
2997
|
+
return;
|
|
2998
|
+
}
|
|
2999
|
+
for (const finding of result.findings) renderFinding(finding, ctx);
|
|
3000
|
+
const { error, warn, info } = result.summary;
|
|
3001
|
+
io.stdout("");
|
|
3002
|
+
io.stdout(`Summary: ${error} error, ${warn} warn, ${info} info`);
|
|
3003
|
+
},
|
|
3004
|
+
name: "pretty"
|
|
3005
|
+
};
|
|
3006
|
+
//#endregion
|
|
3007
|
+
//#region src/adapters/reporters/registry.ts
|
|
3008
|
+
const BUILTINS = [
|
|
3009
|
+
prettyReporter,
|
|
3010
|
+
jsonReporter,
|
|
3011
|
+
githubReporter
|
|
3012
|
+
];
|
|
3013
|
+
const DEFAULT_REPORTER_NAME = prettyReporter.name;
|
|
3014
|
+
const JSON_REPORTER_NAME = jsonReporter.name;
|
|
3015
|
+
/** Names of every built-in reporter, in stable display order. */
|
|
3016
|
+
const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
|
|
3017
|
+
/** Later registrations replace earlier reporters with the same name. */
|
|
3018
|
+
const createRegistry = (extras = []) => new Map([...BUILTINS, ...extras].map((reporter) => [reporter.name, reporter]));
|
|
3019
|
+
//#endregion
|
|
3020
|
+
//#region src/composition/lint.ts
|
|
3021
|
+
const dependencies = {
|
|
3022
|
+
rules,
|
|
3023
|
+
fileSystem: nodeFileSystem,
|
|
3024
|
+
paths: nodePaths,
|
|
3025
|
+
createRepoContext,
|
|
3026
|
+
codecFor,
|
|
3027
|
+
globs: minimatchGlobs,
|
|
3028
|
+
caseInsensitiveGlobs: process.platform === "darwin" || process.platform === "win32"
|
|
3029
|
+
};
|
|
3030
|
+
/** Public Node API: callers can replace the filesystem without assembling the application. */
|
|
3031
|
+
const lint = (options) => lint$1(options, dependencies);
|
|
3032
|
+
const lintCommand = (options, io) => lintCommand$1(options, io, dependencies, {
|
|
3033
|
+
defaultName: DEFAULT_REPORTER_NAME,
|
|
3034
|
+
createRegistry
|
|
3035
|
+
});
|
|
3036
|
+
//#endregion
|
|
2965
3037
|
//#region src/adapters/node-io.ts
|
|
2966
3038
|
const nodeIO = {
|
|
2967
3039
|
stderr: (line) => process.stderr.write(`${line}\n`),
|
|
2968
3040
|
stdout: (line) => process.stdout.write(`${line}\n`)
|
|
2969
3041
|
};
|
|
2970
3042
|
//#endregion
|
|
2971
|
-
export { ConfigError as A, SUPPORTED_NODE_RANGE as C, isNodeError as D, nodeFileSystem as E, UsageError as M, asAbsPath as O,
|
|
3043
|
+
export { ConfigError as A, SUPPORTED_NODE_RANGE as C, isNodeError as D, nodeFileSystem as E, UsageError as M, asAbsPath as O, isSupportedNodeVersion as S, assertDirectory as T, isProjectType as _, DEFAULT_REPORTER_NAME as a, isPM as b, jsonReporter as c, requireConfigKey as d, CONFIG_FILES as f, PROJECT_TYPES as g, getByPath as h, BUILTIN_REPORTER_NAMES as i, SiroError as j, asRelPath as k, githubReporter as l, defineRule as m, lint as n, JSON_REPORTER_NAME as o, loadConfig as p, lintCommand as r, prettyReporter as s, nodeIO as t, overrideBindings as u, PMS as v, version as w, isSeverity as x, SEVERITIES as y };
|
|
2972
3044
|
|
|
2973
|
-
//# sourceMappingURL=node-io-
|
|
3045
|
+
//# sourceMappingURL=node-io-BDHg4J2w.mjs.map
|