@noctcore/lint-meta-rules 0.1.0 → 0.2.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/README.md +7 -1
- package/dist/index.cjs +439 -0
- package/dist/index.d.cts +127 -1
- package/dist/index.d.ts +127 -1
- package/dist/index.js +434 -0
- package/docs/rules/dockerfile-base-image-digest-pin.md +43 -0
- package/docs/rules/github-actions-runner-pinned.md +42 -0
- package/docs/rules/github-actions-sha-pinned.md +46 -0
- package/docs/rules/security-scanner-version-parity.md +61 -0
- package/docs/rules/service-image-digest-pin.md +52 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -44,7 +44,8 @@ over the whole catalog.
|
|
|
44
44
|
|
|
45
45
|
13 nightcore lint-meta rules are ported as 12 factories — nightcore's `web-file-size-ratchet` and
|
|
46
46
|
`engine-file-size-ratchet` were byte-identical logic and collapse into a single
|
|
47
|
-
`createFileSizeRatchetRule` (instantiated once per capped area).
|
|
47
|
+
`createFileSizeRatchetRule` (instantiated once per capped area). Five CI-hygiene rules (category
|
|
48
|
+
`ci`) are ported from a production NestJS + Vite monorepo, one factory each, for 17 factories in all.
|
|
48
49
|
|
|
49
50
|
| Factory | Source rule(s) | Category | What it enforces |
|
|
50
51
|
| --- | --- | --- | --- |
|
|
@@ -60,6 +61,11 @@ over the whole catalog.
|
|
|
60
61
|
| [`createUiPrimitiveShapeRule`](./docs/rules/ui-primitive-shape.md) | `ui-primitive-shape` | source-text | A folder primitive ships its proof siblings; a flat primitive carries none at the ui root. |
|
|
61
62
|
| [`createTestWorkspaceEnrollmentRule`](./docs/rules/test-workspace-enrollment.md) | `test-workspace-enrollment` | testing | Every tested package is enumerated in the aggregate test script. |
|
|
62
63
|
| [`createTestRunnerSegregationRule`](./docs/rules/test-runner-segregation.md) | `test-runner-segregation` | testing | Bun-side and foreign-side test runners are never mixed within a package. |
|
|
64
|
+
| [`createGithubActionsShaPinnedRule`](./docs/rules/github-actions-sha-pinned.md) | `github-actions-sha-pinned` | ci | Workflow `uses:` refs are pinned to a full commit SHA with a `# vN` comment. |
|
|
65
|
+
| [`createGithubActionsRunnerPinnedRule`](./docs/rules/github-actions-runner-pinned.md) | `github-actions-runner-pinned` | ci | Workflow jobs run on a named runner image, never a `*-latest` label. |
|
|
66
|
+
| [`createServiceImageDigestPinRule`](./docs/rules/service-image-digest-pin.md) | `service-image-digest-pin` | ci | Workflow service/container images and compose images are pinned by `@sha256:` digest. |
|
|
67
|
+
| [`createDockerfileBaseImageDigestPinRule`](./docs/rules/dockerfile-base-image-digest-pin.md) | `dockerfile-base-image-digest-pin` | ci | Dockerfile `FROM` base images are pinned by `@sha256:` digest. |
|
|
68
|
+
| [`createSecurityScannerVersionParityRule`](./docs/rules/security-scanner-version-parity.md) | `security-scanner-version-parity` | ci | CI and the local pre-push hook pin the same secret-scanner version, and the hook checks it at run time. |
|
|
63
69
|
|
|
64
70
|
Every factory is callable with no arguments (all options default), so `createAllRules()` and
|
|
65
71
|
per-factory defaults work out of the box; supply options to retarget a rule at your own repo.
|
package/dist/index.cjs
CHANGED
|
@@ -26,11 +26,16 @@ __export(index_exports, {
|
|
|
26
26
|
createAgentsDocPresenceRule: () => createAgentsDocPresenceRule,
|
|
27
27
|
createAllRules: () => createAllRules,
|
|
28
28
|
createCanonicalHelpersSingleHomeRule: () => createCanonicalHelpersSingleHomeRule,
|
|
29
|
+
createDockerfileBaseImageDigestPinRule: () => createDockerfileBaseImageDigestPinRule,
|
|
29
30
|
createFileSizeRatchetRule: () => createFileSizeRatchetRule,
|
|
31
|
+
createGithubActionsRunnerPinnedRule: () => createGithubActionsRunnerPinnedRule,
|
|
32
|
+
createGithubActionsShaPinnedRule: () => createGithubActionsShaPinnedRule,
|
|
30
33
|
createLayerRankRule: () => createLayerRankRule,
|
|
31
34
|
createNoClonedComponentFoldersRule: () => createNoClonedComponentFoldersRule,
|
|
32
35
|
createNoWarnSeverityRule: () => createNoWarnSeverityRule,
|
|
33
36
|
createPackageShapeRule: () => createPackageShapeRule,
|
|
37
|
+
createSecurityScannerVersionParityRule: () => createSecurityScannerVersionParityRule,
|
|
38
|
+
createServiceImageDigestPinRule: () => createServiceImageDigestPinRule,
|
|
34
39
|
createTestRunnerSegregationRule: () => createTestRunnerSegregationRule,
|
|
35
40
|
createTestSiblingEnforcementRule: () => createTestSiblingEnforcementRule,
|
|
36
41
|
createTestWorkspaceEnrollmentRule: () => createTestWorkspaceEnrollmentRule,
|
|
@@ -58,6 +63,36 @@ function countLines(text) {
|
|
|
58
63
|
function recursiveGlobs(roots, extensions) {
|
|
59
64
|
return roots.flatMap((root) => extensions.map((ext) => `${root}/**/*${ext}`));
|
|
60
65
|
}
|
|
66
|
+
var DEFAULT_WORKFLOW_GLOBS = [
|
|
67
|
+
".github/workflows/*.yml",
|
|
68
|
+
".github/workflows/*.yaml"
|
|
69
|
+
];
|
|
70
|
+
var DEFAULT_SKIP_DIRS = [
|
|
71
|
+
"node_modules",
|
|
72
|
+
".git",
|
|
73
|
+
"dist",
|
|
74
|
+
".turbo",
|
|
75
|
+
"coverage"
|
|
76
|
+
];
|
|
77
|
+
function globFiles(glob, globs, skipDirs = []) {
|
|
78
|
+
const skip = new Set(skipDirs);
|
|
79
|
+
const found = /* @__PURE__ */ new Set();
|
|
80
|
+
for (const pattern of globs) {
|
|
81
|
+
for (const rel of glob(pattern)) {
|
|
82
|
+
if (!rel.split("/").some((segment) => skip.has(segment))) found.add(rel);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return [...found].sort();
|
|
86
|
+
}
|
|
87
|
+
function stripYamlComment(line) {
|
|
88
|
+
return line.replace(/(^|\s)#.*$/u, "");
|
|
89
|
+
}
|
|
90
|
+
function unquote(value) {
|
|
91
|
+
return value.trim().replace(/^(['"])(.*)\1$/u, "$2");
|
|
92
|
+
}
|
|
93
|
+
function anywhereGlobs(baseNames) {
|
|
94
|
+
return baseNames.flatMap((name) => [`**/${name}`, `**/.*/**/${name}`]);
|
|
95
|
+
}
|
|
61
96
|
|
|
62
97
|
// src/rules/agents-doc-presence.ts
|
|
63
98
|
function createAgentsDocPresenceRule(options = {}) {
|
|
@@ -152,6 +187,63 @@ function createCanonicalHelpersSingleHomeRule(options = {}) {
|
|
|
152
187
|
};
|
|
153
188
|
}
|
|
154
189
|
|
|
190
|
+
// src/rules/dockerfile-base-image-digest-pin.ts
|
|
191
|
+
var RULE_ID = "dockerfile-base-image-digest-pin";
|
|
192
|
+
var DEFAULT_DOCKERFILE_GLOBS = anywhereGlobs(["Dockerfile", "Dockerfile.*", "*.Dockerfile"]);
|
|
193
|
+
var FROM_LINE = /^\s*FROM\s+(?:--\S+\s+)*(\S+)(?:\s+AS\s+(\S+))?\s*$/iu;
|
|
194
|
+
var ARG_DEFAULT = /^\s*ARG\s+(\w+)=(\S+)\s*$/iu;
|
|
195
|
+
var DIGEST = /@sha256:[0-9a-f]{64}$/u;
|
|
196
|
+
function checkDockerfileBases(file, text) {
|
|
197
|
+
const violations = [];
|
|
198
|
+
const stages = /* @__PURE__ */ new Set();
|
|
199
|
+
const argDefaults = /* @__PURE__ */ new Map();
|
|
200
|
+
let seenFrom = false;
|
|
201
|
+
text.split("\n").forEach((line, index) => {
|
|
202
|
+
const arg = ARG_DEFAULT.exec(line);
|
|
203
|
+
if (arg?.[1] !== void 0 && arg[2] !== void 0 && !seenFrom) {
|
|
204
|
+
argDefaults.set(arg[1], arg[2].replace(/^(['"])(.*)\1$/u, "$2"));
|
|
205
|
+
}
|
|
206
|
+
const from = FROM_LINE.exec(line);
|
|
207
|
+
if (from?.[1] === void 0) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
seenFrom = true;
|
|
211
|
+
const written = from[1];
|
|
212
|
+
const variable = /^\$\{?(\w+)\}?$/u.exec(written)?.[1];
|
|
213
|
+
const image = variable === void 0 ? written : argDefaults.get(variable) ?? written;
|
|
214
|
+
const isExempt = image.toLowerCase() === "scratch" || stages.has(image.toLowerCase());
|
|
215
|
+
if (from[2] !== void 0) {
|
|
216
|
+
stages.add(from[2].toLowerCase());
|
|
217
|
+
}
|
|
218
|
+
if (!isExempt && !DIGEST.test(image)) {
|
|
219
|
+
const reference = image.split("@")[0];
|
|
220
|
+
violations.push({
|
|
221
|
+
file,
|
|
222
|
+
rule: RULE_ID,
|
|
223
|
+
message: `line ${index + 1}: base image "${image}" is not pinned by digest. Write \`FROM ${reference}@sha256:<digest>\` (resolve it with \`docker buildx imagetools inspect ${reference}\`).`,
|
|
224
|
+
line: index + 1
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
return violations;
|
|
229
|
+
}
|
|
230
|
+
function createDockerfileBaseImageDigestPinRule(options = {}) {
|
|
231
|
+
const dockerfileGlobs = options.dockerfileGlobs ?? DEFAULT_DOCKERFILE_GLOBS;
|
|
232
|
+
const skipDirs = options.skipDirs ?? DEFAULT_SKIP_DIRS;
|
|
233
|
+
const ciCritical = options.ciCritical ?? true;
|
|
234
|
+
return {
|
|
235
|
+
id: RULE_ID,
|
|
236
|
+
category: "ci",
|
|
237
|
+
ciCritical,
|
|
238
|
+
description: "Dockerfile FROM base images must be pinned by `@sha256:` digest (scratch and earlier build stages exempt).",
|
|
239
|
+
run(ctx) {
|
|
240
|
+
return globFiles((p) => ctx.glob(p), dockerfileGlobs, skipDirs).flatMap(
|
|
241
|
+
(file) => checkDockerfileBases(file, ctx.read(file) ?? "")
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
155
247
|
// src/rules/file-size-ratchet.ts
|
|
156
248
|
var import_harness = require("@noctcore/harness");
|
|
157
249
|
var DEFAULT_EXCLUDE_CONTAINS = [".test.", ".spec.", ".stories."];
|
|
@@ -234,6 +326,121 @@ function createFileSizeRatchetRule(options = {}) {
|
|
|
234
326
|
};
|
|
235
327
|
}
|
|
236
328
|
|
|
329
|
+
// src/rules/github-actions-runner-pinned.ts
|
|
330
|
+
var RULE_ID2 = "github-actions-runner-pinned";
|
|
331
|
+
var RUNS_ON = /^(\s*)runs-on:\s*(.*)$/u;
|
|
332
|
+
var DEFAULT_FLOATING_LABEL = /^[\w.-]+-latest$/u;
|
|
333
|
+
function runnerLabels(lines, start, indent) {
|
|
334
|
+
const inline = stripYamlComment(RUNS_ON.exec(lines[start] ?? "")?.[2] ?? "").trim();
|
|
335
|
+
const collected = [inline];
|
|
336
|
+
if (inline === "") {
|
|
337
|
+
for (const line of lines.slice(start + 1)) {
|
|
338
|
+
const text = stripYamlComment(line);
|
|
339
|
+
if (text.trim() === "") {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if ((/^\s*/u.exec(text)?.[0].length ?? 0) <= indent) {
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
collected.push(text.replace(/^\s*(?:-\s+|labels:\s*)/u, ""));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return collected.filter((value) => !value.includes("${{")).flatMap((value) => value.split(/[\s,[\]'"]+/u)).filter((label) => label !== "");
|
|
349
|
+
}
|
|
350
|
+
function checkWorkflowRunnersPinned(file, text, floatingLabel = DEFAULT_FLOATING_LABEL) {
|
|
351
|
+
const violations = [];
|
|
352
|
+
const lines = text.split("\n");
|
|
353
|
+
lines.forEach((line, index) => {
|
|
354
|
+
const match = RUNS_ON.exec(line);
|
|
355
|
+
if (match === null || line.trimStart().startsWith("#")) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
for (const label of runnerLabels(lines, index, match[1]?.length ?? 0)) {
|
|
359
|
+
if (floatingLabel.test(label)) {
|
|
360
|
+
violations.push({
|
|
361
|
+
file,
|
|
362
|
+
rule: RULE_ID2,
|
|
363
|
+
message: `line ${index + 1}: \`runs-on\` uses the floating runner label "${label}". Pin a named image (for example \`ubuntu-24.04\`) so a runner image change is a reviewed diff.`,
|
|
364
|
+
line: index + 1
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
return violations;
|
|
370
|
+
}
|
|
371
|
+
function createGithubActionsRunnerPinnedRule(options = {}) {
|
|
372
|
+
const workflowGlobs = options.workflowGlobs ?? DEFAULT_WORKFLOW_GLOBS;
|
|
373
|
+
const floatingLabel = options.floatingLabel ?? DEFAULT_FLOATING_LABEL;
|
|
374
|
+
const ciCritical = options.ciCritical ?? true;
|
|
375
|
+
return {
|
|
376
|
+
id: RULE_ID2,
|
|
377
|
+
category: "ci",
|
|
378
|
+
ciCritical,
|
|
379
|
+
description: "GitHub Actions jobs must run on a pinned runner image (for example ubuntu-24.04), never a *-latest label.",
|
|
380
|
+
run(ctx) {
|
|
381
|
+
return globFiles((p) => ctx.glob(p), workflowGlobs).flatMap(
|
|
382
|
+
(file) => checkWorkflowRunnersPinned(file, ctx.read(file) ?? "", floatingLabel)
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/rules/github-actions-sha-pinned.ts
|
|
389
|
+
var RULE_ID3 = "github-actions-sha-pinned";
|
|
390
|
+
var USES_LINE = /^\s*(?:-\s+)?uses:\s*['"]?([^\s'"#]+)['"]?\s*(#.*)?$/u;
|
|
391
|
+
var FULL_SHA = /^[0-9a-f]{40}$/u;
|
|
392
|
+
var DOCKER_DIGEST = /@sha256:[0-9a-f]{64}$/u;
|
|
393
|
+
var VERSION_COMMENT = /^#\s*v\d/u;
|
|
394
|
+
function checkWorkflowActionsPinned(file, text) {
|
|
395
|
+
const violations = [];
|
|
396
|
+
text.split("\n").forEach((line, index) => {
|
|
397
|
+
const match = USES_LINE.exec(line);
|
|
398
|
+
const ref = match?.[1];
|
|
399
|
+
if (ref === void 0 || ref.startsWith("./")) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const where = `line ${index + 1}`;
|
|
403
|
+
const report = (message) => {
|
|
404
|
+
violations.push({ file, rule: RULE_ID3, message, line: index + 1 });
|
|
405
|
+
};
|
|
406
|
+
if (ref.startsWith("docker://")) {
|
|
407
|
+
if (!DOCKER_DIGEST.test(ref)) {
|
|
408
|
+
report(`${where}: \`uses: ${ref}\` is not pinned by digest. Append \`@sha256:<digest>\`.`);
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
const at = ref.lastIndexOf("@");
|
|
413
|
+
const pin = at === -1 ? "" : ref.slice(at + 1);
|
|
414
|
+
if (!FULL_SHA.test(pin)) {
|
|
415
|
+
report(
|
|
416
|
+
`${where}: \`uses: ${ref}\` is not pinned to a 40-character commit SHA. Resolve the tag to its commit and write \`uses: ${at === -1 ? ref : ref.slice(0, at)}@<sha> # vN\`.`
|
|
417
|
+
);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (!VERSION_COMMENT.test(match?.[2] ?? "")) {
|
|
421
|
+
report(
|
|
422
|
+
`${where}: \`uses: ${ref}\` is SHA-pinned but has no \`# vN\` comment. Keep the version beside the SHA so the pin stays readable and Dependabot can bump it.`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
return violations;
|
|
427
|
+
}
|
|
428
|
+
function createGithubActionsShaPinnedRule(options = {}) {
|
|
429
|
+
const workflowGlobs = options.workflowGlobs ?? DEFAULT_WORKFLOW_GLOBS;
|
|
430
|
+
const ciCritical = options.ciCritical ?? true;
|
|
431
|
+
return {
|
|
432
|
+
id: RULE_ID3,
|
|
433
|
+
category: "ci",
|
|
434
|
+
ciCritical,
|
|
435
|
+
description: "GitHub Actions `uses:` refs must be pinned to a 40-character commit SHA with a `# vN` comment (local ./ actions exempt).",
|
|
436
|
+
run(ctx) {
|
|
437
|
+
return globFiles((p) => ctx.glob(p), workflowGlobs).flatMap(
|
|
438
|
+
(file) => checkWorkflowActionsPinned(file, ctx.read(file) ?? "")
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
237
444
|
// src/rules/layer-rank.ts
|
|
238
445
|
var DEFAULT_SOURCE_GLOBS = [
|
|
239
446
|
"packages/*/src/**/*.ts",
|
|
@@ -446,6 +653,228 @@ function createPackageShapeRule(options = {}) {
|
|
|
446
653
|
};
|
|
447
654
|
}
|
|
448
655
|
|
|
656
|
+
// src/rules/security-scanner-version-parity.ts
|
|
657
|
+
var RULE_ID4 = "security-scanner-version-parity";
|
|
658
|
+
function createSecurityScannerVersionParityRule(options = {}) {
|
|
659
|
+
const scanner = options.scanner ?? "gitleaks";
|
|
660
|
+
const versionVariable = options.versionVariable ?? "GITLEAKS_VERSION";
|
|
661
|
+
const imageVariable = options.imageVariable ?? "GITLEAKS_IMAGE";
|
|
662
|
+
const hookFile = options.hookFile ?? "scripts/ci/pre-push.sh";
|
|
663
|
+
const workflowGlobs = options.workflowGlobs ?? DEFAULT_WORKFLOW_GLOBS;
|
|
664
|
+
const ciCritical = options.ciCritical ?? true;
|
|
665
|
+
const name = escapeRegExp(scanner);
|
|
666
|
+
const versionVar = escapeRegExp(versionVariable);
|
|
667
|
+
const workflowVersion = new RegExp(
|
|
668
|
+
`^\\s*${versionVar}:\\s*['"]?(\\d+\\.\\d+\\.\\d+)['"]?\\s*(?:#.*)?$`,
|
|
669
|
+
"mu"
|
|
670
|
+
);
|
|
671
|
+
const hookVersion = new RegExp(
|
|
672
|
+
`^\\s*(?:readonly\\s+)?${versionVar}=['"]?(\\d+\\.\\d+\\.\\d+)['"]?\\s*(?:#.*)?$`,
|
|
673
|
+
"mu"
|
|
674
|
+
);
|
|
675
|
+
const hookImageTag = new RegExp(
|
|
676
|
+
`^\\s*${escapeRegExp(imageVariable)}=['"][^'"\\n]*${name}:v(\\d+\\.\\d+\\.\\d+)['"]`,
|
|
677
|
+
"mu"
|
|
678
|
+
);
|
|
679
|
+
const runtimeCheck = new RegExp(`\\b${name}\\s+version\\b`, "u");
|
|
680
|
+
const mentionsScanner = new RegExp(name, "iu");
|
|
681
|
+
return {
|
|
682
|
+
id: RULE_ID4,
|
|
683
|
+
category: "ci",
|
|
684
|
+
ciCritical,
|
|
685
|
+
description: `The ${scanner} version pinned in the workflows must equal the one in ${hookFile}, and the hook must compare a native ${scanner} against it at run time.`,
|
|
686
|
+
run(ctx) {
|
|
687
|
+
const hookText = ctx.read(hookFile) ?? "";
|
|
688
|
+
const ciVersions = /* @__PURE__ */ new Map();
|
|
689
|
+
for (const file of globFiles((p) => ctx.glob(p), workflowGlobs)) {
|
|
690
|
+
const version = workflowVersion.exec(ctx.read(file) ?? "")?.[1];
|
|
691
|
+
if (version !== void 0) {
|
|
692
|
+
ciVersions.set(version, [...ciVersions.get(version) ?? [], file]);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
const hookUsesScanner = mentionsScanner.test(hookText);
|
|
696
|
+
if (ciVersions.size === 0 && !hookUsesScanner) {
|
|
697
|
+
return [];
|
|
698
|
+
}
|
|
699
|
+
const violations = [];
|
|
700
|
+
const report = (file, message) => {
|
|
701
|
+
violations.push({ file, rule: RULE_ID4, message });
|
|
702
|
+
};
|
|
703
|
+
if (ciVersions.size > 1) {
|
|
704
|
+
report(
|
|
705
|
+
[...ciVersions.values()].flat()[0] ?? hookFile,
|
|
706
|
+
`Workflows pin different ${scanner} versions (${[...ciVersions.keys()].join(", ")}). Use one \`${versionVariable}\` everywhere.`
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
const ciVersion = ciVersions.size === 1 ? [...ciVersions.keys()][0] : void 0;
|
|
710
|
+
if (ciVersions.size === 0) {
|
|
711
|
+
report(
|
|
712
|
+
hookFile,
|
|
713
|
+
`${hookFile} runs ${scanner} but no workflow pins \`${versionVariable}\`, so CI's scanner version is unknown. Pin it in the workflow env.`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
if (!hookUsesScanner) {
|
|
717
|
+
report(
|
|
718
|
+
hookFile,
|
|
719
|
+
`Workflows pin ${scanner} ${ciVersion ?? ""} but ${hookFile} does not run ${scanner}, so the local hook cannot agree with CI.`
|
|
720
|
+
);
|
|
721
|
+
return violations;
|
|
722
|
+
}
|
|
723
|
+
const declared = hookVersion.exec(hookText)?.[1];
|
|
724
|
+
if (declared === void 0) {
|
|
725
|
+
report(
|
|
726
|
+
hookFile,
|
|
727
|
+
`${hookFile} does not declare \`${versionVariable}="x.y.z"\`. Declare the version CI pins (${ciVersion ?? "see the workflow"}) and derive the Docker image tag from it.`
|
|
728
|
+
);
|
|
729
|
+
} else if (ciVersion !== void 0 && declared !== ciVersion) {
|
|
730
|
+
report(
|
|
731
|
+
hookFile,
|
|
732
|
+
`${hookFile} pins ${scanner} ${declared} but CI pins ${ciVersion}. Bump them together.`
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
const imageTag = hookImageTag.exec(hookText)?.[1];
|
|
736
|
+
if (imageTag !== void 0 && ciVersion !== void 0 && imageTag !== ciVersion) {
|
|
737
|
+
report(
|
|
738
|
+
hookFile,
|
|
739
|
+
`${hookFile} \`${imageVariable}\` is tagged v${imageTag} but CI pins ${ciVersion}. Derive the tag from \`${versionVariable}\` or bump it.`
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
if (!runtimeCheck.test(hookText)) {
|
|
743
|
+
report(
|
|
744
|
+
hookFile,
|
|
745
|
+
`${hookFile} never runs \`${scanner} version\`. A native ${scanner} of another version scans with another ruleset: compare it with \`${versionVariable}\` and refuse a mismatch.`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
return violations;
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/rules/service-image-digest-pin.ts
|
|
754
|
+
var RULE_ID5 = "service-image-digest-pin";
|
|
755
|
+
var DIGEST2 = /@sha256:[0-9a-f]{64}$/u;
|
|
756
|
+
var KEY_LINE = /^(\s*)(?:-\s+)?([\w-]+):\s*(.*)$/u;
|
|
757
|
+
var DEFAULT_COMPOSE_GLOBS = anywhereGlobs(
|
|
758
|
+
["compose", "docker-compose"].flatMap(
|
|
759
|
+
(stem) => ["", ".*", "-*"].flatMap((infix) => ["yml", "yaml"].map((ext) => `${stem}${infix}.${ext}`))
|
|
760
|
+
)
|
|
761
|
+
);
|
|
762
|
+
function indentOf(line) {
|
|
763
|
+
return /^\s*/u.exec(line)?.[0].length ?? 0;
|
|
764
|
+
}
|
|
765
|
+
function workflowImages(text) {
|
|
766
|
+
const found = [];
|
|
767
|
+
const stack = [];
|
|
768
|
+
text.split("\n").forEach((line, index) => {
|
|
769
|
+
if (line.trim() === "" || line.trimStart().startsWith("#")) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const match = KEY_LINE.exec(line);
|
|
773
|
+
if (match === null) {
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const indent = match[1]?.length ?? 0;
|
|
777
|
+
const key = match[2] ?? "";
|
|
778
|
+
const value = unquote(stripYamlComment(match[3] ?? "").trim());
|
|
779
|
+
while (stack.length > 0 && (stack[stack.length - 1]?.indent ?? 0) >= indent) {
|
|
780
|
+
stack.pop();
|
|
781
|
+
}
|
|
782
|
+
const insideService = stack.some((entry) => entry.key === "services");
|
|
783
|
+
const isContainerScalar = key === "container" && value !== "";
|
|
784
|
+
const isImageKey = key === "image" && value !== "";
|
|
785
|
+
const insideContainer = stack.some((entry) => entry.key === "container");
|
|
786
|
+
if (isContainerScalar || isImageKey && (insideService || insideContainer)) {
|
|
787
|
+
found.push({ line: index + 1, image: value });
|
|
788
|
+
}
|
|
789
|
+
stack.push({ indent, key });
|
|
790
|
+
});
|
|
791
|
+
return found;
|
|
792
|
+
}
|
|
793
|
+
function composeImages(text) {
|
|
794
|
+
const lines = text.split("\n");
|
|
795
|
+
const found = [];
|
|
796
|
+
const servicesAt = lines.findIndex((line) => /^services:\s*(?:#.*)?$/u.test(line));
|
|
797
|
+
if (servicesAt === -1) {
|
|
798
|
+
return found;
|
|
799
|
+
}
|
|
800
|
+
let serviceIndent = null;
|
|
801
|
+
let propertyIndent = null;
|
|
802
|
+
let current = null;
|
|
803
|
+
const flush = () => {
|
|
804
|
+
if (current !== null && !current.hasBuild) {
|
|
805
|
+
found.push(...current.images);
|
|
806
|
+
}
|
|
807
|
+
current = null;
|
|
808
|
+
};
|
|
809
|
+
for (let index = servicesAt + 1; index < lines.length; index += 1) {
|
|
810
|
+
const line = lines[index] ?? "";
|
|
811
|
+
if (line.trim() === "" || line.trimStart().startsWith("#")) {
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
const indent = indentOf(line);
|
|
815
|
+
if (indent === 0) {
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
serviceIndent ??= indent;
|
|
819
|
+
const match = KEY_LINE.exec(line);
|
|
820
|
+
if (match === null) {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (indent === serviceIndent) {
|
|
824
|
+
flush();
|
|
825
|
+
current = { images: [], hasBuild: false };
|
|
826
|
+
propertyIndent = null;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
propertyIndent ??= indent;
|
|
830
|
+
if (current === null || indent !== propertyIndent) {
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const value = unquote(stripYamlComment(match[3] ?? "").trim());
|
|
834
|
+
if (match[2] === "build") {
|
|
835
|
+
current.hasBuild = true;
|
|
836
|
+
} else if (match[2] === "image" && value !== "") {
|
|
837
|
+
current.images.push({ line: index + 1, image: value });
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
flush();
|
|
841
|
+
return found;
|
|
842
|
+
}
|
|
843
|
+
function violationsFor(file, images, allowUnpinned) {
|
|
844
|
+
return images.filter(({ image }) => !DIGEST2.test(image) && !allowUnpinned.has(image)).map(({ line, image }) => {
|
|
845
|
+
const reference = image.split("@")[0];
|
|
846
|
+
return {
|
|
847
|
+
file,
|
|
848
|
+
rule: RULE_ID5,
|
|
849
|
+
message: `line ${line}: image "${image}" is not pinned by digest. Write \`${reference}@sha256:<digest>\` (resolve it with \`docker buildx imagetools inspect ${reference}\`) so CI and local runs use the same bytes.`,
|
|
850
|
+
line
|
|
851
|
+
};
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
function createServiceImageDigestPinRule(options = {}) {
|
|
855
|
+
const workflowGlobs = options.workflowGlobs ?? DEFAULT_WORKFLOW_GLOBS;
|
|
856
|
+
const composeGlobs = options.composeGlobs ?? DEFAULT_COMPOSE_GLOBS;
|
|
857
|
+
const skipDirs = options.skipDirs ?? DEFAULT_SKIP_DIRS;
|
|
858
|
+
const allowUnpinned = new Set(options.allowUnpinned ?? []);
|
|
859
|
+
const ciCritical = options.ciCritical ?? true;
|
|
860
|
+
return {
|
|
861
|
+
id: RULE_ID5,
|
|
862
|
+
category: "ci",
|
|
863
|
+
ciCritical,
|
|
864
|
+
description: "Workflow service and container images, and docker-compose images, must be pinned by `@sha256:` digest (a service that builds locally is exempt).",
|
|
865
|
+
run(ctx) {
|
|
866
|
+
const glob = (p) => ctx.glob(p);
|
|
867
|
+
const workflowViolations = globFiles(glob, workflowGlobs).flatMap(
|
|
868
|
+
(file) => violationsFor(file, workflowImages(ctx.read(file) ?? ""), allowUnpinned)
|
|
869
|
+
);
|
|
870
|
+
const composeViolations = globFiles(glob, composeGlobs, skipDirs).flatMap(
|
|
871
|
+
(file) => violationsFor(file, composeImages(ctx.read(file) ?? ""), allowUnpinned)
|
|
872
|
+
);
|
|
873
|
+
return [...workflowViolations, ...composeViolations];
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
449
878
|
// src/rules/test-runner-segregation.ts
|
|
450
879
|
function createTestRunnerSegregationRule(options = {}) {
|
|
451
880
|
const bunPackageGlobs = options.bunPackageGlobs ?? ["packages/*/package.json"];
|
|
@@ -712,11 +1141,16 @@ function createWorkspaceGraphParityRule(options = {}) {
|
|
|
712
1141
|
var RULE_FACTORIES = {
|
|
713
1142
|
"agents-doc-presence": createAgentsDocPresenceRule,
|
|
714
1143
|
"canonical-helpers-single-home": createCanonicalHelpersSingleHomeRule,
|
|
1144
|
+
"dockerfile-base-image-digest-pin": createDockerfileBaseImageDigestPinRule,
|
|
715
1145
|
"file-size-ratchet": createFileSizeRatchetRule,
|
|
1146
|
+
"github-actions-runner-pinned": createGithubActionsRunnerPinnedRule,
|
|
1147
|
+
"github-actions-sha-pinned": createGithubActionsShaPinnedRule,
|
|
716
1148
|
"layer-rank": createLayerRankRule,
|
|
717
1149
|
"no-cloned-component-folders": createNoClonedComponentFoldersRule,
|
|
718
1150
|
"no-warn-severity": createNoWarnSeverityRule,
|
|
719
1151
|
"package-shape": createPackageShapeRule,
|
|
1152
|
+
"security-scanner-version-parity": createSecurityScannerVersionParityRule,
|
|
1153
|
+
"service-image-digest-pin": createServiceImageDigestPinRule,
|
|
720
1154
|
"test-runner-segregation": createTestRunnerSegregationRule,
|
|
721
1155
|
"test-sibling-enforcement": createTestSiblingEnforcementRule,
|
|
722
1156
|
"test-workspace-enrollment": createTestWorkspaceEnrollmentRule,
|
|
@@ -735,11 +1169,16 @@ function createAllRules() {
|
|
|
735
1169
|
createAgentsDocPresenceRule,
|
|
736
1170
|
createAllRules,
|
|
737
1171
|
createCanonicalHelpersSingleHomeRule,
|
|
1172
|
+
createDockerfileBaseImageDigestPinRule,
|
|
738
1173
|
createFileSizeRatchetRule,
|
|
1174
|
+
createGithubActionsRunnerPinnedRule,
|
|
1175
|
+
createGithubActionsShaPinnedRule,
|
|
739
1176
|
createLayerRankRule,
|
|
740
1177
|
createNoClonedComponentFoldersRule,
|
|
741
1178
|
createNoWarnSeverityRule,
|
|
742
1179
|
createPackageShapeRule,
|
|
1180
|
+
createSecurityScannerVersionParityRule,
|
|
1181
|
+
createServiceImageDigestPinRule,
|
|
743
1182
|
createTestRunnerSegregationRule,
|
|
744
1183
|
createTestSiblingEnforcementRule,
|
|
745
1184
|
createTestWorkspaceEnrollmentRule,
|
package/dist/index.d.cts
CHANGED
|
@@ -52,6 +52,26 @@ interface CanonicalHelpersSingleHomeOptions {
|
|
|
52
52
|
*/
|
|
53
53
|
declare function createCanonicalHelpersSingleHomeRule(options?: CanonicalHelpersSingleHomeOptions): IMetaRule;
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Options for {@link createDockerfileBaseImageDigestPinRule}.
|
|
57
|
+
*
|
|
58
|
+
* Ported from a private monorepo that walked the whole tree with a hardcoded Dockerfile
|
|
59
|
+
* name regex and skip list; both are now options.
|
|
60
|
+
*/
|
|
61
|
+
interface DockerfileBaseImageDigestPinOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Globs that find Dockerfiles. Default `Dockerfile`, `Dockerfile.*` and
|
|
64
|
+
* `*.Dockerfile` at any depth, dot-directories included.
|
|
65
|
+
*/
|
|
66
|
+
readonly dockerfileGlobs?: readonly string[];
|
|
67
|
+
/** A path with any of these segments is skipped. Default `node_modules`, `.git`, `dist`, `.turbo`, `coverage`. */
|
|
68
|
+
readonly skipDirs?: readonly string[];
|
|
69
|
+
/** Whether a violation fails CI. Default `true`. */
|
|
70
|
+
readonly ciCritical?: boolean;
|
|
71
|
+
}
|
|
72
|
+
/** Every Dockerfile FROM must pin its base image by digest. */
|
|
73
|
+
declare function createDockerfileBaseImageDigestPinRule(options?: DockerfileBaseImageDigestPinOptions): IMetaRule;
|
|
74
|
+
|
|
55
75
|
/**
|
|
56
76
|
* Options for {@link createFileSizeRatchetRule}.
|
|
57
77
|
*
|
|
@@ -96,6 +116,38 @@ interface FileSizeRatchetOptions {
|
|
|
96
116
|
*/
|
|
97
117
|
declare function createFileSizeRatchetRule(options?: FileSizeRatchetOptions): IMetaRule;
|
|
98
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Options for {@link createGithubActionsRunnerPinnedRule}.
|
|
121
|
+
*
|
|
122
|
+
* Ported from a private monorepo that hardcoded the `*-latest` label shape and read a
|
|
123
|
+
* pre-collected `workflowFiles` list; both are now options.
|
|
124
|
+
*/
|
|
125
|
+
interface GithubActionsRunnerPinnedOptions {
|
|
126
|
+
/** Globs of the workflow files to scan. Default `.github/workflows/*.y(a)ml`. */
|
|
127
|
+
readonly workflowGlobs?: readonly string[];
|
|
128
|
+
/** A runner label matching this is floating. Default `/^[\w.-]+-latest$/u`. */
|
|
129
|
+
readonly floatingLabel?: RegExp;
|
|
130
|
+
/** Whether a violation fails CI. Default `true`. */
|
|
131
|
+
readonly ciCritical?: boolean;
|
|
132
|
+
}
|
|
133
|
+
/** No workflow job may run on a floating (`*-latest`) runner label. */
|
|
134
|
+
declare function createGithubActionsRunnerPinnedRule(options?: GithubActionsRunnerPinnedOptions): IMetaRule;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Options for {@link createGithubActionsShaPinnedRule}.
|
|
138
|
+
*
|
|
139
|
+
* Ported from a private monorepo whose runner handed the rule a pre-collected
|
|
140
|
+
* `workflowFiles` list; that list is now the `workflowGlobs` option.
|
|
141
|
+
*/
|
|
142
|
+
interface GithubActionsShaPinnedOptions {
|
|
143
|
+
/** Globs of the workflow files to scan. Default `.github/workflows/*.y(a)ml`. */
|
|
144
|
+
readonly workflowGlobs?: readonly string[];
|
|
145
|
+
/** Whether a violation fails CI. Default `true`. */
|
|
146
|
+
readonly ciCritical?: boolean;
|
|
147
|
+
}
|
|
148
|
+
/** Every workflow `uses:` ref must be a full commit SHA with a `# vN` comment. */
|
|
149
|
+
declare function createGithubActionsShaPinnedRule(options?: GithubActionsShaPinnedOptions): IMetaRule;
|
|
150
|
+
|
|
99
151
|
/**
|
|
100
152
|
* Options for {@link createLayerRankRule}.
|
|
101
153
|
*
|
|
@@ -219,6 +271,75 @@ interface PackageShapeOptions {
|
|
|
219
271
|
*/
|
|
220
272
|
declare function createPackageShapeRule(options?: PackageShapeOptions): IMetaRule;
|
|
221
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Options for {@link createSecurityScannerVersionParityRule}.
|
|
276
|
+
*
|
|
277
|
+
* Ported from a private monorepo that hardcoded gitleaks, its `GITLEAKS_VERSION` /
|
|
278
|
+
* `GITLEAKS_IMAGE` variable names, the hook path `scripts/ci/pre-push.sh` and a
|
|
279
|
+
* pre-collected `workflowFiles` list. Each is now an option whose default is the
|
|
280
|
+
* original value, so the rule can pin any scanner that prints `<scanner> version`.
|
|
281
|
+
*/
|
|
282
|
+
interface SecurityScannerVersionParityOptions {
|
|
283
|
+
/** The scanner's binary name, as it appears in the hook and in its image name. Default `gitleaks`. */
|
|
284
|
+
readonly scanner?: string;
|
|
285
|
+
/** The env / shell variable both sides pin the version in. Default `GITLEAKS_VERSION`. */
|
|
286
|
+
readonly versionVariable?: string;
|
|
287
|
+
/** The hook's optional image variable, whose `<scanner>:vX.Y.Z` tag must agree. Default `GITLEAKS_IMAGE`. */
|
|
288
|
+
readonly imageVariable?: string;
|
|
289
|
+
/** The local hook script, repo-relative. Default `scripts/ci/pre-push.sh`. */
|
|
290
|
+
readonly hookFile?: string;
|
|
291
|
+
/** Globs of the workflow files to scan. Default `.github/workflows/*.y(a)ml`. */
|
|
292
|
+
readonly workflowGlobs?: readonly string[];
|
|
293
|
+
/** Whether a violation fails CI. Default `true`. */
|
|
294
|
+
readonly ciCritical?: boolean;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Different scanner versions ship different rulesets, so a push can pass the
|
|
298
|
+
* local hook and fail CI (or the reverse) with no code difference. This ties
|
|
299
|
+
* three things together:
|
|
300
|
+
*
|
|
301
|
+
* - every workflow that pins the version variable pins the same one,
|
|
302
|
+
* - the hook declares the same variable equal to it (and a literal image tag,
|
|
303
|
+
* if it has one, agrees too),
|
|
304
|
+
* - the hook actually runs `<scanner> version`, so a drifted native install is
|
|
305
|
+
* caught at run time.
|
|
306
|
+
*
|
|
307
|
+
* Dormant when neither side mentions the scanner.
|
|
308
|
+
*/
|
|
309
|
+
declare function createSecurityScannerVersionParityRule(options?: SecurityScannerVersionParityOptions): IMetaRule;
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Options for {@link createServiceImageDigestPinRule}.
|
|
313
|
+
*
|
|
314
|
+
* Ported from a private monorepo that hardcoded a one-entry exemption list (an
|
|
315
|
+
* OpenTelemetry collector tag its registry does not serve), a compose file name
|
|
316
|
+
* regex, a skip list and a pre-collected `workflowFiles` list. All four are now
|
|
317
|
+
* options; the exemption list defaults to empty.
|
|
318
|
+
*/
|
|
319
|
+
interface ServiceImageDigestPinOptions {
|
|
320
|
+
/** Globs of the workflow files to scan. Default `.github/workflows/*.y(a)ml`. */
|
|
321
|
+
readonly workflowGlobs?: readonly string[];
|
|
322
|
+
/** Globs that find compose files. Default every `compose` / `docker-compose` `.yml` / `.yaml` variant at any depth, dot-directories included. */
|
|
323
|
+
readonly composeGlobs?: readonly string[];
|
|
324
|
+
/** A compose path with any of these segments is skipped. Default `node_modules`, `.git`, `dist`, `.turbo`, `coverage`. */
|
|
325
|
+
readonly skipDirs?: readonly string[];
|
|
326
|
+
/**
|
|
327
|
+
* Exact image refs that are allowed unpinned, for a ref the registry cannot
|
|
328
|
+
* serve a digest for. Each entry should say why in the consumer's config.
|
|
329
|
+
* Default none.
|
|
330
|
+
*/
|
|
331
|
+
readonly allowUnpinned?: readonly string[];
|
|
332
|
+
/** Whether a violation fails CI. Default `true`. */
|
|
333
|
+
readonly ciCritical?: boolean;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Every workflow service/container image and every compose image must be pinned
|
|
337
|
+
* by digest. A tag (`postgres:17-alpine`) or `latest` names whatever the registry
|
|
338
|
+
* serves today, so a green build can change under an unchanged commit.
|
|
339
|
+
* `tag@sha256:<digest>` keeps the readable tag and fixes the content.
|
|
340
|
+
*/
|
|
341
|
+
declare function createServiceImageDigestPinRule(options?: ServiceImageDigestPinOptions): IMetaRule;
|
|
342
|
+
|
|
222
343
|
/**
|
|
223
344
|
* Options for {@link createTestRunnerSegregationRule}.
|
|
224
345
|
*
|
|
@@ -376,11 +497,16 @@ declare function countLines(text: string): number;
|
|
|
376
497
|
declare const RULE_FACTORIES: {
|
|
377
498
|
readonly 'agents-doc-presence': typeof createAgentsDocPresenceRule;
|
|
378
499
|
readonly 'canonical-helpers-single-home': typeof createCanonicalHelpersSingleHomeRule;
|
|
500
|
+
readonly 'dockerfile-base-image-digest-pin': typeof createDockerfileBaseImageDigestPinRule;
|
|
379
501
|
readonly 'file-size-ratchet': typeof createFileSizeRatchetRule;
|
|
502
|
+
readonly 'github-actions-runner-pinned': typeof createGithubActionsRunnerPinnedRule;
|
|
503
|
+
readonly 'github-actions-sha-pinned': typeof createGithubActionsShaPinnedRule;
|
|
380
504
|
readonly 'layer-rank': typeof createLayerRankRule;
|
|
381
505
|
readonly 'no-cloned-component-folders': typeof createNoClonedComponentFoldersRule;
|
|
382
506
|
readonly 'no-warn-severity': typeof createNoWarnSeverityRule;
|
|
383
507
|
readonly 'package-shape': typeof createPackageShapeRule;
|
|
508
|
+
readonly 'security-scanner-version-parity': typeof createSecurityScannerVersionParityRule;
|
|
509
|
+
readonly 'service-image-digest-pin': typeof createServiceImageDigestPinRule;
|
|
384
510
|
readonly 'test-runner-segregation': typeof createTestRunnerSegregationRule;
|
|
385
511
|
readonly 'test-sibling-enforcement': typeof createTestSiblingEnforcementRule;
|
|
386
512
|
readonly 'test-workspace-enrollment': typeof createTestWorkspaceEnrollmentRule;
|
|
@@ -392,4 +518,4 @@ declare const RULE_IDS: (keyof typeof RULE_FACTORIES)[];
|
|
|
392
518
|
/** Instantiate every catalog rule with its default options. */
|
|
393
519
|
declare function createAllRules(): IMetaRule[];
|
|
394
520
|
|
|
395
|
-
export { type AgentsDocPresenceOptions, type CanonicalHelpersSingleHomeOptions, type FileSizeRatchetOptions, type LayerRankOptions, type NoClonedComponentFoldersOptions, type NoWarnSeverityOptions, type PackageShapeOptions, RULE_FACTORIES, RULE_IDS, type TestRunnerSegregationOptions, type TestSiblingEnforcementOptions, type TestWorkspaceEnrollmentOptions, type UiPrimitiveShapeOptions, type WorkspaceGraphParityOptions, countLines, createAgentsDocPresenceRule, createAllRules, createCanonicalHelpersSingleHomeRule, createFileSizeRatchetRule, createLayerRankRule, createNoClonedComponentFoldersRule, createNoWarnSeverityRule, createPackageShapeRule, createTestRunnerSegregationRule, createTestSiblingEnforcementRule, createTestWorkspaceEnrollmentRule, createUiPrimitiveShapeRule, createWorkspaceGraphParityRule };
|
|
521
|
+
export { type AgentsDocPresenceOptions, type CanonicalHelpersSingleHomeOptions, type DockerfileBaseImageDigestPinOptions, type FileSizeRatchetOptions, type GithubActionsRunnerPinnedOptions, type GithubActionsShaPinnedOptions, type LayerRankOptions, type NoClonedComponentFoldersOptions, type NoWarnSeverityOptions, type PackageShapeOptions, RULE_FACTORIES, RULE_IDS, type SecurityScannerVersionParityOptions, type ServiceImageDigestPinOptions, type TestRunnerSegregationOptions, type TestSiblingEnforcementOptions, type TestWorkspaceEnrollmentOptions, type UiPrimitiveShapeOptions, type WorkspaceGraphParityOptions, countLines, createAgentsDocPresenceRule, createAllRules, createCanonicalHelpersSingleHomeRule, createDockerfileBaseImageDigestPinRule, createFileSizeRatchetRule, createGithubActionsRunnerPinnedRule, createGithubActionsShaPinnedRule, createLayerRankRule, createNoClonedComponentFoldersRule, createNoWarnSeverityRule, createPackageShapeRule, createSecurityScannerVersionParityRule, createServiceImageDigestPinRule, createTestRunnerSegregationRule, createTestSiblingEnforcementRule, createTestWorkspaceEnrollmentRule, createUiPrimitiveShapeRule, createWorkspaceGraphParityRule };
|