@noctcore/lint-meta-rules 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/dist/{chunk-Z7TXSZR4.js → chunk-OYFQKSJN.js} +9 -1
- package/dist/i18n.js +1 -1
- package/dist/index.cjs +305 -14
- package/dist/index.d.cts +43 -1
- package/dist/index.d.ts +43 -1
- package/dist/index.js +304 -15
- package/dist/prisma.js +3 -3
- package/dist/session.cjs +297 -0
- package/dist/session.d.cts +226 -0
- package/dist/session.d.ts +226 -0
- package/dist/session.js +244 -0
- package/dist/trpc.cjs +122 -0
- package/dist/trpc.d.cts +59 -0
- package/dist/trpc.d.ts +59 -0
- package/dist/trpc.js +73 -0
- package/docs/rules/github-actions-least-privilege-permissions.md +71 -0
- package/docs/rules/github-actions-no-template-injection.md +93 -0
- package/docs/rules/idempotency-key-parity.md +83 -0
- package/docs/rules/session-epoch-captured.md +83 -0
- package/docs/rules/session-kind-stamped.md +81 -0
- package/docs/rules/session-landing-declared.md +87 -0
- package/docs/rules/session-mint-callers.md +77 -0
- package/package.json +17 -7
package/README.md
CHANGED
|
@@ -45,7 +45,8 @@ over the whole catalog.
|
|
|
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
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,
|
|
48
|
+
`ci`) are ported from a production NestJS + Vite monorepo, one factory each, and two GitHub Actions
|
|
49
|
+
security rules (also `ci`) are written for this catalog, for 19 factories in all.
|
|
49
50
|
|
|
50
51
|
| Factory | Source rule(s) | Category | What it enforces |
|
|
51
52
|
| --- | --- | --- | --- |
|
|
@@ -63,6 +64,8 @@ over the whole catalog.
|
|
|
63
64
|
| [`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
65
|
| [`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
66
|
| [`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. |
|
|
67
|
+
| [`createGithubActionsNoTemplateInjectionRule`](./docs/rules/github-actions-no-template-injection.md) | `github-actions-no-template-injection` | ci | `run:` and github-script bodies never expand attacker-controllable `${{ }}` context (issue/PR titles, comments, branch names). |
|
|
68
|
+
| [`createGithubActionsLeastPrivilegePermissionsRule`](./docs/rules/github-actions-least-privilege-permissions.md) | `github-actions-least-privilege-permissions` | ci | A workflow's top-level `permissions:` exists, is not `write-all`/`read-all`, and grants no write. |
|
|
66
69
|
| [`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
70
|
| [`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
71
|
| [`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. |
|
|
@@ -103,3 +106,27 @@ needs the optional peer `eslint`. Neither is in `RULE_FACTORIES`: both need the
|
|
|
103
106
|
| --- | --- | --- |
|
|
104
107
|
| [`createTenantModelRegistryParityRule`](./docs/rules/tenant-model-registry-parity.md) | config | Every tenant-bearing schema model is scoped at runtime or exempt with a reason, and the tenant lint rules resolve with exactly that registry. |
|
|
105
108
|
| [`createPrismaMethodSurfaceRule`](./docs/rules/prisma-method-surface.md) | config | The reads and writes the rules police are exactly the generated client's delegate methods, so a Prisma upgrade cannot add an unguarded one. |
|
|
109
|
+
|
|
110
|
+
### `@noctcore/lint-meta-rules/session`
|
|
111
|
+
|
|
112
|
+
Fences around the one seam that turns an authenticated principal into a session. Each rule is inert
|
|
113
|
+
until you name that seam: the method that mints, the gate in front of it, the files allowed to call it,
|
|
114
|
+
the landings a sign-in can end in. None is in `RULE_FACTORIES`. They load nothing beyond the harness
|
|
115
|
+
contract. Ported from a production NestJS app, where each one exists because its bug shipped once.
|
|
116
|
+
|
|
117
|
+
| Factory | Category | What it enforces |
|
|
118
|
+
| --- | --- | --- |
|
|
119
|
+
| [`createSessionMintCallersRule`](./docs/rules/session-mint-callers.md) | source-text | The method that mints a session is called only from allowlisted files, so a new sign-in entry point cannot skip the gate in front of it. |
|
|
120
|
+
| [`createSessionKindStampedRule`](./docs/rules/session-kind-stamped.md) | source-text | Every mint stamps the principal kind onto the session, or sits in an allowlisted, provably single-kind flow. |
|
|
121
|
+
| [`createSessionEpochCapturedRule`](./docs/rules/session-epoch-captured.md) | source-text | Every call into the sign-in seam passes the revocation epoch captured before the credential was read. |
|
|
122
|
+
| [`createSessionLandingDeclaredRule`](./docs/rules/session-landing-declared.md) | source-text | Every file that opens a door into a session declares its landing, and a door whose landing demands a return shape has it. |
|
|
123
|
+
|
|
124
|
+
### `@noctcore/lint-meta-rules/trpc`
|
|
125
|
+
|
|
126
|
+
Cross-tree checks between tRPC routers and the clients that call them. The decorator shape defaults to
|
|
127
|
+
`nestjs-trpc`'s; the rule is inert until you name the middleware and the two trees. Not in
|
|
128
|
+
`RULE_FACTORIES`.
|
|
129
|
+
|
|
130
|
+
| Factory | Category | What it enforces |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| [`createIdempotencyKeyParityRule`](./docs/rules/idempotency-key-parity.md) | source-text | A procedure guarded by an idempotency middleware has a client caller that sends the key, or no client caller at all. |
|
|
@@ -47,6 +47,13 @@ function unquote(value) {
|
|
|
47
47
|
function anywhereGlobs(baseNames) {
|
|
48
48
|
return baseNames.flatMap((name) => [`**/${name}`, `**/.*/**/${name}`]);
|
|
49
49
|
}
|
|
50
|
+
function readSourceText(read, rel) {
|
|
51
|
+
try {
|
|
52
|
+
return read(rel);
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
50
57
|
|
|
51
58
|
export {
|
|
52
59
|
dirOf,
|
|
@@ -59,5 +66,6 @@ export {
|
|
|
59
66
|
globFiles,
|
|
60
67
|
stripYamlComment,
|
|
61
68
|
unquote,
|
|
62
|
-
anywhereGlobs
|
|
69
|
+
anywhereGlobs,
|
|
70
|
+
readSourceText
|
|
63
71
|
};
|
package/dist/i18n.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -28,6 +28,8 @@ __export(index_exports, {
|
|
|
28
28
|
createCanonicalHelpersSingleHomeRule: () => createCanonicalHelpersSingleHomeRule,
|
|
29
29
|
createDockerfileBaseImageDigestPinRule: () => createDockerfileBaseImageDigestPinRule,
|
|
30
30
|
createFileSizeRatchetRule: () => createFileSizeRatchetRule,
|
|
31
|
+
createGithubActionsLeastPrivilegePermissionsRule: () => createGithubActionsLeastPrivilegePermissionsRule,
|
|
32
|
+
createGithubActionsNoTemplateInjectionRule: () => createGithubActionsNoTemplateInjectionRule,
|
|
31
33
|
createGithubActionsRunnerPinnedRule: () => createGithubActionsRunnerPinnedRule,
|
|
32
34
|
createGithubActionsShaPinnedRule: () => createGithubActionsShaPinnedRule,
|
|
33
35
|
createLayerRankRule: () => createLayerRankRule,
|
|
@@ -326,8 +328,293 @@ function createFileSizeRatchetRule(options = {}) {
|
|
|
326
328
|
};
|
|
327
329
|
}
|
|
328
330
|
|
|
331
|
+
// src/rules/github-actions-least-privilege-permissions.ts
|
|
332
|
+
var RULE_ID2 = "github-actions-least-privilege-permissions";
|
|
333
|
+
var TOP_KEY = /^([\w-]+):\s*(.*)$/u;
|
|
334
|
+
var SCOPE_ENTRY = /^\s*['"]?([\w-]+)['"]?:\s*['"]?([\w-]+)['"]?\s*$/u;
|
|
335
|
+
function indentOf(line) {
|
|
336
|
+
return /^\s*/u.exec(line)?.[0].length ?? 0;
|
|
337
|
+
}
|
|
338
|
+
function isContent(line) {
|
|
339
|
+
const trimmed = line.trim();
|
|
340
|
+
return trimmed !== "" && !trimmed.startsWith("#");
|
|
341
|
+
}
|
|
342
|
+
function topLevelKeys(lines) {
|
|
343
|
+
const keys = [];
|
|
344
|
+
lines.forEach((line, index) => {
|
|
345
|
+
const match = TOP_KEY.exec(line);
|
|
346
|
+
if (match === null) return;
|
|
347
|
+
keys.push({ key: unquote(match[1] ?? ""), value: stripYamlComment(match[2] ?? "").trim(), index });
|
|
348
|
+
});
|
|
349
|
+
return keys;
|
|
350
|
+
}
|
|
351
|
+
function blockOf(lines, index) {
|
|
352
|
+
const block = [];
|
|
353
|
+
for (let j = index + 1; j < lines.length; j += 1) {
|
|
354
|
+
const line = lines[j] ?? "";
|
|
355
|
+
if (!isContent(line)) continue;
|
|
356
|
+
if (indentOf(line) === 0) break;
|
|
357
|
+
block.push({ text: stripYamlComment(line), index: j });
|
|
358
|
+
}
|
|
359
|
+
return block;
|
|
360
|
+
}
|
|
361
|
+
function scopeEntries(lines, permissions) {
|
|
362
|
+
if (permissions.value.startsWith("{")) {
|
|
363
|
+
return permissions.value.replace(/^\{|\}$/gu, "").split(",").map((pair) => SCOPE_ENTRY.exec(pair)).filter((match) => match !== null).map((match) => ({ scope: match[1] ?? "", level: match[2] ?? "", line: permissions.index + 1 }));
|
|
364
|
+
}
|
|
365
|
+
return blockOf(lines, permissions.index).flatMap(({ text, index }) => {
|
|
366
|
+
const match = SCOPE_ENTRY.exec(text);
|
|
367
|
+
return match === null ? [] : [{ scope: match[1] ?? "", level: match[2] ?? "", line: index + 1 }];
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
function jobsWithoutPermissions(lines, jobs) {
|
|
371
|
+
const block = blockOf(lines, jobs.index);
|
|
372
|
+
const jobColumn = indentOf(block[0]?.text ?? "");
|
|
373
|
+
const missing = [];
|
|
374
|
+
let current;
|
|
375
|
+
let keyColumn = -1;
|
|
376
|
+
const settle = () => {
|
|
377
|
+
if (current !== void 0 && !current.hasPermissions) missing.push(current.name);
|
|
378
|
+
};
|
|
379
|
+
for (const { text } of block) {
|
|
380
|
+
const column = indentOf(text);
|
|
381
|
+
if (column === jobColumn) {
|
|
382
|
+
settle();
|
|
383
|
+
current = { name: unquote(text.trim().replace(/:.*$/u, "")), hasPermissions: false };
|
|
384
|
+
keyColumn = -1;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (current === void 0 || column < jobColumn) continue;
|
|
388
|
+
if (keyColumn === -1) keyColumn = column;
|
|
389
|
+
if (column === keyColumn && /^\s*permissions:/u.test(text)) current.hasPermissions = true;
|
|
390
|
+
}
|
|
391
|
+
settle();
|
|
392
|
+
return missing;
|
|
393
|
+
}
|
|
394
|
+
function checkWorkflowPermissions(file, text, allowTopLevelWrite = []) {
|
|
395
|
+
const lines = text.split("\n");
|
|
396
|
+
const keys = topLevelKeys(lines);
|
|
397
|
+
const permissions = keys.find((entry) => entry.key === "permissions");
|
|
398
|
+
const report = (line, message) => ({
|
|
399
|
+
file,
|
|
400
|
+
rule: RULE_ID2,
|
|
401
|
+
message: `line ${line}: ${message}`,
|
|
402
|
+
line
|
|
403
|
+
});
|
|
404
|
+
if (permissions === void 0) {
|
|
405
|
+
const jobs = keys.find((entry) => entry.key === "jobs");
|
|
406
|
+
const missing = jobs === void 0 ? [] : jobsWithoutPermissions(lines, jobs);
|
|
407
|
+
if (missing.length === 0) return [];
|
|
408
|
+
return [
|
|
409
|
+
report(
|
|
410
|
+
1,
|
|
411
|
+
`no top-level \`permissions:\`, so ${missing.map((job) => `\`${job}\``).join(", ")} ${missing.length === 1 ? "runs" : "run"} with the repository's default token, which can be read-write. Add \`permissions: { contents: read }\` at the top and grant writes on the job that needs them.`
|
|
412
|
+
)
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
const shorthand = unquote(permissions.value);
|
|
416
|
+
if (shorthand === "write-all" || shorthand === "read-all") {
|
|
417
|
+
return [
|
|
418
|
+
report(
|
|
419
|
+
permissions.index + 1,
|
|
420
|
+
`top-level \`permissions: ${shorthand}\` grants every scope to every job. List only the scopes the workflow reads (for example \`contents: read\`) and grant writes on the job that needs them.`
|
|
421
|
+
)
|
|
422
|
+
];
|
|
423
|
+
}
|
|
424
|
+
const allowed = new Set(allowTopLevelWrite);
|
|
425
|
+
return scopeEntries(lines, permissions).filter(({ scope, level }) => level === "write" && !allowed.has(scope)).map(
|
|
426
|
+
({ scope, line }) => report(
|
|
427
|
+
line,
|
|
428
|
+
`top-level \`permissions\` grants \`${scope}: write\` to every job. Move it to the job that needs it and keep the top level read-only.`
|
|
429
|
+
)
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
function createGithubActionsLeastPrivilegePermissionsRule(options = {}) {
|
|
433
|
+
const workflowGlobs = options.workflowGlobs ?? DEFAULT_WORKFLOW_GLOBS;
|
|
434
|
+
const allowTopLevelWrite = options.allowTopLevelWrite ?? [];
|
|
435
|
+
const ciCritical = options.ciCritical ?? true;
|
|
436
|
+
return {
|
|
437
|
+
id: RULE_ID2,
|
|
438
|
+
category: "ci",
|
|
439
|
+
ciCritical,
|
|
440
|
+
description: "GitHub Actions workflows declare a read-only top-level `permissions:` (no `write-all`/`read-all`, no `<scope>: write`); writes go on the job that needs them.",
|
|
441
|
+
run(ctx) {
|
|
442
|
+
return globFiles((p) => ctx.glob(p), workflowGlobs).flatMap(
|
|
443
|
+
(file) => checkWorkflowPermissions(file, ctx.read(file) ?? "", allowTopLevelWrite)
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/rules/github-actions-no-template-injection.ts
|
|
450
|
+
var RULE_ID3 = "github-actions-no-template-injection";
|
|
451
|
+
var DEFAULT_ACTION_GLOBS = anywhereGlobs(["action.yml", "action.yaml"]);
|
|
452
|
+
var EVENT = String.raw`(?<![\w.])github\.event\.`;
|
|
453
|
+
var ANY_ITEM = String.raw`(?:\.\*|\[\d+\])`;
|
|
454
|
+
var TAINTED_CONTEXTS = [
|
|
455
|
+
new RegExp(String.raw`${EVENT}(?:issue|pull_request|discussion)\.(?:title|body)\b`, "u"),
|
|
456
|
+
new RegExp(String.raw`${EVENT}pull_request\.head\.(?:ref|label)\b`, "u"),
|
|
457
|
+
new RegExp(String.raw`${EVENT}(?:comment|review|review_comment)\.body\b`, "u"),
|
|
458
|
+
new RegExp(String.raw`${EVENT}pages${ANY_ITEM}\.page_name\b`, "u"),
|
|
459
|
+
new RegExp(String.raw`${EVENT}commits${ANY_ITEM}\.(?:message|author|committer)\b`, "u"),
|
|
460
|
+
new RegExp(String.raw`${EVENT}head_commit\.(?:message|author|committer)\b`, "u"),
|
|
461
|
+
new RegExp(String.raw`${EVENT}workflow_run\.head_branch\b`, "u"),
|
|
462
|
+
new RegExp(String.raw`${EVENT}workflow_run\.head_commit\.(?:message|author|committer)\b`, "u"),
|
|
463
|
+
/(?<![\w.])github\.head_ref\b/u
|
|
464
|
+
];
|
|
465
|
+
var INPUT_CONTEXT = /(?<![\w.])(?:github\.event\.)?inputs\.([\w-]+)/gu;
|
|
466
|
+
var STEP_OUTPUT_CONTEXT = /(?<![\w.])steps\.[\w-]+\.outputs\.[\w-]+/u;
|
|
467
|
+
var SAFE_INPUT_TYPES = /* @__PURE__ */ new Set(["boolean", "number", "choice"]);
|
|
468
|
+
var EXPRESSION = /\$\{\{(.*?)\}\}/gu;
|
|
469
|
+
var SCRIPT_KEY = /^(\s*)(-\s+)?(run|script):(?:\s+(.*))?$/u;
|
|
470
|
+
var BLOCK_INDICATOR = /^[|>][-+0-9]*\s*(?:#.*)?$/u;
|
|
471
|
+
var MAPPING_LINE = /^\s*[\w-]+:(?:\s|$)/u;
|
|
472
|
+
var GITHUB_SCRIPT_USES = /^\s*(?:-\s+)?uses:\s*['"]?actions\/github-script@/u;
|
|
473
|
+
function indentOf2(line) {
|
|
474
|
+
return /^\s*/u.exec(line)?.[0].length ?? 0;
|
|
475
|
+
}
|
|
476
|
+
function isContent2(line) {
|
|
477
|
+
const trimmed = line.trim();
|
|
478
|
+
return trimmed !== "" && !trimmed.startsWith("#");
|
|
479
|
+
}
|
|
480
|
+
function parentOf(lines, index, column) {
|
|
481
|
+
for (let j = index - 1; j >= 0; j -= 1) {
|
|
482
|
+
const line = lines[j] ?? "";
|
|
483
|
+
if (isContent2(line) && indentOf2(line) < column) return j;
|
|
484
|
+
}
|
|
485
|
+
return -1;
|
|
486
|
+
}
|
|
487
|
+
function isGithubScriptStep(lines, withIndex) {
|
|
488
|
+
const stepStart = parentOf(lines, withIndex, indentOf2(lines[withIndex] ?? ""));
|
|
489
|
+
const stepLine = lines[stepStart] ?? "";
|
|
490
|
+
if (!stepLine.trimStart().startsWith("-")) return false;
|
|
491
|
+
const dashColumn = indentOf2(stepLine);
|
|
492
|
+
for (let j = stepStart; j < lines.length; j += 1) {
|
|
493
|
+
const line = lines[j] ?? "";
|
|
494
|
+
if (j > stepStart && isContent2(line) && indentOf2(line) <= dashColumn) break;
|
|
495
|
+
if (GITHUB_SCRIPT_USES.test(line)) return true;
|
|
496
|
+
}
|
|
497
|
+
return false;
|
|
498
|
+
}
|
|
499
|
+
function safeInputNames(lines) {
|
|
500
|
+
const safe = /* @__PURE__ */ new Set();
|
|
501
|
+
lines.forEach((line, index) => {
|
|
502
|
+
if (!/^\s*inputs:\s*(?:#.*)?$/u.test(line)) return;
|
|
503
|
+
const column = indentOf2(line);
|
|
504
|
+
let nameColumn = -1;
|
|
505
|
+
let name = "";
|
|
506
|
+
for (const child of lines.slice(index + 1)) {
|
|
507
|
+
if (!isContent2(child)) continue;
|
|
508
|
+
const childColumn = indentOf2(child);
|
|
509
|
+
if (childColumn <= column) break;
|
|
510
|
+
if (nameColumn === -1) nameColumn = childColumn;
|
|
511
|
+
if (childColumn === nameColumn) {
|
|
512
|
+
name = /^\s*['"]?([\w-]+)['"]?:/u.exec(child)?.[1] ?? "";
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const type = /^\s*type:\s*['"]?(\w+)/u.exec(stripYamlComment(child))?.[1];
|
|
516
|
+
if (type !== void 0 && SAFE_INPUT_TYPES.has(type) && name !== "") safe.add(name);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
return safe;
|
|
520
|
+
}
|
|
521
|
+
function scriptLines(lines, index, keyColumn) {
|
|
522
|
+
const inline = SCRIPT_KEY.exec(lines[index] ?? "")?.[4] ?? "";
|
|
523
|
+
const block = BLOCK_INDICATOR.test(inline.trim());
|
|
524
|
+
const quoted = /^['"]/u.test(inline.trim());
|
|
525
|
+
const keep = (text) => block || quoted ? text : stripYamlComment(text);
|
|
526
|
+
const collected = block ? [] : [{ line: index + 1, text: keep(inline) }];
|
|
527
|
+
for (let j = index + 1; j < lines.length; j += 1) {
|
|
528
|
+
const line = lines[j] ?? "";
|
|
529
|
+
if (line.trim() !== "" && indentOf2(line) <= keyColumn) break;
|
|
530
|
+
collected.push({ line: j + 1, text: keep(line) });
|
|
531
|
+
}
|
|
532
|
+
const first = collected.find((entry) => entry.text.trim() !== "");
|
|
533
|
+
if (inline.trim() === "" && first !== void 0 && MAPPING_LINE.test(first.text)) return [];
|
|
534
|
+
return collected;
|
|
535
|
+
}
|
|
536
|
+
function taintedContexts(expression, checkInputs, checkStepOutputs, safeInputs) {
|
|
537
|
+
const found = [];
|
|
538
|
+
for (const pattern of TAINTED_CONTEXTS) {
|
|
539
|
+
const match = pattern.exec(expression);
|
|
540
|
+
if (match !== null) found.push(match[0]);
|
|
541
|
+
}
|
|
542
|
+
if (checkInputs) {
|
|
543
|
+
for (const match of expression.matchAll(INPUT_CONTEXT)) {
|
|
544
|
+
if (!safeInputs.has(match[1] ?? "")) found.push(match[0]);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (checkStepOutputs) {
|
|
548
|
+
const match = STEP_OUTPUT_CONTEXT.exec(expression);
|
|
549
|
+
if (match !== null) found.push(match[0]);
|
|
550
|
+
}
|
|
551
|
+
return found;
|
|
552
|
+
}
|
|
553
|
+
function checkWorkflowTemplateInjection(file, text, options = {}) {
|
|
554
|
+
const checkInputs = options.checkInputs ?? true;
|
|
555
|
+
const checkStepOutputs = options.checkStepOutputs ?? false;
|
|
556
|
+
const lines = text.split("\n");
|
|
557
|
+
const safeInputs = checkInputs ? safeInputNames(lines) : /* @__PURE__ */ new Set();
|
|
558
|
+
const violations = [];
|
|
559
|
+
let skipThrough = -1;
|
|
560
|
+
lines.forEach((line, index) => {
|
|
561
|
+
const match = index > skipThrough ? SCRIPT_KEY.exec(line) : null;
|
|
562
|
+
if (match === null) return;
|
|
563
|
+
const keyColumn = (match[1]?.length ?? 0) + (match[2]?.length ?? 0);
|
|
564
|
+
const key = match[3] ?? "run";
|
|
565
|
+
if (key === "script") {
|
|
566
|
+
const parent = parentOf(lines, index, keyColumn);
|
|
567
|
+
if (!/^\s*with:\s*(?:#.*)?$/u.test(lines[parent] ?? "")) return;
|
|
568
|
+
if (!isGithubScriptStep(lines, parent)) return;
|
|
569
|
+
}
|
|
570
|
+
const where = key === "run" ? "`run:` script" : "`actions/github-script` `script:`";
|
|
571
|
+
const script = scriptLines(lines, index, keyColumn);
|
|
572
|
+
skipThrough = (script[script.length - 1]?.line ?? index + 1) - 1;
|
|
573
|
+
for (const entry of script) {
|
|
574
|
+
for (const expression of entry.text.matchAll(EXPRESSION)) {
|
|
575
|
+
const contexts = taintedContexts(
|
|
576
|
+
expression[1] ?? "",
|
|
577
|
+
checkInputs,
|
|
578
|
+
checkStepOutputs,
|
|
579
|
+
safeInputs
|
|
580
|
+
);
|
|
581
|
+
if (contexts.length === 0) continue;
|
|
582
|
+
violations.push({
|
|
583
|
+
file,
|
|
584
|
+
rule: RULE_ID3,
|
|
585
|
+
message: `line ${entry.line}: \`${expression[0]}\` expands attacker-controllable \`${contexts.join("`, `")}\` into a ${where}, where it runs as code. Pass it through \`env:\` (for example \`VALUE: ${expression[0]}\`) and read \`"$VALUE"\` instead.`,
|
|
586
|
+
line: entry.line
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
return violations;
|
|
592
|
+
}
|
|
593
|
+
function createGithubActionsNoTemplateInjectionRule(options = {}) {
|
|
594
|
+
const workflowGlobs = options.workflowGlobs ?? DEFAULT_WORKFLOW_GLOBS;
|
|
595
|
+
const actionGlobs = options.actionGlobs ?? DEFAULT_ACTION_GLOBS;
|
|
596
|
+
const skipDirs = options.skipDirs ?? DEFAULT_SKIP_DIRS;
|
|
597
|
+
const ciCritical = options.ciCritical ?? true;
|
|
598
|
+
return {
|
|
599
|
+
id: RULE_ID3,
|
|
600
|
+
category: "ci",
|
|
601
|
+
ciCritical,
|
|
602
|
+
description: "GitHub Actions `run:` and github-script bodies never expand attacker-controllable `${{ }}` context (issue/PR titles, comments, branch names); pass it through `env:` instead.",
|
|
603
|
+
run(ctx) {
|
|
604
|
+
const files = globFiles(
|
|
605
|
+
(p) => ctx.glob(p),
|
|
606
|
+
[...workflowGlobs, ...actionGlobs],
|
|
607
|
+
skipDirs
|
|
608
|
+
);
|
|
609
|
+
return files.flatMap(
|
|
610
|
+
(file) => checkWorkflowTemplateInjection(file, ctx.read(file) ?? "", options)
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
329
616
|
// src/rules/github-actions-runner-pinned.ts
|
|
330
|
-
var
|
|
617
|
+
var RULE_ID4 = "github-actions-runner-pinned";
|
|
331
618
|
var RUNS_ON = /^(\s*)runs-on:\s*(.*)$/u;
|
|
332
619
|
var DEFAULT_FLOATING_LABEL = /^[\w.-]+-latest$/u;
|
|
333
620
|
function runnerLabels(lines, start, indent) {
|
|
@@ -359,7 +646,7 @@ function checkWorkflowRunnersPinned(file, text, floatingLabel = DEFAULT_FLOATING
|
|
|
359
646
|
if (floatingLabel.test(label)) {
|
|
360
647
|
violations.push({
|
|
361
648
|
file,
|
|
362
|
-
rule:
|
|
649
|
+
rule: RULE_ID4,
|
|
363
650
|
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
651
|
line: index + 1
|
|
365
652
|
});
|
|
@@ -373,7 +660,7 @@ function createGithubActionsRunnerPinnedRule(options = {}) {
|
|
|
373
660
|
const floatingLabel = options.floatingLabel ?? DEFAULT_FLOATING_LABEL;
|
|
374
661
|
const ciCritical = options.ciCritical ?? true;
|
|
375
662
|
return {
|
|
376
|
-
id:
|
|
663
|
+
id: RULE_ID4,
|
|
377
664
|
category: "ci",
|
|
378
665
|
ciCritical,
|
|
379
666
|
description: "GitHub Actions jobs must run on a pinned runner image (for example ubuntu-24.04), never a *-latest label.",
|
|
@@ -386,7 +673,7 @@ function createGithubActionsRunnerPinnedRule(options = {}) {
|
|
|
386
673
|
}
|
|
387
674
|
|
|
388
675
|
// src/rules/github-actions-sha-pinned.ts
|
|
389
|
-
var
|
|
676
|
+
var RULE_ID5 = "github-actions-sha-pinned";
|
|
390
677
|
var USES_LINE = /^\s*(?:-\s+)?uses:\s*['"]?([^\s'"#]+)['"]?\s*(#.*)?$/u;
|
|
391
678
|
var FULL_SHA = /^[0-9a-f]{40}$/u;
|
|
392
679
|
var DOCKER_DIGEST = /@sha256:[0-9a-f]{64}$/u;
|
|
@@ -401,7 +688,7 @@ function checkWorkflowActionsPinned(file, text) {
|
|
|
401
688
|
}
|
|
402
689
|
const where = `line ${index + 1}`;
|
|
403
690
|
const report = (message) => {
|
|
404
|
-
violations.push({ file, rule:
|
|
691
|
+
violations.push({ file, rule: RULE_ID5, message, line: index + 1 });
|
|
405
692
|
};
|
|
406
693
|
if (ref.startsWith("docker://")) {
|
|
407
694
|
if (!DOCKER_DIGEST.test(ref)) {
|
|
@@ -429,7 +716,7 @@ function createGithubActionsShaPinnedRule(options = {}) {
|
|
|
429
716
|
const workflowGlobs = options.workflowGlobs ?? DEFAULT_WORKFLOW_GLOBS;
|
|
430
717
|
const ciCritical = options.ciCritical ?? true;
|
|
431
718
|
return {
|
|
432
|
-
id:
|
|
719
|
+
id: RULE_ID5,
|
|
433
720
|
category: "ci",
|
|
434
721
|
ciCritical,
|
|
435
722
|
description: "GitHub Actions `uses:` refs must be pinned to a 40-character commit SHA with a `# vN` comment (local ./ actions exempt).",
|
|
@@ -654,7 +941,7 @@ function createPackageShapeRule(options = {}) {
|
|
|
654
941
|
}
|
|
655
942
|
|
|
656
943
|
// src/rules/security-scanner-version-parity.ts
|
|
657
|
-
var
|
|
944
|
+
var RULE_ID6 = "security-scanner-version-parity";
|
|
658
945
|
function createSecurityScannerVersionParityRule(options = {}) {
|
|
659
946
|
const scanner = options.scanner ?? "gitleaks";
|
|
660
947
|
const versionVariable = options.versionVariable ?? "GITLEAKS_VERSION";
|
|
@@ -679,7 +966,7 @@ function createSecurityScannerVersionParityRule(options = {}) {
|
|
|
679
966
|
const runtimeCheck = new RegExp(`\\b${name}\\s+version\\b`, "u");
|
|
680
967
|
const mentionsScanner = new RegExp(name, "iu");
|
|
681
968
|
return {
|
|
682
|
-
id:
|
|
969
|
+
id: RULE_ID6,
|
|
683
970
|
category: "ci",
|
|
684
971
|
ciCritical,
|
|
685
972
|
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.`,
|
|
@@ -698,7 +985,7 @@ function createSecurityScannerVersionParityRule(options = {}) {
|
|
|
698
985
|
}
|
|
699
986
|
const violations = [];
|
|
700
987
|
const report = (file, message) => {
|
|
701
|
-
violations.push({ file, rule:
|
|
988
|
+
violations.push({ file, rule: RULE_ID6, message });
|
|
702
989
|
};
|
|
703
990
|
if (ciVersions.size > 1) {
|
|
704
991
|
report(
|
|
@@ -751,7 +1038,7 @@ function createSecurityScannerVersionParityRule(options = {}) {
|
|
|
751
1038
|
}
|
|
752
1039
|
|
|
753
1040
|
// src/rules/service-image-digest-pin.ts
|
|
754
|
-
var
|
|
1041
|
+
var RULE_ID7 = "service-image-digest-pin";
|
|
755
1042
|
var DIGEST2 = /@sha256:[0-9a-f]{64}$/u;
|
|
756
1043
|
var KEY_LINE = /^(\s*)(?:-\s+)?([\w-]+):\s*(.*)$/u;
|
|
757
1044
|
var DEFAULT_COMPOSE_GLOBS = anywhereGlobs(
|
|
@@ -759,7 +1046,7 @@ var DEFAULT_COMPOSE_GLOBS = anywhereGlobs(
|
|
|
759
1046
|
(stem) => ["", ".*", "-*"].flatMap((infix) => ["yml", "yaml"].map((ext) => `${stem}${infix}.${ext}`))
|
|
760
1047
|
)
|
|
761
1048
|
);
|
|
762
|
-
function
|
|
1049
|
+
function indentOf3(line) {
|
|
763
1050
|
return /^\s*/u.exec(line)?.[0].length ?? 0;
|
|
764
1051
|
}
|
|
765
1052
|
function workflowImages(text) {
|
|
@@ -811,7 +1098,7 @@ function composeImages(text) {
|
|
|
811
1098
|
if (line.trim() === "" || line.trimStart().startsWith("#")) {
|
|
812
1099
|
continue;
|
|
813
1100
|
}
|
|
814
|
-
const indent =
|
|
1101
|
+
const indent = indentOf3(line);
|
|
815
1102
|
if (indent === 0) {
|
|
816
1103
|
break;
|
|
817
1104
|
}
|
|
@@ -845,7 +1132,7 @@ function violationsFor(file, images, allowUnpinned) {
|
|
|
845
1132
|
const reference = image.split("@")[0];
|
|
846
1133
|
return {
|
|
847
1134
|
file,
|
|
848
|
-
rule:
|
|
1135
|
+
rule: RULE_ID7,
|
|
849
1136
|
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
1137
|
line
|
|
851
1138
|
};
|
|
@@ -858,7 +1145,7 @@ function createServiceImageDigestPinRule(options = {}) {
|
|
|
858
1145
|
const allowUnpinned = new Set(options.allowUnpinned ?? []);
|
|
859
1146
|
const ciCritical = options.ciCritical ?? true;
|
|
860
1147
|
return {
|
|
861
|
-
id:
|
|
1148
|
+
id: RULE_ID7,
|
|
862
1149
|
category: "ci",
|
|
863
1150
|
ciCritical,
|
|
864
1151
|
description: "Workflow service and container images, and docker-compose images, must be pinned by `@sha256:` digest (a service that builds locally is exempt).",
|
|
@@ -1143,6 +1430,8 @@ var RULE_FACTORIES = {
|
|
|
1143
1430
|
"canonical-helpers-single-home": createCanonicalHelpersSingleHomeRule,
|
|
1144
1431
|
"dockerfile-base-image-digest-pin": createDockerfileBaseImageDigestPinRule,
|
|
1145
1432
|
"file-size-ratchet": createFileSizeRatchetRule,
|
|
1433
|
+
"github-actions-least-privilege-permissions": createGithubActionsLeastPrivilegePermissionsRule,
|
|
1434
|
+
"github-actions-no-template-injection": createGithubActionsNoTemplateInjectionRule,
|
|
1146
1435
|
"github-actions-runner-pinned": createGithubActionsRunnerPinnedRule,
|
|
1147
1436
|
"github-actions-sha-pinned": createGithubActionsShaPinnedRule,
|
|
1148
1437
|
"layer-rank": createLayerRankRule,
|
|
@@ -1171,6 +1460,8 @@ function createAllRules() {
|
|
|
1171
1460
|
createCanonicalHelpersSingleHomeRule,
|
|
1172
1461
|
createDockerfileBaseImageDigestPinRule,
|
|
1173
1462
|
createFileSizeRatchetRule,
|
|
1463
|
+
createGithubActionsLeastPrivilegePermissionsRule,
|
|
1464
|
+
createGithubActionsNoTemplateInjectionRule,
|
|
1174
1465
|
createGithubActionsRunnerPinnedRule,
|
|
1175
1466
|
createGithubActionsShaPinnedRule,
|
|
1176
1467
|
createLayerRankRule,
|
package/dist/index.d.cts
CHANGED
|
@@ -116,6 +116,46 @@ interface FileSizeRatchetOptions {
|
|
|
116
116
|
*/
|
|
117
117
|
declare function createFileSizeRatchetRule(options?: FileSizeRatchetOptions): IMetaRule;
|
|
118
118
|
|
|
119
|
+
/** Options for {@link createGithubActionsLeastPrivilegePermissionsRule}. */
|
|
120
|
+
interface GithubActionsLeastPrivilegePermissionsOptions {
|
|
121
|
+
/** Globs of the workflow files to scan. Default `.github/workflows/*.y(a)ml`. */
|
|
122
|
+
readonly workflowGlobs?: readonly string[];
|
|
123
|
+
/**
|
|
124
|
+
* Scopes allowed at `write` in the top-level `permissions:` block (for example `contents` in a
|
|
125
|
+
* release workflow whose every job pushes). Default none.
|
|
126
|
+
*/
|
|
127
|
+
readonly allowTopLevelWrite?: readonly string[];
|
|
128
|
+
/** Whether a violation fails CI. Default `true`. */
|
|
129
|
+
readonly ciCritical?: boolean;
|
|
130
|
+
}
|
|
131
|
+
/** A workflow's top-level `permissions:` exists, is not `*-all`, and grants no write. */
|
|
132
|
+
declare function createGithubActionsLeastPrivilegePermissionsRule(options?: GithubActionsLeastPrivilegePermissionsOptions): IMetaRule;
|
|
133
|
+
|
|
134
|
+
/** Options for {@link createGithubActionsNoTemplateInjectionRule}. */
|
|
135
|
+
interface GithubActionsNoTemplateInjectionOptions {
|
|
136
|
+
/** Globs of the workflow files to scan. Default `.github/workflows/*.y(a)ml`. */
|
|
137
|
+
readonly workflowGlobs?: readonly string[];
|
|
138
|
+
/** Globs that find composite action metadata. Default `action.yml` / `action.yaml` at any depth. */
|
|
139
|
+
readonly actionGlobs?: readonly string[];
|
|
140
|
+
/** An action path with any of these segments is skipped. Default `node_modules`, `.git`, `dist`, `.turbo`, `coverage`. */
|
|
141
|
+
readonly skipDirs?: readonly string[];
|
|
142
|
+
/**
|
|
143
|
+
* Treat `inputs.<name>` (and `github.event.inputs.<name>`) as attacker-controlled. An input the
|
|
144
|
+
* same file declares with `type: boolean`, `number` or `choice` is still left alone: its value
|
|
145
|
+
* cannot carry a script. Default `true`.
|
|
146
|
+
*/
|
|
147
|
+
readonly checkInputs?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Treat `steps.<id>.outputs.<name>` as attacker-controlled. An output is only as tainted as
|
|
150
|
+
* whatever the step wrote into it, which the text cannot show. Default `false`.
|
|
151
|
+
*/
|
|
152
|
+
readonly checkStepOutputs?: boolean;
|
|
153
|
+
/** Whether a violation fails CI. Default `true`. */
|
|
154
|
+
readonly ciCritical?: boolean;
|
|
155
|
+
}
|
|
156
|
+
/** No attacker-controllable `${{ }}` expression may be expanded into a `run:` or github-script body. */
|
|
157
|
+
declare function createGithubActionsNoTemplateInjectionRule(options?: GithubActionsNoTemplateInjectionOptions): IMetaRule;
|
|
158
|
+
|
|
119
159
|
/**
|
|
120
160
|
* Options for {@link createGithubActionsRunnerPinnedRule}.
|
|
121
161
|
*
|
|
@@ -499,6 +539,8 @@ declare const RULE_FACTORIES: {
|
|
|
499
539
|
readonly 'canonical-helpers-single-home': typeof createCanonicalHelpersSingleHomeRule;
|
|
500
540
|
readonly 'dockerfile-base-image-digest-pin': typeof createDockerfileBaseImageDigestPinRule;
|
|
501
541
|
readonly 'file-size-ratchet': typeof createFileSizeRatchetRule;
|
|
542
|
+
readonly 'github-actions-least-privilege-permissions': typeof createGithubActionsLeastPrivilegePermissionsRule;
|
|
543
|
+
readonly 'github-actions-no-template-injection': typeof createGithubActionsNoTemplateInjectionRule;
|
|
502
544
|
readonly 'github-actions-runner-pinned': typeof createGithubActionsRunnerPinnedRule;
|
|
503
545
|
readonly 'github-actions-sha-pinned': typeof createGithubActionsShaPinnedRule;
|
|
504
546
|
readonly 'layer-rank': typeof createLayerRankRule;
|
|
@@ -518,4 +560,4 @@ declare const RULE_IDS: (keyof typeof RULE_FACTORIES)[];
|
|
|
518
560
|
/** Instantiate every catalog rule with its default options. */
|
|
519
561
|
declare function createAllRules(): IMetaRule[];
|
|
520
562
|
|
|
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 };
|
|
563
|
+
export { type AgentsDocPresenceOptions, type CanonicalHelpersSingleHomeOptions, type DockerfileBaseImageDigestPinOptions, type FileSizeRatchetOptions, type GithubActionsLeastPrivilegePermissionsOptions, type GithubActionsNoTemplateInjectionOptions, 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, createGithubActionsLeastPrivilegePermissionsRule, createGithubActionsNoTemplateInjectionRule, createGithubActionsRunnerPinnedRule, createGithubActionsShaPinnedRule, createLayerRankRule, createNoClonedComponentFoldersRule, createNoWarnSeverityRule, createPackageShapeRule, createSecurityScannerVersionParityRule, createServiceImageDigestPinRule, createTestRunnerSegregationRule, createTestSiblingEnforcementRule, createTestWorkspaceEnrollmentRule, createUiPrimitiveShapeRule, createWorkspaceGraphParityRule };
|
package/dist/index.d.ts
CHANGED
|
@@ -116,6 +116,46 @@ interface FileSizeRatchetOptions {
|
|
|
116
116
|
*/
|
|
117
117
|
declare function createFileSizeRatchetRule(options?: FileSizeRatchetOptions): IMetaRule;
|
|
118
118
|
|
|
119
|
+
/** Options for {@link createGithubActionsLeastPrivilegePermissionsRule}. */
|
|
120
|
+
interface GithubActionsLeastPrivilegePermissionsOptions {
|
|
121
|
+
/** Globs of the workflow files to scan. Default `.github/workflows/*.y(a)ml`. */
|
|
122
|
+
readonly workflowGlobs?: readonly string[];
|
|
123
|
+
/**
|
|
124
|
+
* Scopes allowed at `write` in the top-level `permissions:` block (for example `contents` in a
|
|
125
|
+
* release workflow whose every job pushes). Default none.
|
|
126
|
+
*/
|
|
127
|
+
readonly allowTopLevelWrite?: readonly string[];
|
|
128
|
+
/** Whether a violation fails CI. Default `true`. */
|
|
129
|
+
readonly ciCritical?: boolean;
|
|
130
|
+
}
|
|
131
|
+
/** A workflow's top-level `permissions:` exists, is not `*-all`, and grants no write. */
|
|
132
|
+
declare function createGithubActionsLeastPrivilegePermissionsRule(options?: GithubActionsLeastPrivilegePermissionsOptions): IMetaRule;
|
|
133
|
+
|
|
134
|
+
/** Options for {@link createGithubActionsNoTemplateInjectionRule}. */
|
|
135
|
+
interface GithubActionsNoTemplateInjectionOptions {
|
|
136
|
+
/** Globs of the workflow files to scan. Default `.github/workflows/*.y(a)ml`. */
|
|
137
|
+
readonly workflowGlobs?: readonly string[];
|
|
138
|
+
/** Globs that find composite action metadata. Default `action.yml` / `action.yaml` at any depth. */
|
|
139
|
+
readonly actionGlobs?: readonly string[];
|
|
140
|
+
/** An action path with any of these segments is skipped. Default `node_modules`, `.git`, `dist`, `.turbo`, `coverage`. */
|
|
141
|
+
readonly skipDirs?: readonly string[];
|
|
142
|
+
/**
|
|
143
|
+
* Treat `inputs.<name>` (and `github.event.inputs.<name>`) as attacker-controlled. An input the
|
|
144
|
+
* same file declares with `type: boolean`, `number` or `choice` is still left alone: its value
|
|
145
|
+
* cannot carry a script. Default `true`.
|
|
146
|
+
*/
|
|
147
|
+
readonly checkInputs?: boolean;
|
|
148
|
+
/**
|
|
149
|
+
* Treat `steps.<id>.outputs.<name>` as attacker-controlled. An output is only as tainted as
|
|
150
|
+
* whatever the step wrote into it, which the text cannot show. Default `false`.
|
|
151
|
+
*/
|
|
152
|
+
readonly checkStepOutputs?: boolean;
|
|
153
|
+
/** Whether a violation fails CI. Default `true`. */
|
|
154
|
+
readonly ciCritical?: boolean;
|
|
155
|
+
}
|
|
156
|
+
/** No attacker-controllable `${{ }}` expression may be expanded into a `run:` or github-script body. */
|
|
157
|
+
declare function createGithubActionsNoTemplateInjectionRule(options?: GithubActionsNoTemplateInjectionOptions): IMetaRule;
|
|
158
|
+
|
|
119
159
|
/**
|
|
120
160
|
* Options for {@link createGithubActionsRunnerPinnedRule}.
|
|
121
161
|
*
|
|
@@ -499,6 +539,8 @@ declare const RULE_FACTORIES: {
|
|
|
499
539
|
readonly 'canonical-helpers-single-home': typeof createCanonicalHelpersSingleHomeRule;
|
|
500
540
|
readonly 'dockerfile-base-image-digest-pin': typeof createDockerfileBaseImageDigestPinRule;
|
|
501
541
|
readonly 'file-size-ratchet': typeof createFileSizeRatchetRule;
|
|
542
|
+
readonly 'github-actions-least-privilege-permissions': typeof createGithubActionsLeastPrivilegePermissionsRule;
|
|
543
|
+
readonly 'github-actions-no-template-injection': typeof createGithubActionsNoTemplateInjectionRule;
|
|
502
544
|
readonly 'github-actions-runner-pinned': typeof createGithubActionsRunnerPinnedRule;
|
|
503
545
|
readonly 'github-actions-sha-pinned': typeof createGithubActionsShaPinnedRule;
|
|
504
546
|
readonly 'layer-rank': typeof createLayerRankRule;
|
|
@@ -518,4 +560,4 @@ declare const RULE_IDS: (keyof typeof RULE_FACTORIES)[];
|
|
|
518
560
|
/** Instantiate every catalog rule with its default options. */
|
|
519
561
|
declare function createAllRules(): IMetaRule[];
|
|
520
562
|
|
|
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 };
|
|
563
|
+
export { type AgentsDocPresenceOptions, type CanonicalHelpersSingleHomeOptions, type DockerfileBaseImageDigestPinOptions, type FileSizeRatchetOptions, type GithubActionsLeastPrivilegePermissionsOptions, type GithubActionsNoTemplateInjectionOptions, 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, createGithubActionsLeastPrivilegePermissionsRule, createGithubActionsNoTemplateInjectionRule, createGithubActionsRunnerPinnedRule, createGithubActionsShaPinnedRule, createLayerRankRule, createNoClonedComponentFoldersRule, createNoWarnSeverityRule, createPackageShapeRule, createSecurityScannerVersionParityRule, createServiceImageDigestPinRule, createTestRunnerSegregationRule, createTestSiblingEnforcementRule, createTestWorkspaceEnrollmentRule, createUiPrimitiveShapeRule, createWorkspaceGraphParityRule };
|