@pho9ubenaa/siro 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +3 -2
- package/dist/cli.js +2 -3
- package/dist/cli.mjs +44 -30
- package/dist/index.d.mts +35 -37
- package/dist/index.mjs +2 -2
- package/dist/{node-io-Cb8nixgE.mjs → node-io-CUcMwlGH.mjs} +291 -222
- package/package.json +2 -2
|
@@ -20,11 +20,6 @@ const asRelPath = (path) => path;
|
|
|
20
20
|
* place so individual commands never have to think about exit semantics.
|
|
21
21
|
* The exit-code table lives in docs/configuration.md S"Exit codes" and
|
|
22
22
|
* src/cli/help.ts (HELP_LINT) -- not restated here.
|
|
23
|
-
*
|
|
24
|
-
* One mapping note that belongs at this altitude: filesystem errno errors
|
|
25
|
-
* are routed to 2 by SHAPE (numeric `errno`) in cli run(), so an
|
|
26
|
-
* errno-bearing throw from a user extension also lands on 2 rather than the
|
|
27
|
-
* 70 crash path -- same remedy (fix the environment) either way.
|
|
28
23
|
*/
|
|
29
24
|
var SiroError = class extends Error {
|
|
30
25
|
/** Recommended process exit code for this error. */
|
|
@@ -114,9 +109,7 @@ const isPM = (value) => PM_SET.has(value);
|
|
|
114
109
|
const isSeverity = (value) => SEVERITY_SET.has(value);
|
|
115
110
|
const PACKAGE_MANAGER_FIELD = /^(?<pmName>[a-z]+)(?:@.+)?$/u;
|
|
116
111
|
const parsePackageManagerField = (value) => {
|
|
117
|
-
const
|
|
118
|
-
let name = void 0;
|
|
119
|
-
if (match && match.groups) name = match.groups.pmName;
|
|
112
|
+
const name = PACKAGE_MANAGER_FIELD.exec(value.trim())?.groups?.pmName;
|
|
120
113
|
if (typeof name !== "undefined" && isPM(name)) return name;
|
|
121
114
|
};
|
|
122
115
|
|
|
@@ -160,7 +153,7 @@ const githubReporter = {
|
|
|
160
153
|
|
|
161
154
|
//#endregion
|
|
162
155
|
//#region src/version.ts
|
|
163
|
-
const version = "0.
|
|
156
|
+
const version = "0.3.0";
|
|
164
157
|
|
|
165
158
|
//#endregion
|
|
166
159
|
//#region src/adapters/reporters/json.ts
|
|
@@ -246,6 +239,8 @@ const BUILTINS = [
|
|
|
246
239
|
jsonReporter,
|
|
247
240
|
githubReporter
|
|
248
241
|
];
|
|
242
|
+
const DEFAULT_REPORTER_NAME = prettyReporter.name;
|
|
243
|
+
const JSON_REPORTER_NAME = jsonReporter.name;
|
|
249
244
|
/** Names of every built-in reporter, in stable display order. */
|
|
250
245
|
const BUILTIN_REPORTER_NAMES = BUILTINS.map((rep) => rep.name);
|
|
251
246
|
/** Build a registry from the builtins plus any extras (later wins on collision). */
|
|
@@ -261,6 +256,29 @@ const createRegistry = (extras = []) => {
|
|
|
261
256
|
};
|
|
262
257
|
};
|
|
263
258
|
|
|
259
|
+
//#endregion
|
|
260
|
+
//#region src/shared/node-version.ts
|
|
261
|
+
const SUPPORTED_NODE_RANGE = "^22.18.0 || ^23.6.0 || >=24.0.0";
|
|
262
|
+
const MAJOR_22 = 22;
|
|
263
|
+
const MINOR_22 = 18;
|
|
264
|
+
const MAJOR_23 = 23;
|
|
265
|
+
const MINOR_23 = 6;
|
|
266
|
+
const ALWAYS_SUPPORTED_SINCE = 24;
|
|
267
|
+
const isSupportedNodeVersion = (version) => {
|
|
268
|
+
const [majorRaw = "", minorRaw = "0"] = version.replace(/^v/u, "").split(".");
|
|
269
|
+
const major = Number(majorRaw);
|
|
270
|
+
const minor = Number(minorRaw);
|
|
271
|
+
if (major >= ALWAYS_SUPPORTED_SINCE) return true;
|
|
272
|
+
if (major === MAJOR_23) return minor >= MINOR_23;
|
|
273
|
+
if (major === MAJOR_22) return minor >= MINOR_22;
|
|
274
|
+
return false;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
//#endregion
|
|
278
|
+
//#region src/domain/entities/project-type.ts
|
|
279
|
+
const PROJECT_TYPES = ["application", "package"];
|
|
280
|
+
const isProjectType = (value) => PROJECT_TYPES.some((projectType) => projectType === value);
|
|
281
|
+
|
|
264
282
|
//#endregion
|
|
265
283
|
//#region src/adapters/node-errors.ts
|
|
266
284
|
/**
|
|
@@ -310,6 +328,12 @@ const exitCodeForLint = (result, threshold = "error") => {
|
|
|
310
328
|
|
|
311
329
|
//#endregion
|
|
312
330
|
//#region src/domain/entities/config-value.ts
|
|
331
|
+
const CODEC_KINDS = [
|
|
332
|
+
"json",
|
|
333
|
+
"npmrc",
|
|
334
|
+
"toml",
|
|
335
|
+
"yaml"
|
|
336
|
+
];
|
|
313
337
|
const isParsedConfigObject = (value) => {
|
|
314
338
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
315
339
|
const proto = Object.getPrototypeOf(value);
|
|
@@ -327,6 +351,7 @@ const getByPath = (config, keyPath) => {
|
|
|
327
351
|
let current = config;
|
|
328
352
|
for (const key of keyPath) {
|
|
329
353
|
if (!isParsedConfigObject(current)) return;
|
|
354
|
+
if (!Object.hasOwn(current, key)) return;
|
|
330
355
|
current = current[key];
|
|
331
356
|
}
|
|
332
357
|
return current;
|
|
@@ -347,6 +372,29 @@ const toParsedConfig = (value) => {
|
|
|
347
372
|
return {};
|
|
348
373
|
};
|
|
349
374
|
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/domain/entities/rule.ts
|
|
377
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
378
|
+
const isOptionalString = (value) => typeof value === "undefined" || typeof value === "string";
|
|
379
|
+
const CONFIG_FILE_KINDS = new Set([...CODEC_KINDS, "fileGlob"]);
|
|
380
|
+
const isConfigFileRefShape = (value) => {
|
|
381
|
+
if (!isRecord(value) || typeof value.kind !== "string" || typeof value.path !== "string") return false;
|
|
382
|
+
return CONFIG_FILE_KINDS.has(value.kind);
|
|
383
|
+
};
|
|
384
|
+
const isVersionNoteShape = (value) => typeof value === "undefined" || isRecord(value) && isOptionalString(value.configAvailableSince) && isOptionalString(value.defaultSafeSince) && isOptionalString(value.note);
|
|
385
|
+
const isRuleBindingShape = (value) => {
|
|
386
|
+
if (!isRecord(value)) return false;
|
|
387
|
+
return isConfigFileRefShape(value.file) && (value.fixKind === "auto" || value.fixKind === "advisory") && isOptionalString(value.docs) && (typeof value.severity === "undefined" || typeof value.severity === "string" && isSeverity(value.severity)) && isVersionNoteShape(value.versionNote) && typeof value.check === "function" && typeof value.fix === "function";
|
|
388
|
+
};
|
|
389
|
+
const isBindingsShape = (value) => isRecord(value) && Object.entries(value).every(([pm, binding]) => isPM(pm) && isRuleBindingShape(binding));
|
|
390
|
+
const isProjectTypeValue = (value) => typeof value === "string" && isProjectType(value);
|
|
391
|
+
const isProjectTypesShape = (value) => typeof value === "undefined" || Array.isArray(value) && Array.from(value).every((projectType) => isProjectTypeValue(projectType));
|
|
392
|
+
const isRuleShape = (value) => {
|
|
393
|
+
if (!isRecord(value)) return false;
|
|
394
|
+
return typeof value.id === "string" && typeof value.title === "string" && typeof value.description === "string" && typeof value.severity === "string" && isSeverity(value.severity) && isOptionalString(value.docs) && isProjectTypesShape(value.projectTypes) && isBindingsShape(value.bindings);
|
|
395
|
+
};
|
|
396
|
+
const defineRule = (rule) => rule;
|
|
397
|
+
|
|
350
398
|
//#endregion
|
|
351
399
|
//#region src/adapters/codecs/ini.ts
|
|
352
400
|
const INTEGER_PATTERN = /^-?\d+$/u;
|
|
@@ -393,14 +441,18 @@ const jsonCodec = { parse(text) {
|
|
|
393
441
|
//#region src/adapters/codecs/toml.ts
|
|
394
442
|
const tomlCodec = { parse(text) {
|
|
395
443
|
if (text.trim() === "") return {};
|
|
396
|
-
return toParsedConfig(parse(text));
|
|
444
|
+
return toParsedConfig(parse(text.replace(/^\uFEFF/u, "")));
|
|
397
445
|
} };
|
|
398
446
|
|
|
399
447
|
//#endregion
|
|
400
448
|
//#region src/adapters/codecs/yaml.ts
|
|
401
449
|
const yamlCodec = { parse(text) {
|
|
402
450
|
if (text.trim() === "") return {};
|
|
403
|
-
|
|
451
|
+
const document = parseDocument(text);
|
|
452
|
+
if (document.errors.length > 0) throw document.errors[0];
|
|
453
|
+
return toParsedConfig(document.toJS({ onAnchor: (value) => {
|
|
454
|
+
JSON.stringify(value);
|
|
455
|
+
} }));
|
|
404
456
|
} };
|
|
405
457
|
|
|
406
458
|
//#endregion
|
|
@@ -461,14 +513,12 @@ const applyOverride = (rule, overrides, out) => {
|
|
|
461
513
|
else out.push(rewriteBindingsForSeverityOverride(rule, override));
|
|
462
514
|
};
|
|
463
515
|
const mergeBase = (base, config) => {
|
|
464
|
-
|
|
465
|
-
if (config) ({customRules} = config);
|
|
516
|
+
const customRules = config?.customRules;
|
|
466
517
|
return [...base, ...customRules ?? []];
|
|
467
518
|
};
|
|
468
519
|
const applyConfig = (base, config) => {
|
|
469
520
|
const merged = mergeBase(base, config);
|
|
470
|
-
|
|
471
|
-
if (config) overrides = config.rules;
|
|
521
|
+
const overrides = config?.rules;
|
|
472
522
|
if (typeof overrides === "undefined") return merged;
|
|
473
523
|
const out = [];
|
|
474
524
|
for (const rule of merged) applyOverride(rule, overrides, out);
|
|
@@ -534,28 +584,6 @@ const overrideBindings = (rule, overrides) => ({
|
|
|
534
584
|
...overrides
|
|
535
585
|
}
|
|
536
586
|
});
|
|
537
|
-
/**
|
|
538
|
-
* Compose `message` with the structured suffix derived from {@link VersionNote}.
|
|
539
|
-
* Exported for the two hand-written bindings that can't be expressed via
|
|
540
|
-
* {@link requireConfigKey}: the pnpm slot of `disable-lifecycle-scripts`
|
|
541
|
-
* (two-key gate+bypass) and `bun-security-scanner` (advisory shape).
|
|
542
|
-
* New rules should grow `requireConfigKey` rather than reach for this
|
|
543
|
-
* helper — every additional caller raises the cost of evolving the suffix
|
|
544
|
-
* format.
|
|
545
|
-
*/
|
|
546
|
-
const buildVersionNoteParts = (versionNote) => {
|
|
547
|
-
const parts = [];
|
|
548
|
-
if (typeof versionNote.configAvailableSince !== "undefined") parts.push(`available since ${versionNote.configAvailableSince}`);
|
|
549
|
-
if (typeof versionNote.defaultSafeSince !== "undefined") parts.push(`default safe since ${versionNote.defaultSafeSince}`);
|
|
550
|
-
if (typeof versionNote.note !== "undefined") parts.push(versionNote.note);
|
|
551
|
-
return parts;
|
|
552
|
-
};
|
|
553
|
-
const renderVersionNoteMessage = (message, versionNote) => {
|
|
554
|
-
if (typeof versionNote === "undefined") return message;
|
|
555
|
-
const parts = buildVersionNoteParts(versionNote);
|
|
556
|
-
if (parts.length === 0) return message;
|
|
557
|
-
return `${message} (${parts.join("; ")})`;
|
|
558
|
-
};
|
|
559
587
|
const isDefaultOk = (spec) => {
|
|
560
588
|
if (spec.accept) return spec.accept(spec.documentedDefault);
|
|
561
589
|
return spec.documentedDefault === spec.value;
|
|
@@ -606,7 +634,7 @@ const checkKeyValue = (spec, config, message) => {
|
|
|
606
634
|
const buildBinding = (spec, applies) => ({
|
|
607
635
|
check(ctx, config) {
|
|
608
636
|
if (typeof applies !== "undefined" && !applies(ctx)) return { state: "na" };
|
|
609
|
-
return checkKeyValue(spec, config,
|
|
637
|
+
return checkKeyValue(spec, config, spec.message);
|
|
610
638
|
},
|
|
611
639
|
docs: spec.docs,
|
|
612
640
|
file: spec.file,
|
|
@@ -626,7 +654,8 @@ const buildBinding = (spec, applies) => ({
|
|
|
626
654
|
return ops;
|
|
627
655
|
},
|
|
628
656
|
fixKind: "auto",
|
|
629
|
-
severity: spec.severity
|
|
657
|
+
severity: spec.severity,
|
|
658
|
+
versionNote: spec.versionNote
|
|
630
659
|
});
|
|
631
660
|
/** Build a Rule from a per-PM table of {file, keyPath, value, message}. */
|
|
632
661
|
const requireConfigKey = (options) => {
|
|
@@ -640,6 +669,7 @@ const requireConfigKey = (options) => {
|
|
|
640
669
|
description: options.description,
|
|
641
670
|
docs: options.docs,
|
|
642
671
|
id: options.id,
|
|
672
|
+
projectTypes: options.projectTypes,
|
|
643
673
|
severity: options.severity,
|
|
644
674
|
title: options.title
|
|
645
675
|
};
|
|
@@ -667,7 +697,7 @@ const advisoryCheck = requireConfigKey({
|
|
|
667
697
|
//#endregion
|
|
668
698
|
//#region src/domain/rules/approved-git-repos.ts
|
|
669
699
|
const { yarnrc: yarnrc$9 } = CONFIG_FILES;
|
|
670
|
-
const message =
|
|
700
|
+
const message = "Set `approvedGitRepositories: []` in .yarnrc.yml to block all git: protocol dependencies (or list approved repository globs).";
|
|
671
701
|
const yarnBinding$2 = {
|
|
672
702
|
check(_ctx, config) {
|
|
673
703
|
const value = getByPath(config, ["approvedGitRepositories"]);
|
|
@@ -688,16 +718,17 @@ const yarnBinding$2 = {
|
|
|
688
718
|
op: "note"
|
|
689
719
|
}];
|
|
690
720
|
},
|
|
691
|
-
fixKind: "advisory"
|
|
721
|
+
fixKind: "advisory",
|
|
722
|
+
versionNote: { configAvailableSince: "yarn 4.14.0" }
|
|
692
723
|
};
|
|
693
|
-
const approvedGitRepos = {
|
|
724
|
+
const approvedGitRepos = defineRule({
|
|
694
725
|
bindings: { yarn: yarnBinding$2 },
|
|
695
726
|
description: "Restrict git: protocol dependencies to an explicit allowlist of approved repository URL patterns.",
|
|
696
727
|
docs: "https://yarnpkg.com/configuration/yarnrc#approvedGitRepositories",
|
|
697
728
|
id: "approved-git-repos",
|
|
698
729
|
severity: "warn",
|
|
699
730
|
title: "Approve git repository dependencies"
|
|
700
|
-
};
|
|
731
|
+
});
|
|
701
732
|
|
|
702
733
|
//#endregion
|
|
703
734
|
//#region src/domain/rules/audit-suppression.ts
|
|
@@ -729,14 +760,14 @@ const yarnBinding$1 = {
|
|
|
729
760
|
},
|
|
730
761
|
fixKind: "advisory"
|
|
731
762
|
};
|
|
732
|
-
const auditSuppression = {
|
|
763
|
+
const auditSuppression = defineRule({
|
|
733
764
|
bindings: { yarn: yarnBinding$1 },
|
|
734
765
|
description: "Flag audit advisory suppressions that may silently hide future vulnerabilities via broad glob patterns.",
|
|
735
766
|
docs: "https://yarnpkg.com/configuration/yarnrc#npmAuditIgnoreAdvisories",
|
|
736
767
|
id: "audit-suppression",
|
|
737
768
|
severity: "info",
|
|
738
769
|
title: "Review audit suppression entries"
|
|
739
|
-
};
|
|
770
|
+
});
|
|
740
771
|
|
|
741
772
|
//#endregion
|
|
742
773
|
//#region src/domain/rules/block-auto-install.ts
|
|
@@ -813,7 +844,10 @@ const builtRule$3 = requireConfigKey({
|
|
|
813
844
|
keyPath: ["blockExoticSubdeps"],
|
|
814
845
|
message: "Set `blockExoticSubdeps: true` in pnpm-workspace.yaml to block git/tarball URLs in transitive dependencies.",
|
|
815
846
|
value: true,
|
|
816
|
-
versionNote: {
|
|
847
|
+
versionNote: {
|
|
848
|
+
configAvailableSince: "pnpm 10.26.0",
|
|
849
|
+
defaultSafeSince: "pnpm 10.26.0"
|
|
850
|
+
}
|
|
817
851
|
}
|
|
818
852
|
},
|
|
819
853
|
description: "Refuse to install transitive dependencies sourced from git or tarball URLs, which bypass registry integrity checking.",
|
|
@@ -827,7 +861,6 @@ const blockExoticSubdeps = overrideBindings(builtRule$3, { npm: npmBinding });
|
|
|
827
861
|
//#endregion
|
|
828
862
|
//#region src/domain/rules/bun-security-scanner.ts
|
|
829
863
|
const { bunfig: bunfig$4 } = CONFIG_FILES;
|
|
830
|
-
const versionNote = { configAvailableSince: "bun 1.3.0" };
|
|
831
864
|
const bunScannerBinding = {
|
|
832
865
|
check(_ctx, config) {
|
|
833
866
|
const scanner = getByPath(config, [
|
|
@@ -838,7 +871,7 @@ const bunScannerBinding = {
|
|
|
838
871
|
if (typeof scanner === "string" && scanner.length > 0) return { state: "ok" };
|
|
839
872
|
return {
|
|
840
873
|
actual: scanner,
|
|
841
|
-
message:
|
|
874
|
+
message: "Configure `[install.security] scanner = \"...\"` in bunfig.toml (e.g. `@socketsecurity/bun-security-scanner`) to scan new packages on install.",
|
|
842
875
|
state: "violation"
|
|
843
876
|
};
|
|
844
877
|
},
|
|
@@ -851,16 +884,17 @@ const bunScannerBinding = {
|
|
|
851
884
|
op: "note"
|
|
852
885
|
}];
|
|
853
886
|
},
|
|
854
|
-
fixKind: "advisory"
|
|
887
|
+
fixKind: "advisory",
|
|
888
|
+
versionNote: { configAvailableSince: "bun 1.3.0" }
|
|
855
889
|
};
|
|
856
|
-
const bunSecurityScanner = {
|
|
890
|
+
const bunSecurityScanner = defineRule({
|
|
857
891
|
bindings: { bun: bunScannerBinding },
|
|
858
892
|
description: "Bun supports a Security Scanner API that intercepts new packages at install time (e.g. Socket Firewall).",
|
|
859
893
|
docs: "https://github.com/bodadotsh/npm-security-best-practices#preinstall-scanners",
|
|
860
894
|
id: "bun-security-scanner",
|
|
861
895
|
severity: "info",
|
|
862
896
|
title: "Enable a bun install-time security scanner"
|
|
863
|
-
};
|
|
897
|
+
});
|
|
864
898
|
|
|
865
899
|
//#endregion
|
|
866
900
|
//#region src/domain/rules/checksum-verification.ts
|
|
@@ -929,6 +963,7 @@ const LOCKFILE_DOCS = {
|
|
|
929
963
|
pnpm: "https://pnpm.io/git#lockfiles",
|
|
930
964
|
yarn: "https://yarnpkg.com/getting-started/qa#should-lockfiles-be-committed-to-the-repository"
|
|
931
965
|
};
|
|
966
|
+
const LOCKFILE_VERSION_NOTES = { deno: { configAvailableSince: "deno 1.28.0" } };
|
|
932
967
|
const lockfileBinding = (pm) => {
|
|
933
968
|
const { lockfiles, reusesLockfiles } = PM_SIGNALS[pm];
|
|
934
969
|
const [primary] = lockfiles;
|
|
@@ -954,10 +989,11 @@ const lockfileBinding = (pm) => {
|
|
|
954
989
|
op: "ensureFileTracked"
|
|
955
990
|
}];
|
|
956
991
|
},
|
|
957
|
-
fixKind: "advisory"
|
|
992
|
+
fixKind: "advisory",
|
|
993
|
+
versionNote: LOCKFILE_VERSION_NOTES[pm]
|
|
958
994
|
};
|
|
959
995
|
};
|
|
960
|
-
const commitLockfile = {
|
|
996
|
+
const commitLockfile = defineRule({
|
|
961
997
|
bindings: {
|
|
962
998
|
aube: lockfileBinding("aube"),
|
|
963
999
|
bun: lockfileBinding("bun"),
|
|
@@ -971,7 +1007,7 @@ const commitLockfile = {
|
|
|
971
1007
|
id: "commit-lockfile",
|
|
972
1008
|
severity: "error",
|
|
973
1009
|
title: "Commit a lockfile"
|
|
974
|
-
};
|
|
1010
|
+
});
|
|
975
1011
|
|
|
976
1012
|
//#endregion
|
|
977
1013
|
//#region src/domain/rules/dependency-overrides.ts
|
|
@@ -1001,7 +1037,7 @@ const makeBinding = (file, fileName, docs) => ({
|
|
|
1001
1037
|
},
|
|
1002
1038
|
fixKind: "advisory"
|
|
1003
1039
|
});
|
|
1004
|
-
const dependencyOverrides = {
|
|
1040
|
+
const dependencyOverrides = defineRule({
|
|
1005
1041
|
bindings: {
|
|
1006
1042
|
aube: makeBinding(aubeWorkspace$7, "aube-workspace.yaml", "https://aube.jdx.dev/settings.html"),
|
|
1007
1043
|
pnpm: makeBinding(pnpmWorkspace$9, "pnpm-workspace.yaml", "https://pnpm.io/settings#overrides")
|
|
@@ -1011,17 +1047,11 @@ const dependencyOverrides = {
|
|
|
1011
1047
|
id: "dependency-overrides",
|
|
1012
1048
|
severity: "info",
|
|
1013
1049
|
title: "Review dependency overrides"
|
|
1014
|
-
};
|
|
1050
|
+
});
|
|
1015
1051
|
|
|
1016
1052
|
//#endregion
|
|
1017
1053
|
//#region src/domain/rules/disable-lifecycle-scripts.ts
|
|
1018
1054
|
const { npmrc: npmrc$5, pnpmWorkspace: pnpmWorkspace$8, yarnrc: yarnrc$6, aubeWorkspace: aubeWorkspace$6, bunfig: bunfig$3 } = CONFIG_FILES;
|
|
1019
|
-
const pnpmStrictDepBuildsDocs = "https://pnpm.io/settings#strictdepbuilds";
|
|
1020
|
-
const pnpmVersionNote = {
|
|
1021
|
-
configAvailableSince: "pnpm 10.3.0",
|
|
1022
|
-
defaultSafeSince: "pnpm 11.0.0"
|
|
1023
|
-
};
|
|
1024
|
-
const pnpmMsg = (body) => renderVersionNoteMessage(body, pnpmVersionNote);
|
|
1025
1055
|
const pnpmBinding$4 = {
|
|
1026
1056
|
check(_ctx, config) {
|
|
1027
1057
|
const bypass = getByPath(config, ["dangerouslyAllowAllBuilds"]);
|
|
@@ -1029,7 +1059,7 @@ const pnpmBinding$4 = {
|
|
|
1029
1059
|
actual: bypass,
|
|
1030
1060
|
expected: false,
|
|
1031
1061
|
manualSteps: ["Remove `dangerouslyAllowAllBuilds: true` from pnpm-workspace.yaml (or set it to `false`). Setting `strictDepBuilds: true` alone has no effect while the bypass remains."],
|
|
1032
|
-
message:
|
|
1062
|
+
message: "`dangerouslyAllowAllBuilds: true` in pnpm-workspace.yaml bypasses strictDepBuilds — remove it (or set it to false) to restore lifecycle-script gating.",
|
|
1033
1063
|
state: "violation"
|
|
1034
1064
|
};
|
|
1035
1065
|
const strict = getByPath(config, ["strictDepBuilds"]);
|
|
@@ -1037,18 +1067,18 @@ const pnpmBinding$4 = {
|
|
|
1037
1067
|
if (typeof strict === "undefined") return {
|
|
1038
1068
|
actual: strict,
|
|
1039
1069
|
expected: true,
|
|
1040
|
-
message:
|
|
1070
|
+
message: "Set `strictDepBuilds: true` in pnpm-workspace.yaml to pin lifecycle-script gating across versions.",
|
|
1041
1071
|
severity: "info",
|
|
1042
1072
|
state: "violation"
|
|
1043
1073
|
};
|
|
1044
1074
|
return {
|
|
1045
1075
|
actual: strict,
|
|
1046
1076
|
expected: true,
|
|
1047
|
-
message:
|
|
1077
|
+
message: "Set `strictDepBuilds: true` in pnpm-workspace.yaml to block silent skips of un-approved dep builds.",
|
|
1048
1078
|
state: "violation"
|
|
1049
1079
|
};
|
|
1050
1080
|
},
|
|
1051
|
-
docs:
|
|
1081
|
+
docs: "https://pnpm.io/settings#strictdepbuilds",
|
|
1052
1082
|
file: pnpmWorkspace$8,
|
|
1053
1083
|
fix() {
|
|
1054
1084
|
return [{
|
|
@@ -1058,7 +1088,11 @@ const pnpmBinding$4 = {
|
|
|
1058
1088
|
value: true
|
|
1059
1089
|
}];
|
|
1060
1090
|
},
|
|
1061
|
-
fixKind: "auto"
|
|
1091
|
+
fixKind: "auto",
|
|
1092
|
+
versionNote: {
|
|
1093
|
+
configAvailableSince: "pnpm 10.3.0",
|
|
1094
|
+
defaultSafeSince: "pnpm 11.0.0"
|
|
1095
|
+
}
|
|
1062
1096
|
};
|
|
1063
1097
|
const aubeBinding = {
|
|
1064
1098
|
check(_ctx, config) {
|
|
@@ -1095,15 +1129,13 @@ const aubeBinding = {
|
|
|
1095
1129
|
},
|
|
1096
1130
|
fixKind: "auto"
|
|
1097
1131
|
};
|
|
1098
|
-
const bunMessage =
|
|
1132
|
+
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).";
|
|
1099
1133
|
const bunBinding = {
|
|
1100
1134
|
check(ctx, config) {
|
|
1101
1135
|
const ignoreScripts = getByPath(config, ["install", "ignoreScripts"]);
|
|
1102
1136
|
if (ignoreScripts === true) return { state: "ok" };
|
|
1103
|
-
const
|
|
1104
|
-
|
|
1105
|
-
if (ctx.packageJson) trusted = ctx.packageJson.trustedDependencies;
|
|
1106
|
-
if (typeof trusted !== "undefined" && trusted.length === EMPTY) return { state: "ok" };
|
|
1137
|
+
const trusted = ctx.packageJson?.trustedDependencies;
|
|
1138
|
+
if (typeof trusted !== "undefined" && trusted.length === 0) return { state: "ok" };
|
|
1107
1139
|
return {
|
|
1108
1140
|
actual: ignoreScripts,
|
|
1109
1141
|
expected: true,
|
|
@@ -1122,7 +1154,8 @@ const bunBinding = {
|
|
|
1122
1154
|
}];
|
|
1123
1155
|
},
|
|
1124
1156
|
fixKind: "auto",
|
|
1125
|
-
severity: "info"
|
|
1157
|
+
severity: "info",
|
|
1158
|
+
versionNote: { configAvailableSince: "bun 1.2.0" }
|
|
1126
1159
|
};
|
|
1127
1160
|
const builtRule$2 = requireConfigKey({
|
|
1128
1161
|
bindings: {
|
|
@@ -1165,15 +1198,14 @@ const yarnMessage = "Set `enableStrictSsl: true` in .yarnrc.yml to enforce SSL c
|
|
|
1165
1198
|
const yarnBinding = {
|
|
1166
1199
|
check(_ctx, config) {
|
|
1167
1200
|
const strictSsl = getByPath(config, ["enableStrictSsl"]);
|
|
1168
|
-
if (strictSsl
|
|
1169
|
-
actual:
|
|
1201
|
+
if (typeof strictSsl !== "undefined" && strictSsl !== true) return {
|
|
1202
|
+
actual: strictSsl,
|
|
1170
1203
|
expected: true,
|
|
1171
1204
|
message: yarnMessage,
|
|
1172
1205
|
state: "violation"
|
|
1173
1206
|
};
|
|
1174
|
-
const EMPTY = 0;
|
|
1175
1207
|
const whitelist = getByPath(config, ["unsafeHttpWhitelist"]);
|
|
1176
|
-
if (Array.isArray(whitelist) && whitelist.length >
|
|
1208
|
+
if (Array.isArray(whitelist) && whitelist.length > 0) return {
|
|
1177
1209
|
actual: whitelist,
|
|
1178
1210
|
expected: "",
|
|
1179
1211
|
manualSteps: ["Review and remove entries from `unsafeHttpWhitelist` in .yarnrc.yml. Each entry permits unencrypted HTTP to that hostname."],
|
|
@@ -1219,12 +1251,24 @@ const builtRule$1 = requireConfigKey({
|
|
|
1219
1251
|
const enforceStrictSsl = overrideBindings(builtRule$1, { yarn: yarnBinding });
|
|
1220
1252
|
|
|
1221
1253
|
//#endregion
|
|
1222
|
-
//#region src/domain/
|
|
1223
|
-
const
|
|
1254
|
+
//#region src/domain/services/project-type.ts
|
|
1255
|
+
const EMPTY$1 = 0;
|
|
1256
|
+
const isPublishableName = (value) => typeof value === "string" && value.trim().length > EMPTY$1;
|
|
1257
|
+
const resolveProjectType = (selected, inferredPackage) => {
|
|
1258
|
+
if (typeof selected !== "undefined") return selected;
|
|
1259
|
+
if (inferredPackage) return "package";
|
|
1260
|
+
return "application";
|
|
1261
|
+
};
|
|
1262
|
+
const resolvePackageJsonProjectType = (ctx) => {
|
|
1224
1263
|
const pkg = ctx.packageJson;
|
|
1225
|
-
|
|
1226
|
-
return
|
|
1264
|
+
const inferredPackage = pkg?.private !== true && isPublishableName(pkg?.name);
|
|
1265
|
+
return resolveProjectType(ctx.projectType, inferredPackage);
|
|
1227
1266
|
};
|
|
1267
|
+
const resolveDenoProjectType = (ctx, denoConfig) => resolveProjectType(ctx.projectType, isPublishableName(getByPath(denoConfig, ["name"])));
|
|
1268
|
+
|
|
1269
|
+
//#endregion
|
|
1270
|
+
//#region src/domain/rules/publishable.ts
|
|
1271
|
+
const isPublishable = (ctx) => resolvePackageJsonProjectType(ctx) === "package";
|
|
1228
1272
|
|
|
1229
1273
|
//#endregion
|
|
1230
1274
|
//#region src/domain/rules/files-field.ts
|
|
@@ -1232,8 +1276,7 @@ const { packageJson: packageJson$1, denoJson: denoJson$3 } = CONFIG_FILES;
|
|
|
1232
1276
|
const packageJsonFilesBinding = {
|
|
1233
1277
|
check(ctx) {
|
|
1234
1278
|
if (!isPublishable(ctx)) return { state: "na" };
|
|
1235
|
-
|
|
1236
|
-
if (ctx.packageJson) ({files} = ctx.packageJson);
|
|
1279
|
+
const files = ctx.packageJson?.files;
|
|
1237
1280
|
if (Array.isArray(files) && files.length > 0) return { state: "ok" };
|
|
1238
1281
|
return {
|
|
1239
1282
|
actual: files,
|
|
@@ -1253,8 +1296,8 @@ const packageJsonFilesBinding = {
|
|
|
1253
1296
|
fixKind: "advisory"
|
|
1254
1297
|
};
|
|
1255
1298
|
const denoPublishBinding = {
|
|
1256
|
-
check(
|
|
1257
|
-
if (
|
|
1299
|
+
check(ctx, config) {
|
|
1300
|
+
if (resolveDenoProjectType(ctx, config) !== "package") return { state: "na" };
|
|
1258
1301
|
const include = getByPath(config, ["publish", "include"]);
|
|
1259
1302
|
if (Array.isArray(include) && include.length > 0) return { state: "ok" };
|
|
1260
1303
|
return {
|
|
@@ -1274,7 +1317,7 @@ const denoPublishBinding = {
|
|
|
1274
1317
|
},
|
|
1275
1318
|
fixKind: "advisory"
|
|
1276
1319
|
};
|
|
1277
|
-
const filesField = {
|
|
1320
|
+
const filesField = defineRule({
|
|
1278
1321
|
bindings: {
|
|
1279
1322
|
aube: packageJsonFilesBinding,
|
|
1280
1323
|
bun: packageJsonFilesBinding,
|
|
@@ -1286,9 +1329,10 @@ const filesField = {
|
|
|
1286
1329
|
description: "An explicit `files` array in package.json restricts what gets published, preventing accidental inclusion of secrets or local files.",
|
|
1287
1330
|
docs: "https://github.com/bodadotsh/npm-security-best-practices#11-review-published-files",
|
|
1288
1331
|
id: "files-field",
|
|
1332
|
+
projectTypes: ["package"],
|
|
1289
1333
|
severity: "info",
|
|
1290
1334
|
title: "Declare a published files allow-list"
|
|
1291
|
-
};
|
|
1335
|
+
});
|
|
1292
1336
|
|
|
1293
1337
|
//#endregion
|
|
1294
1338
|
//#region src/domain/rules/frozen-lockfile.ts
|
|
@@ -1394,14 +1438,14 @@ const pnpmBinding$3 = {
|
|
|
1394
1438
|
},
|
|
1395
1439
|
fixKind: "advisory"
|
|
1396
1440
|
};
|
|
1397
|
-
const frozenStore = {
|
|
1441
|
+
const frozenStore = defineRule({
|
|
1398
1442
|
bindings: { pnpm: pnpmBinding$3 },
|
|
1399
1443
|
description: "Recommend enabling frozenStore to prevent mutations to the content-addressable store, strengthening supply-chain integrity in CI and deploy environments.",
|
|
1400
1444
|
docs: "https://pnpm.io/settings#frozenstore",
|
|
1401
1445
|
id: "frozen-store",
|
|
1402
1446
|
severity: "info",
|
|
1403
1447
|
title: "Enable frozen store"
|
|
1404
|
-
};
|
|
1448
|
+
});
|
|
1405
1449
|
|
|
1406
1450
|
//#endregion
|
|
1407
1451
|
//#region src/domain/rules/hardened-mode.ts
|
|
@@ -1438,12 +1482,22 @@ const RECOMMENDED_RELEASE_AGE_MINUTES = RECOMMENDED_RELEASE_AGE_DAYS * MINUTES_P
|
|
|
1438
1482
|
const RECOMMENDED_RELEASE_AGE_SECONDS = RECOMMENDED_RELEASE_AGE_DAYS * SECONDS_PER_DAY;
|
|
1439
1483
|
const DOCUMENTED_DEFAULT_MINUTES = MINUTES_PER_DAY;
|
|
1440
1484
|
const { npmrc: npmrc$3, pnpmWorkspace: pnpmWorkspace$5, yarnrc: yarnrc$2, bunfig: bunfig$1, denoJson: denoJson$1, aubeWorkspace: aubeWorkspace$4 } = CONFIG_FILES;
|
|
1441
|
-
const
|
|
1442
|
-
const
|
|
1485
|
+
const isPositiveNumber = (value) => typeof value === "number" && value > 0;
|
|
1486
|
+
const DENO_DURATION = /^P(?=.*\d)(?:\d+(?:[.,]\d+)?Y)?(?:\d+(?:[.,]\d+)?M)?(?:\d+(?:[.,]\d+)?W)?(?:\d+(?:[.,]\d+)?D)?(?:T(?=\d)(?:\d+(?:[.,]\d+)?H)?(?:\d+(?:[.,]\d+)?M)?(?:\d+(?:[.,]\d+)?S)?)?$/u;
|
|
1487
|
+
const DENO_ZERO_DURATION = /^P(?=.*\d)(?:0+(?:[.,]0+)?[YMWD])*(?:T(?:0+(?:[.,]0+)?[HMS])*)?$/u;
|
|
1488
|
+
const DENO_DATE = /^\d{4}-\d{2}-\d{2}$/u;
|
|
1489
|
+
const DENO_RFC3339_TIMESTAMP = /^\d{4}-\d{2}-\d{2}[Tt](?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/u;
|
|
1490
|
+
const DENO_DATE_LENGTH = 10;
|
|
1443
1491
|
const isNonDisabledDenoDuration = (value) => {
|
|
1444
|
-
if (typeof value === "number") return value >
|
|
1445
|
-
if (typeof value === "string")
|
|
1446
|
-
|
|
1492
|
+
if (typeof value === "number") return value > 0;
|
|
1493
|
+
if (typeof value === "string") {
|
|
1494
|
+
const isDuration = DENO_DURATION.test(value);
|
|
1495
|
+
const date = value.slice(0, DENO_DATE_LENGTH);
|
|
1496
|
+
const dateTimestamp = Date.parse(`${date}T00:00:00Z`);
|
|
1497
|
+
return isDuration && !DENO_ZERO_DURATION.test(value) || !isDuration && (DENO_DATE.test(value) || DENO_RFC3339_TIMESTAMP.test(value)) && !Number.isNaN(dateTimestamp) && new Date(dateTimestamp).toISOString().slice(0, date.length) === date && (DENO_DATE.test(value) || !Number.isNaN(Date.parse(value)));
|
|
1498
|
+
}
|
|
1499
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1500
|
+
return !("age" in value) || isNonDisabledDenoDuration(value.age);
|
|
1447
1501
|
};
|
|
1448
1502
|
const minimumReleaseAge = requireConfigKey({
|
|
1449
1503
|
bindings: {
|
|
@@ -1468,10 +1522,15 @@ const minimumReleaseAge = requireConfigKey({
|
|
|
1468
1522
|
deno: {
|
|
1469
1523
|
accept: isNonDisabledDenoDuration,
|
|
1470
1524
|
docs: "https://docs.deno.com/runtime/reference/deno_json/",
|
|
1525
|
+
documentedDefault: DOCUMENTED_DEFAULT_MINUTES,
|
|
1471
1526
|
file: denoJson$1,
|
|
1472
1527
|
keyPath: ["minimumDependencyAge"],
|
|
1473
1528
|
message: `Set minimumDependencyAge (e.g. "P3D" for a ${RECOMMENDED_RELEASE_AGE_DAYS}-day cooldown) in deno.json.`,
|
|
1474
|
-
value: "P3D"
|
|
1529
|
+
value: "P3D",
|
|
1530
|
+
versionNote: {
|
|
1531
|
+
defaultSafeSince: "deno 2.9.0 (1440 minutes)",
|
|
1532
|
+
note: "object age optional since deno 2.9.4"
|
|
1533
|
+
}
|
|
1475
1534
|
},
|
|
1476
1535
|
npm: {
|
|
1477
1536
|
accept: isPositiveNumber,
|
|
@@ -1541,14 +1600,14 @@ const pnpmBinding$2 = {
|
|
|
1541
1600
|
},
|
|
1542
1601
|
fixKind: "advisory"
|
|
1543
1602
|
};
|
|
1544
|
-
const namedRegistries = {
|
|
1603
|
+
const namedRegistries = defineRule({
|
|
1545
1604
|
bindings: { pnpm: pnpmBinding$2 },
|
|
1546
1605
|
description: "Flag named registry mappings that redirect package resolution to custom registries, which may enable dependency confusion attacks.",
|
|
1547
1606
|
docs: "https://pnpm.io/settings#namedregistries",
|
|
1548
1607
|
id: "named-registries",
|
|
1549
1608
|
severity: "info",
|
|
1550
1609
|
title: "Review named registry mappings"
|
|
1551
|
-
};
|
|
1610
|
+
});
|
|
1552
1611
|
|
|
1553
1612
|
//#endregion
|
|
1554
1613
|
//#region src/domain/rules/paranoid-mode.ts
|
|
@@ -1593,14 +1652,14 @@ const pnpmBinding$1 = {
|
|
|
1593
1652
|
},
|
|
1594
1653
|
fixKind: "advisory"
|
|
1595
1654
|
};
|
|
1596
|
-
const patchedDependencies = {
|
|
1655
|
+
const patchedDependencies = defineRule({
|
|
1597
1656
|
bindings: { pnpm: pnpmBinding$1 },
|
|
1598
1657
|
description: "Flag patched dependencies whose source code is modified by local patch files, bypassing registry integrity verification.",
|
|
1599
1658
|
docs: "https://pnpm.io/settings#patcheddependencies",
|
|
1600
1659
|
id: "patched-dependencies",
|
|
1601
1660
|
severity: "info",
|
|
1602
1661
|
title: "Review patched dependencies"
|
|
1603
|
-
};
|
|
1662
|
+
});
|
|
1604
1663
|
|
|
1605
1664
|
//#endregion
|
|
1606
1665
|
//#region src/domain/rules/pin-exact-versions.ts
|
|
@@ -1614,18 +1673,18 @@ const isConfigObject = (value) => !Array.isArray(value);
|
|
|
1614
1673
|
*/
|
|
1615
1674
|
/** Wildcard segments (`1.x`, `1.*`, `1.2.X`) are ranges too. */
|
|
1616
1675
|
const SPLIT_LIMIT_FIRST = 1;
|
|
1617
|
-
const FIRST_ELEMENT = 0;
|
|
1618
1676
|
const hasWildcardSegment = (spec) => {
|
|
1619
|
-
|
|
1677
|
+
const [core = spec] = spec.split("-", SPLIT_LIMIT_FIRST);
|
|
1678
|
+
return core.split(".").some((seg) => seg === "x" || seg === "X" || seg === "*");
|
|
1620
1679
|
};
|
|
1621
1680
|
const RANGE_OPERATOR = /[\^~]|>=|<=|<|>|\|\||\s/u;
|
|
1622
|
-
const
|
|
1623
|
-
const
|
|
1681
|
+
const INCOMPLETE_NUMERIC_VERSION = /^\d+(?:\.\d+)?(?:-|$)/u;
|
|
1682
|
+
const isRangeSpec = (spec) => spec === "" || spec === "*" || RANGE_OPERATOR.test(spec) || INCOMPLETE_NUMERIC_VERSION.test(spec) || hasWildcardSegment(spec);
|
|
1624
1683
|
const AFTER_AT_SIGN = 1;
|
|
1625
1684
|
const hasSemverRange = (specifier) => {
|
|
1626
1685
|
if (specifier.startsWith("http://") || specifier.startsWith("https://")) return false;
|
|
1627
1686
|
const at = specifier.lastIndexOf("@");
|
|
1628
|
-
if (at <=
|
|
1687
|
+
if (at <= 0) return false;
|
|
1629
1688
|
return isRangeSpec(specifier.slice(at + AFTER_AT_SIGN));
|
|
1630
1689
|
};
|
|
1631
1690
|
const collectRangedImports = (imports) => {
|
|
@@ -1634,9 +1693,8 @@ const collectRangedImports = (imports) => {
|
|
|
1634
1693
|
return offenders;
|
|
1635
1694
|
};
|
|
1636
1695
|
const MAX_SAMPLE_COUNT = 3;
|
|
1637
|
-
const EMPTY$2 = 0;
|
|
1638
1696
|
const formatOffenders = (offenders) => {
|
|
1639
|
-
const sample = offenders.slice(
|
|
1697
|
+
const sample = offenders.slice(0, MAX_SAMPLE_COUNT).join(", ");
|
|
1640
1698
|
let more = "";
|
|
1641
1699
|
if (offenders.length > MAX_SAMPLE_COUNT) more = ` (and ${offenders.length - MAX_SAMPLE_COUNT} more)`;
|
|
1642
1700
|
return {
|
|
@@ -1655,7 +1713,7 @@ const denoBinding = {
|
|
|
1655
1713
|
const imports = extractImportsRecord(config);
|
|
1656
1714
|
if (!imports) return OK;
|
|
1657
1715
|
const offenders = collectRangedImports(imports);
|
|
1658
|
-
if (offenders.length ===
|
|
1716
|
+
if (offenders.length === 0) return OK;
|
|
1659
1717
|
return formatOffenders(offenders);
|
|
1660
1718
|
},
|
|
1661
1719
|
docs: "https://docs.deno.com/runtime/reference/cli/add/",
|
|
@@ -1667,7 +1725,8 @@ const denoBinding = {
|
|
|
1667
1725
|
op: "note"
|
|
1668
1726
|
}];
|
|
1669
1727
|
},
|
|
1670
|
-
fixKind: "advisory"
|
|
1728
|
+
fixKind: "advisory",
|
|
1729
|
+
versionNote: { configAvailableSince: "deno 1.30.0" }
|
|
1671
1730
|
};
|
|
1672
1731
|
const baseRule = requireConfigKey({
|
|
1673
1732
|
bindings: {
|
|
@@ -1751,6 +1810,7 @@ const provenance = requireConfigKey({
|
|
|
1751
1810
|
description: "Provenance statements (via Sigstore) tie a release to its source and build, letting consumers verify it was not tampered with.",
|
|
1752
1811
|
docs: "https://github.com/bodadotsh/npm-security-best-practices#10-generate-provenance-statements",
|
|
1753
1812
|
id: "provenance",
|
|
1813
|
+
projectTypes: ["package"],
|
|
1754
1814
|
severity: "warn",
|
|
1755
1815
|
title: "Publish with provenance"
|
|
1756
1816
|
});
|
|
@@ -1761,8 +1821,7 @@ const { packageJson } = CONFIG_FILES;
|
|
|
1761
1821
|
const publishAccessBinding = {
|
|
1762
1822
|
check(ctx) {
|
|
1763
1823
|
if (!isPublishable(ctx)) return { state: "na" };
|
|
1764
|
-
|
|
1765
|
-
if (ctx.packageJson && ctx.packageJson.publishConfig) ({access} = ctx.packageJson.publishConfig);
|
|
1824
|
+
const access = ctx.packageJson?.publishConfig?.access;
|
|
1766
1825
|
if (access === "public" || access === "restricted") return { state: "ok" };
|
|
1767
1826
|
return {
|
|
1768
1827
|
actual: access,
|
|
@@ -1781,7 +1840,7 @@ const publishAccessBinding = {
|
|
|
1781
1840
|
},
|
|
1782
1841
|
fixKind: "advisory"
|
|
1783
1842
|
};
|
|
1784
|
-
const publishAccess = {
|
|
1843
|
+
const publishAccess = defineRule({
|
|
1785
1844
|
bindings: {
|
|
1786
1845
|
aube: publishAccessBinding,
|
|
1787
1846
|
bun: publishAccessBinding,
|
|
@@ -1792,9 +1851,10 @@ const publishAccess = {
|
|
|
1792
1851
|
description: "Set `publishConfig.access` so a misconfigured scope or registry never accidentally publishes an internal package publicly.",
|
|
1793
1852
|
docs: "https://github.com/bodadotsh/npm-security-best-practices#for-maintainers",
|
|
1794
1853
|
id: "publish-access",
|
|
1854
|
+
projectTypes: ["package"],
|
|
1795
1855
|
severity: "info",
|
|
1796
1856
|
title: "Declare publish access explicitly"
|
|
1797
|
-
};
|
|
1857
|
+
});
|
|
1798
1858
|
|
|
1799
1859
|
//#endregion
|
|
1800
1860
|
//#region src/domain/rules/store-server.ts
|
|
@@ -1820,14 +1880,14 @@ const pnpmBinding = {
|
|
|
1820
1880
|
},
|
|
1821
1881
|
fixKind: "advisory"
|
|
1822
1882
|
};
|
|
1823
|
-
const storeServer = {
|
|
1883
|
+
const storeServer = defineRule({
|
|
1824
1884
|
bindings: { pnpm: pnpmBinding },
|
|
1825
1885
|
description: "Flag use of an external store server process, which introduces a trust boundary where tampered packages could be served.",
|
|
1826
1886
|
docs: "https://pnpm.io/settings#userunningStoreserver",
|
|
1827
1887
|
id: "store-server",
|
|
1828
1888
|
severity: "info",
|
|
1829
1889
|
title: "Review store server usage"
|
|
1830
|
-
};
|
|
1890
|
+
});
|
|
1831
1891
|
|
|
1832
1892
|
//#endregion
|
|
1833
1893
|
//#region src/domain/rules/strict-allow-scripts.ts
|
|
@@ -1913,48 +1973,39 @@ const trustPolicy = requireConfigKey({
|
|
|
1913
1973
|
|
|
1914
1974
|
//#endregion
|
|
1915
1975
|
//#region src/domain/builtin-rules.ts
|
|
1916
|
-
const
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1976
|
+
const rules = [
|
|
1977
|
+
advisoryCheck,
|
|
1978
|
+
approvedGitRepos,
|
|
1979
|
+
auditSuppression,
|
|
1980
|
+
blockAutoInstall,
|
|
1981
|
+
blockExoticSubdeps,
|
|
1982
|
+
bunSecurityScanner,
|
|
1983
|
+
checksumVerification,
|
|
1984
|
+
commitLockfile,
|
|
1985
|
+
dependencyOverrides,
|
|
1986
|
+
disableLifecycleScripts,
|
|
1987
|
+
enforceStrictSsl,
|
|
1988
|
+
filesField,
|
|
1989
|
+
frozenLockfile,
|
|
1990
|
+
frozenStore,
|
|
1991
|
+
hardenedMode,
|
|
1992
|
+
minimumReleaseAge,
|
|
1993
|
+
namedRegistries,
|
|
1994
|
+
paranoidMode,
|
|
1995
|
+
patchedDependencies,
|
|
1996
|
+
pinExactVersions,
|
|
1937
1997
|
provenance,
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
const rules = Object.values(RULE_REGISTRY);
|
|
1998
|
+
publishAccess,
|
|
1999
|
+
storeServer,
|
|
2000
|
+
strictAllowScripts,
|
|
2001
|
+
strictReleaseAge,
|
|
2002
|
+
strictStoreIntegrity,
|
|
2003
|
+
trustPolicy
|
|
2004
|
+
];
|
|
1946
2005
|
|
|
1947
2006
|
//#endregion
|
|
1948
2007
|
//#region src/domain/services/validate-rule-ids.ts
|
|
1949
2008
|
/**
|
|
1950
|
-
* Cross-check a loaded SiroConfig against the registered builtin rules.
|
|
1951
|
-
*
|
|
1952
|
-
* Pure: no I/O, no globals, takes builtins as an argument so it stays
|
|
1953
|
-
* testable and so the domain layer doesn't reach for `builtin-rules.ts`
|
|
1954
|
-
* via a hidden static dependency. Callers (the adapters-layer loadConfig
|
|
1955
|
-
* and the application-layer assertConfigRuleIdsKnown) pass the builtin
|
|
1956
|
-
* list explicitly.
|
|
1957
|
-
*
|
|
1958
2009
|
* Why both checks in one pass? `duplicates` must be detected against the
|
|
1959
2010
|
* builtin set *before* `unknown` consults the merged (builtin ∪ custom)
|
|
1960
2011
|
* set — otherwise a colliding customRule id silently widens the known
|
|
@@ -1989,7 +2040,6 @@ const validateRuleIds = (config, builtins, extraKnownIds = []) => {
|
|
|
1989
2040
|
|
|
1990
2041
|
//#endregion
|
|
1991
2042
|
//#region src/application/validate-config-rules.ts
|
|
1992
|
-
const EMPTY$1 = 0;
|
|
1993
2043
|
const SINGLE = 1;
|
|
1994
2044
|
const throwUnknownIds = (unknown) => {
|
|
1995
2045
|
let plural = "";
|
|
@@ -1999,7 +2049,7 @@ const throwUnknownIds = (unknown) => {
|
|
|
1999
2049
|
const assertConfigRuleIdsKnown = (userConfig, programmaticRules) => {
|
|
2000
2050
|
if (typeof userConfig === "undefined") return;
|
|
2001
2051
|
const { unknown } = validateRuleIds(userConfig, rules, (programmaticRules ?? []).map((rule) => rule.id));
|
|
2002
|
-
if (unknown.length >
|
|
2052
|
+
if (unknown.length > 0) throwUnknownIds(unknown);
|
|
2003
2053
|
};
|
|
2004
2054
|
|
|
2005
2055
|
//#endregion
|
|
@@ -2014,7 +2064,7 @@ const assertConfigRuleIdsKnown = (userConfig, programmaticRules) => {
|
|
|
2014
2064
|
* A malformed value reads as "absent" — EXCEPT `private`, which falls back to
|
|
2015
2065
|
* `true`: a security tool must never flip a broken `private` into publishable.
|
|
2016
2066
|
*/
|
|
2017
|
-
const
|
|
2067
|
+
const ABSENT = void 0;
|
|
2018
2068
|
const optionalString = vb.fallback(vb.optional(vb.string()), ABSENT);
|
|
2019
2069
|
const PackageJsonSchema = vb.looseObject({
|
|
2020
2070
|
files: vb.fallback(vb.optional(vb.array(vb.string())), ABSENT),
|
|
@@ -2066,7 +2116,7 @@ const tryParseJson = (text) => {
|
|
|
2066
2116
|
const parsePackageJson = (raw) => {
|
|
2067
2117
|
return safeParsePackageJson(tryParseJson(raw.trim()));
|
|
2068
2118
|
};
|
|
2069
|
-
const createRepoContext = (root, fs = nodeFileSystem) => {
|
|
2119
|
+
const createRepoContext = (root, fs = nodeFileSystem, projectType) => {
|
|
2070
2120
|
const readText = (relPath) => fs.readText(resolveIn(root, relPath));
|
|
2071
2121
|
const exists = (relPath) => fs.exists(resolveIn(root, relPath));
|
|
2072
2122
|
const raw = readText(asRelPath("package.json"));
|
|
@@ -2075,6 +2125,7 @@ const createRepoContext = (root, fs = nodeFileSystem) => {
|
|
|
2075
2125
|
return {
|
|
2076
2126
|
exists,
|
|
2077
2127
|
packageJson,
|
|
2128
|
+
projectType,
|
|
2078
2129
|
readText,
|
|
2079
2130
|
root
|
|
2080
2131
|
};
|
|
@@ -2089,8 +2140,9 @@ const CONFIG_NAMES = [
|
|
|
2089
2140
|
"siro.config.js"
|
|
2090
2141
|
];
|
|
2091
2142
|
const ConfigSchema = vb.strictObject({
|
|
2092
|
-
customRules: vb.optional(vb.array(vb.custom(
|
|
2143
|
+
customRules: vb.optional(vb.array(vb.custom(isRuleShape, "must be a structurally valid rule"))),
|
|
2093
2144
|
pms: vb.optional(vb.pipe(vb.array(vb.picklist(PMS)), vb.minLength(MIN_PMS_LENGTH, "must not be empty (omit the key to auto-detect, or list at least one PM)"))),
|
|
2145
|
+
projectType: vb.optional(vb.picklist(PROJECT_TYPES)),
|
|
2094
2146
|
reporters: vb.optional(vb.array(vb.custom(isReporterShape, "must be a { name, format } reporter"))),
|
|
2095
2147
|
rules: vb.optional(vb.record(vb.string(), vb.union([vb.picklist(SEVERITIES), vb.literal("off")])))
|
|
2096
2148
|
}, "unknown config key (check for a typo)");
|
|
@@ -2103,21 +2155,6 @@ const formatIssues = (issues) => issues.map((issue) => {
|
|
|
2103
2155
|
if (keyPath) return `${keyPath}: ${issue.message}`;
|
|
2104
2156
|
return issue.message;
|
|
2105
2157
|
}).join("; ");
|
|
2106
|
-
const TS_STRIPPING_MAJOR_22 = 22;
|
|
2107
|
-
const TS_STRIPPING_MINOR_22 = 18;
|
|
2108
|
-
const TS_STRIPPING_MAJOR_23 = 23;
|
|
2109
|
-
const TS_STRIPPING_MINOR_23 = 6;
|
|
2110
|
-
const TS_STRIPPING_ALWAYS_SINCE = 24;
|
|
2111
|
-
/** Whether a Node version imports .ts via native type stripping (unflagged since 22.18.0 / 23.6.0). */
|
|
2112
|
-
const hasNativeTypeStripping = (version) => {
|
|
2113
|
-
const [majorRaw = "", minorRaw = "0"] = version.replace(/^v/u, "").split(".");
|
|
2114
|
-
const major = Number(majorRaw);
|
|
2115
|
-
const minor = Number(minorRaw);
|
|
2116
|
-
if (major >= TS_STRIPPING_ALWAYS_SINCE) return true;
|
|
2117
|
-
if (major === TS_STRIPPING_MAJOR_23) return minor >= TS_STRIPPING_MINOR_23;
|
|
2118
|
-
if (major === TS_STRIPPING_MAJOR_22) return minor >= TS_STRIPPING_MINOR_22;
|
|
2119
|
-
return false;
|
|
2120
|
-
};
|
|
2121
2158
|
let loadCounter = 0;
|
|
2122
2159
|
const INCREMENT = 1;
|
|
2123
2160
|
const importConfig = (modulePath, name) => {
|
|
@@ -2146,10 +2183,9 @@ const validateSchema = (candidate, name) => {
|
|
|
2146
2183
|
return result.output;
|
|
2147
2184
|
};
|
|
2148
2185
|
const rejectDuplicateCustomRuleIds = (config, name) => {
|
|
2149
|
-
const EMPTY = 0;
|
|
2150
2186
|
const SINGLE = 1;
|
|
2151
2187
|
const { duplicates } = validateRuleIds(config, rules);
|
|
2152
|
-
if (duplicates.length >
|
|
2188
|
+
if (duplicates.length > 0) {
|
|
2153
2189
|
let plural = "";
|
|
2154
2190
|
if (duplicates.length > SINGLE) plural = "s";
|
|
2155
2191
|
throw new ConfigError(`${name}: duplicate customRule id${plural}: ${duplicates.map((id) => `'${id}'`).join(", ")}. Pick a unique id (or use 'rules' to override an existing rule's severity).`);
|
|
@@ -2167,7 +2203,7 @@ const parseCandidate = (configPath, name) => importConfig(configPath, name).then
|
|
|
2167
2203
|
const loadConfig = (cwd, fs = nodeFileSystem, nodeVersion = process.versions.node) => {
|
|
2168
2204
|
const candidate = CONFIG_NAMES.find((name) => fs.exists(asAbsPath(path.join(cwd, name))));
|
|
2169
2205
|
if (typeof candidate === "undefined") return Promise.resolve(void 0);
|
|
2170
|
-
if (candidate.endsWith(".ts") && !
|
|
2206
|
+
if (candidate.endsWith(".ts") && !isSupportedNodeVersion(nodeVersion)) return Promise.reject(new ConfigError(`${candidate} requires Node.js with native type stripping (${SUPPORTED_NODE_RANGE}); current is v${nodeVersion}. Rename the config to siro.config.mjs (plain JS) or upgrade Node.js.`));
|
|
2171
2207
|
return parseCandidate(asAbsPath(path.join(cwd, candidate)), candidate);
|
|
2172
2208
|
};
|
|
2173
2209
|
|
|
@@ -2198,10 +2234,9 @@ const throwCollisionError = (collisions) => {
|
|
|
2198
2234
|
throw new ConfigError(`customRules: ${noun} with builtin, config, or another programmatic rule: ${ids.map((id) => `'${id}'`).join(", ")}. Pick a unique id (or use 'rules' to override an existing rule's severity).`);
|
|
2199
2235
|
};
|
|
2200
2236
|
const mergeProgrammaticRules = (builtins, programmatic, configCustom) => {
|
|
2201
|
-
|
|
2202
|
-
if (typeof programmatic === "undefined" || programmatic.length === EMPTY) return builtins;
|
|
2237
|
+
if (typeof programmatic === "undefined" || programmatic.length === 0) return builtins;
|
|
2203
2238
|
const collisions = detectCollisions(programmatic, new Set([...builtins.map((rule) => rule.id), ...(configCustom ?? []).map((rule) => rule.id)]));
|
|
2204
|
-
if (collisions.size >
|
|
2239
|
+
if (collisions.size > 0) throwCollisionError(collisions);
|
|
2205
2240
|
return [...builtins, ...programmatic];
|
|
2206
2241
|
};
|
|
2207
2242
|
|
|
@@ -2235,19 +2270,8 @@ const buildDetectionSignals = () => {
|
|
|
2235
2270
|
return out;
|
|
2236
2271
|
};
|
|
2237
2272
|
const DETECTION_SIGNALS = buildDetectionSignals();
|
|
2238
|
-
/**
|
|
2239
|
-
* Detect the package managers in use, strongest signal first:
|
|
2240
|
-
* 1) `packageManager` in package.json,
|
|
2241
|
-
* 2) lockfiles + configs (see DETECTION_SIGNALS for the per-PM allow-list
|
|
2242
|
-
* that skips filenames shared between PMs).
|
|
2243
|
-
* Returns every PM with evidence, in canonical `PMS` order. `ctx` carries
|
|
2244
|
-
* the repo signals — `packageJson` (already parsed) and `exists(relPath)`
|
|
2245
|
-
* for filename probing — so embedders can swap in a virtual FS without
|
|
2246
|
-
* touching the real disk.
|
|
2247
|
-
*/
|
|
2248
2273
|
const addDeclaredPM = (ctx, found) => {
|
|
2249
|
-
|
|
2250
|
-
if (ctx.packageJson) declared = ctx.packageJson.packageManager;
|
|
2274
|
+
const declared = ctx.packageJson?.packageManager;
|
|
2251
2275
|
if (typeof declared === "string") {
|
|
2252
2276
|
const pm = parsePackageManagerField(declared);
|
|
2253
2277
|
if (typeof pm !== "undefined") found.add(pm);
|
|
@@ -2268,9 +2292,8 @@ const detectPMs = (ctx) => {
|
|
|
2268
2292
|
const throwEmptyPMsError = (detected, opts, allowed) => {
|
|
2269
2293
|
if (allowed) {
|
|
2270
2294
|
if (opts.pmOverride) throw new UsageError(`--pm ${opts.pmOverride} is not in siro.config.ts pms (${allowed.join(", ")}). Adjust the config or the flag.`);
|
|
2271
|
-
const EMPTY = 0;
|
|
2272
2295
|
let msg = `No package manager detected, and siro.config.ts restricts pms to ${allowed.join(", ")}. Adjust the config or remove the restriction.`;
|
|
2273
|
-
if (detected.length >
|
|
2296
|
+
if (detected.length > 0) msg = `Detected PMs (${detected.join(", ")}) do not match siro.config.ts pms (${allowed.join(", ")}). Adjust the config or remove the restriction.`;
|
|
2274
2297
|
throw new UsageError(msg);
|
|
2275
2298
|
}
|
|
2276
2299
|
throw new UsageError(`No package manager detected. Pass --pm <${PMS.join("|")}> to be explicit.`);
|
|
@@ -2281,7 +2304,7 @@ const throwEmptyPMsError = (detected, opts, allowed) => {
|
|
|
2281
2304
|
* needs. Throws `UsageError` for the two empty-set cases so the
|
|
2282
2305
|
* caller never has to invent its own messaging.
|
|
2283
2306
|
*
|
|
2284
|
-
* Empty-set branches are usage errors, not silent npm fallback
|
|
2307
|
+
* Empty-set branches are usage errors, not a silent npm fallback.
|
|
2285
2308
|
*/
|
|
2286
2309
|
const computePMs = (ctx, opts) => {
|
|
2287
2310
|
let detected = detectPMs(ctx);
|
|
@@ -2311,17 +2334,15 @@ const resolvePMs = (ctx, opts) => {
|
|
|
2311
2334
|
* applyConfig protocol).
|
|
2312
2335
|
*/
|
|
2313
2336
|
const resolveRuleSet = (userConfig, customRules) => {
|
|
2314
|
-
|
|
2315
|
-
if (userConfig) configCustomRules = userConfig.customRules;
|
|
2337
|
+
const configCustomRules = userConfig?.customRules;
|
|
2316
2338
|
const base = mergeProgrammaticRules(rules, customRules, configCustomRules);
|
|
2317
2339
|
assertConfigRuleIdsKnown(userConfig, customRules);
|
|
2318
2340
|
return applyConfig(base, userConfig);
|
|
2319
2341
|
};
|
|
2320
2342
|
const buildRunResult = (options, userConfig) => {
|
|
2321
2343
|
const { cwd, fs, pm, customRules } = options;
|
|
2322
|
-
const ctx = createRepoContext(cwd, fs);
|
|
2323
|
-
|
|
2324
|
-
if (userConfig) allowedPms = userConfig.pms;
|
|
2344
|
+
const ctx = createRepoContext(cwd, fs, options.projectType ?? userConfig?.projectType);
|
|
2345
|
+
const allowedPms = userConfig?.pms;
|
|
2325
2346
|
return {
|
|
2326
2347
|
ctx,
|
|
2327
2348
|
pms: resolvePMs(ctx, {
|
|
@@ -2336,17 +2357,24 @@ const prepareRun = (options) => loadConfig(options.cwd, options.fs).then((userCo
|
|
|
2336
2357
|
|
|
2337
2358
|
//#endregion
|
|
2338
2359
|
//#region src/domain/services/evaluate-bindings.ts
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2360
|
+
const appliesToProject = (rule, projectType) => typeof projectType === "undefined" || typeof rule.projectTypes === "undefined" || rule.projectTypes.some((candidate) => candidate === projectType);
|
|
2361
|
+
const resolveBindingProjectType = (ctx, pm, parseConfig) => {
|
|
2362
|
+
if (typeof ctx.projectType !== "undefined") return ctx.projectType;
|
|
2363
|
+
if (pm === "deno") {
|
|
2364
|
+
const { parsed } = parseConfig(ctx, CONFIG_FILES.denoJson);
|
|
2365
|
+
return resolveDenoProjectType(ctx, parsed);
|
|
2366
|
+
}
|
|
2367
|
+
return resolvePackageJsonProjectType(ctx);
|
|
2368
|
+
};
|
|
2369
|
+
const appliesToBinding = (rule, pm, opts) => {
|
|
2370
|
+
const { ctx, parseConfig } = opts;
|
|
2371
|
+
return typeof rule.projectTypes === "undefined" || appliesToProject(rule, resolveBindingProjectType(ctx, pm, parseConfig));
|
|
2372
|
+
};
|
|
2373
|
+
const evaluateRule = (rule, opts) => {
|
|
2374
|
+
const { ctx, pms, parseConfig, onViolation } = opts;
|
|
2375
|
+
for (const pm of pms) {
|
|
2348
2376
|
const binding = rule.bindings[pm];
|
|
2349
|
-
if (typeof binding !== "undefined") {
|
|
2377
|
+
if (typeof binding !== "undefined" && appliesToBinding(rule, pm, opts)) {
|
|
2350
2378
|
const { parsed } = parseConfig(ctx, binding.file);
|
|
2351
2379
|
const status = binding.check(ctx, parsed);
|
|
2352
2380
|
if (status.state === "violation") onViolation({
|
|
@@ -2358,6 +2386,16 @@ const evaluateBindings = (opts) => {
|
|
|
2358
2386
|
}
|
|
2359
2387
|
}
|
|
2360
2388
|
};
|
|
2389
|
+
/**
|
|
2390
|
+
* Iterate every applicable `(rule, pm)` binding, run its check, and hand
|
|
2391
|
+
* each violation to `onViolation`. Folding the loop into one place keeps
|
|
2392
|
+
* the rule-evaluation contract (parse once, run check, gate on violation)
|
|
2393
|
+
* in a single location so the call site (`runLint`) stays declarative.
|
|
2394
|
+
*/
|
|
2395
|
+
const evaluateBindings = (opts) => {
|
|
2396
|
+
const { ruleSet } = opts;
|
|
2397
|
+
for (const rule of ruleSet) evaluateRule(rule, opts);
|
|
2398
|
+
};
|
|
2361
2399
|
|
|
2362
2400
|
//#endregion
|
|
2363
2401
|
//#region src/domain/services/parse-config-file.ts
|
|
@@ -2394,11 +2432,27 @@ const createConfigParser = (codecFor) => {
|
|
|
2394
2432
|
*/
|
|
2395
2433
|
const decideSeverity = (status, binding, rule) => status.severity ?? binding.severity ?? rule.severity;
|
|
2396
2434
|
|
|
2435
|
+
//#endregion
|
|
2436
|
+
//#region src/domain/services/render-version-note.ts
|
|
2437
|
+
const buildVersionNoteParts = (versionNote) => {
|
|
2438
|
+
const parts = [];
|
|
2439
|
+
if (typeof versionNote.configAvailableSince !== "undefined") parts.push(`available since ${versionNote.configAvailableSince}`);
|
|
2440
|
+
if (typeof versionNote.defaultSafeSince !== "undefined") parts.push(`default safe since ${versionNote.defaultSafeSince}`);
|
|
2441
|
+
if (typeof versionNote.note !== "undefined") parts.push(versionNote.note);
|
|
2442
|
+
return parts;
|
|
2443
|
+
};
|
|
2444
|
+
const renderVersionNoteMessage = (message, versionNote) => {
|
|
2445
|
+
if (typeof versionNote === "undefined") return message;
|
|
2446
|
+
const parts = buildVersionNoteParts(versionNote);
|
|
2447
|
+
if (parts.length === 0) return message;
|
|
2448
|
+
return `${message} (${parts.join("; ")})`;
|
|
2449
|
+
};
|
|
2450
|
+
|
|
2397
2451
|
//#endregion
|
|
2398
2452
|
//#region src/application/run-lint.ts
|
|
2399
2453
|
const EMPTY = 0;
|
|
2400
2454
|
const resolveManualSteps = (raw) => {
|
|
2401
|
-
if (typeof raw !== "undefined" && raw.length >
|
|
2455
|
+
if (typeof raw !== "undefined" && raw.length > 0) return raw;
|
|
2402
2456
|
};
|
|
2403
2457
|
const buildFinding = (visit, ctx) => {
|
|
2404
2458
|
const { rule, pm, binding, status } = visit;
|
|
@@ -2414,9 +2468,9 @@ const buildFinding = (visit, ctx) => {
|
|
|
2414
2468
|
expected: status.expected,
|
|
2415
2469
|
file: filePath,
|
|
2416
2470
|
fix,
|
|
2417
|
-
fixable: binding.fixKind === "auto" && typeof manualSteps === "undefined",
|
|
2471
|
+
fixable: binding.fixKind === "auto" && typeof manualSteps === "undefined" && fix.length > EMPTY,
|
|
2418
2472
|
manualSteps,
|
|
2419
|
-
message: status.message,
|
|
2473
|
+
message: renderVersionNoteMessage(status.message, binding.versionNote),
|
|
2420
2474
|
pm,
|
|
2421
2475
|
ruleId: rule.id,
|
|
2422
2476
|
severity
|
|
@@ -2451,14 +2505,27 @@ const runLint = (opts) => {
|
|
|
2451
2505
|
//#endregion
|
|
2452
2506
|
//#region src/application/commands/lint.ts
|
|
2453
2507
|
const resolveReporter = (value, registry) => {
|
|
2454
|
-
if (typeof value === "undefined") return registry.get(
|
|
2508
|
+
if (typeof value === "undefined") return registry.get(DEFAULT_REPORTER_NAME);
|
|
2455
2509
|
if (typeof value === "string") return registry.get(value);
|
|
2456
2510
|
if (!isReporterShape(value)) throw new UsageError("The 'reporter' option must be a name or a { name, format } object.");
|
|
2457
2511
|
return value;
|
|
2458
2512
|
};
|
|
2459
2513
|
const validateEmbedderReporters = (reporters) => {
|
|
2514
|
+
if (typeof reporters !== "undefined" && !Array.isArray(reporters)) throw new UsageError("The 'reporters' option must be an array of reporter objects.");
|
|
2460
2515
|
for (const rep of reporters ?? []) if (!isReporterShape(rep)) throw new UsageError("A reporter in 'reporters' needs a string 'name' and a 'format' function.");
|
|
2461
2516
|
};
|
|
2517
|
+
const validateSelection = (value, isValid, errorMessage) => {
|
|
2518
|
+
if (typeof value !== "undefined" && (typeof value !== "string" || !isValid(value))) throw new UsageError(errorMessage);
|
|
2519
|
+
};
|
|
2520
|
+
const validateCustomRules = (customRules) => {
|
|
2521
|
+
if (typeof customRules !== "undefined" && (!Array.isArray(customRules) || !Array.from(customRules).every((rule) => isRuleShape(rule)))) throw new UsageError("The 'customRules' option must contain structurally valid rules.");
|
|
2522
|
+
};
|
|
2523
|
+
const validateLintOptions = (options) => {
|
|
2524
|
+
validateCustomRules(options.customRules);
|
|
2525
|
+
validateSelection(options.projectType, isProjectType, `Unknown project type: ${String(options.projectType)} (expected ${PROJECT_TYPES.join("|")})`);
|
|
2526
|
+
validateSelection(options.pm, isPM, `Unknown package manager: ${String(options.pm)} (expected one of: ${PMS.join(", ")})`);
|
|
2527
|
+
validateSelection(options.severity, isSeverity, `Invalid severity: ${String(options.severity)} (expected ${SEVERITIES.join("|")})`);
|
|
2528
|
+
};
|
|
2462
2529
|
const resolveAndValidateReporter = (value, registry) => {
|
|
2463
2530
|
const reporter = resolveReporter(value, registry);
|
|
2464
2531
|
if (typeof reporter === "undefined") throw new UsageError(`Unknown reporter: ${String(value)} (available: ${registry.list().join(", ")})`);
|
|
@@ -2477,15 +2544,17 @@ const formatAndExit = (args) => {
|
|
|
2477
2544
|
return exitCodeForLint(result, failThreshold);
|
|
2478
2545
|
};
|
|
2479
2546
|
/** `siro lint`: detect PMs, evaluate rules, report findings. */
|
|
2480
|
-
const lintCommand = (options, io) =>
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2547
|
+
const lintCommand = async (options, io) => {
|
|
2548
|
+
validateLintOptions(options);
|
|
2549
|
+
const { userConfig, ctx, pms, ruleSet } = await prepareRun({
|
|
2550
|
+
customRules: options.customRules,
|
|
2551
|
+
cwd: options.cwd,
|
|
2552
|
+
fs: options.fs,
|
|
2553
|
+
pm: options.pm,
|
|
2554
|
+
projectType: options.projectType
|
|
2555
|
+
});
|
|
2486
2556
|
validateEmbedderReporters(options.reporters);
|
|
2487
|
-
|
|
2488
|
-
if (userConfig && userConfig.reporters) configReporters = userConfig.reporters;
|
|
2557
|
+
const configReporters = userConfig?.reporters ?? [];
|
|
2489
2558
|
const registry = createRegistry([...options.reporters ?? [], ...configReporters]);
|
|
2490
2559
|
return formatAndExit({
|
|
2491
2560
|
ctx,
|
|
@@ -2495,7 +2564,7 @@ const lintCommand = (options, io) => prepareRun({
|
|
|
2495
2564
|
reporter: resolveAndValidateReporter(options.reporter, registry),
|
|
2496
2565
|
ruleSet
|
|
2497
2566
|
});
|
|
2498
|
-
}
|
|
2567
|
+
};
|
|
2499
2568
|
|
|
2500
2569
|
//#endregion
|
|
2501
2570
|
//#region src/adapters/node-io.ts
|
|
@@ -2505,5 +2574,5 @@ const nodeIO = {
|
|
|
2505
2574
|
};
|
|
2506
2575
|
|
|
2507
2576
|
//#endregion
|
|
2508
|
-
export {
|
|
2509
|
-
//# sourceMappingURL=node-io-
|
|
2577
|
+
export { isSeverity as A, jsonReporter as C, PMS as D, isReporterShape as E, asAbsPath as F, asRelPath as I, UsageError as M, ConfigError as N, SEVERITIES as O, SiroError as P, prettyReporter as S, githubReporter as T, isSupportedNodeVersion as _, nodeFileSystem as a, JSON_REPORTER_NAME as b, requireConfigKey as c, exitCodeForLint as d, filterBySeverity as f, SUPPORTED_NODE_RANGE as g, isProjectType as h, detectPMs as i, wrapCodecError as j, isPM as k, CONFIG_FILES as l, PROJECT_TYPES as m, lintCommand as n, PM_SIGNALS as o, isNodeError as p, renderVersionNoteMessage as r, overrideBindings as s, nodeIO as t, getByPath as u, BUILTIN_REPORTER_NAMES as v, version as w, createRegistry as x, DEFAULT_REPORTER_NAME as y };
|
|
2578
|
+
//# sourceMappingURL=node-io-CUcMwlGH.mjs.map
|