@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/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
recursiveGlobs,
|
|
11
11
|
stripYamlComment,
|
|
12
12
|
unquote
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-OYFQKSJN.js";
|
|
14
14
|
|
|
15
15
|
// src/rules/agents-doc-presence.ts
|
|
16
16
|
function createAgentsDocPresenceRule(options = {}) {
|
|
@@ -244,8 +244,293 @@ function createFileSizeRatchetRule(options = {}) {
|
|
|
244
244
|
};
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
// src/rules/github-actions-least-privilege-permissions.ts
|
|
248
|
+
var RULE_ID2 = "github-actions-least-privilege-permissions";
|
|
249
|
+
var TOP_KEY = /^([\w-]+):\s*(.*)$/u;
|
|
250
|
+
var SCOPE_ENTRY = /^\s*['"]?([\w-]+)['"]?:\s*['"]?([\w-]+)['"]?\s*$/u;
|
|
251
|
+
function indentOf(line) {
|
|
252
|
+
return /^\s*/u.exec(line)?.[0].length ?? 0;
|
|
253
|
+
}
|
|
254
|
+
function isContent(line) {
|
|
255
|
+
const trimmed = line.trim();
|
|
256
|
+
return trimmed !== "" && !trimmed.startsWith("#");
|
|
257
|
+
}
|
|
258
|
+
function topLevelKeys(lines) {
|
|
259
|
+
const keys = [];
|
|
260
|
+
lines.forEach((line, index) => {
|
|
261
|
+
const match = TOP_KEY.exec(line);
|
|
262
|
+
if (match === null) return;
|
|
263
|
+
keys.push({ key: unquote(match[1] ?? ""), value: stripYamlComment(match[2] ?? "").trim(), index });
|
|
264
|
+
});
|
|
265
|
+
return keys;
|
|
266
|
+
}
|
|
267
|
+
function blockOf(lines, index) {
|
|
268
|
+
const block = [];
|
|
269
|
+
for (let j = index + 1; j < lines.length; j += 1) {
|
|
270
|
+
const line = lines[j] ?? "";
|
|
271
|
+
if (!isContent(line)) continue;
|
|
272
|
+
if (indentOf(line) === 0) break;
|
|
273
|
+
block.push({ text: stripYamlComment(line), index: j });
|
|
274
|
+
}
|
|
275
|
+
return block;
|
|
276
|
+
}
|
|
277
|
+
function scopeEntries(lines, permissions) {
|
|
278
|
+
if (permissions.value.startsWith("{")) {
|
|
279
|
+
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 }));
|
|
280
|
+
}
|
|
281
|
+
return blockOf(lines, permissions.index).flatMap(({ text, index }) => {
|
|
282
|
+
const match = SCOPE_ENTRY.exec(text);
|
|
283
|
+
return match === null ? [] : [{ scope: match[1] ?? "", level: match[2] ?? "", line: index + 1 }];
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function jobsWithoutPermissions(lines, jobs) {
|
|
287
|
+
const block = blockOf(lines, jobs.index);
|
|
288
|
+
const jobColumn = indentOf(block[0]?.text ?? "");
|
|
289
|
+
const missing = [];
|
|
290
|
+
let current;
|
|
291
|
+
let keyColumn = -1;
|
|
292
|
+
const settle = () => {
|
|
293
|
+
if (current !== void 0 && !current.hasPermissions) missing.push(current.name);
|
|
294
|
+
};
|
|
295
|
+
for (const { text } of block) {
|
|
296
|
+
const column = indentOf(text);
|
|
297
|
+
if (column === jobColumn) {
|
|
298
|
+
settle();
|
|
299
|
+
current = { name: unquote(text.trim().replace(/:.*$/u, "")), hasPermissions: false };
|
|
300
|
+
keyColumn = -1;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (current === void 0 || column < jobColumn) continue;
|
|
304
|
+
if (keyColumn === -1) keyColumn = column;
|
|
305
|
+
if (column === keyColumn && /^\s*permissions:/u.test(text)) current.hasPermissions = true;
|
|
306
|
+
}
|
|
307
|
+
settle();
|
|
308
|
+
return missing;
|
|
309
|
+
}
|
|
310
|
+
function checkWorkflowPermissions(file, text, allowTopLevelWrite = []) {
|
|
311
|
+
const lines = text.split("\n");
|
|
312
|
+
const keys = topLevelKeys(lines);
|
|
313
|
+
const permissions = keys.find((entry) => entry.key === "permissions");
|
|
314
|
+
const report = (line, message) => ({
|
|
315
|
+
file,
|
|
316
|
+
rule: RULE_ID2,
|
|
317
|
+
message: `line ${line}: ${message}`,
|
|
318
|
+
line
|
|
319
|
+
});
|
|
320
|
+
if (permissions === void 0) {
|
|
321
|
+
const jobs = keys.find((entry) => entry.key === "jobs");
|
|
322
|
+
const missing = jobs === void 0 ? [] : jobsWithoutPermissions(lines, jobs);
|
|
323
|
+
if (missing.length === 0) return [];
|
|
324
|
+
return [
|
|
325
|
+
report(
|
|
326
|
+
1,
|
|
327
|
+
`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.`
|
|
328
|
+
)
|
|
329
|
+
];
|
|
330
|
+
}
|
|
331
|
+
const shorthand = unquote(permissions.value);
|
|
332
|
+
if (shorthand === "write-all" || shorthand === "read-all") {
|
|
333
|
+
return [
|
|
334
|
+
report(
|
|
335
|
+
permissions.index + 1,
|
|
336
|
+
`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.`
|
|
337
|
+
)
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
const allowed = new Set(allowTopLevelWrite);
|
|
341
|
+
return scopeEntries(lines, permissions).filter(({ scope, level }) => level === "write" && !allowed.has(scope)).map(
|
|
342
|
+
({ scope, line }) => report(
|
|
343
|
+
line,
|
|
344
|
+
`top-level \`permissions\` grants \`${scope}: write\` to every job. Move it to the job that needs it and keep the top level read-only.`
|
|
345
|
+
)
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
function createGithubActionsLeastPrivilegePermissionsRule(options = {}) {
|
|
349
|
+
const workflowGlobs = options.workflowGlobs ?? DEFAULT_WORKFLOW_GLOBS;
|
|
350
|
+
const allowTopLevelWrite = options.allowTopLevelWrite ?? [];
|
|
351
|
+
const ciCritical = options.ciCritical ?? true;
|
|
352
|
+
return {
|
|
353
|
+
id: RULE_ID2,
|
|
354
|
+
category: "ci",
|
|
355
|
+
ciCritical,
|
|
356
|
+
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.",
|
|
357
|
+
run(ctx) {
|
|
358
|
+
return globFiles((p) => ctx.glob(p), workflowGlobs).flatMap(
|
|
359
|
+
(file) => checkWorkflowPermissions(file, ctx.read(file) ?? "", allowTopLevelWrite)
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/rules/github-actions-no-template-injection.ts
|
|
366
|
+
var RULE_ID3 = "github-actions-no-template-injection";
|
|
367
|
+
var DEFAULT_ACTION_GLOBS = anywhereGlobs(["action.yml", "action.yaml"]);
|
|
368
|
+
var EVENT = String.raw`(?<![\w.])github\.event\.`;
|
|
369
|
+
var ANY_ITEM = String.raw`(?:\.\*|\[\d+\])`;
|
|
370
|
+
var TAINTED_CONTEXTS = [
|
|
371
|
+
new RegExp(String.raw`${EVENT}(?:issue|pull_request|discussion)\.(?:title|body)\b`, "u"),
|
|
372
|
+
new RegExp(String.raw`${EVENT}pull_request\.head\.(?:ref|label)\b`, "u"),
|
|
373
|
+
new RegExp(String.raw`${EVENT}(?:comment|review|review_comment)\.body\b`, "u"),
|
|
374
|
+
new RegExp(String.raw`${EVENT}pages${ANY_ITEM}\.page_name\b`, "u"),
|
|
375
|
+
new RegExp(String.raw`${EVENT}commits${ANY_ITEM}\.(?:message|author|committer)\b`, "u"),
|
|
376
|
+
new RegExp(String.raw`${EVENT}head_commit\.(?:message|author|committer)\b`, "u"),
|
|
377
|
+
new RegExp(String.raw`${EVENT}workflow_run\.head_branch\b`, "u"),
|
|
378
|
+
new RegExp(String.raw`${EVENT}workflow_run\.head_commit\.(?:message|author|committer)\b`, "u"),
|
|
379
|
+
/(?<![\w.])github\.head_ref\b/u
|
|
380
|
+
];
|
|
381
|
+
var INPUT_CONTEXT = /(?<![\w.])(?:github\.event\.)?inputs\.([\w-]+)/gu;
|
|
382
|
+
var STEP_OUTPUT_CONTEXT = /(?<![\w.])steps\.[\w-]+\.outputs\.[\w-]+/u;
|
|
383
|
+
var SAFE_INPUT_TYPES = /* @__PURE__ */ new Set(["boolean", "number", "choice"]);
|
|
384
|
+
var EXPRESSION = /\$\{\{(.*?)\}\}/gu;
|
|
385
|
+
var SCRIPT_KEY = /^(\s*)(-\s+)?(run|script):(?:\s+(.*))?$/u;
|
|
386
|
+
var BLOCK_INDICATOR = /^[|>][-+0-9]*\s*(?:#.*)?$/u;
|
|
387
|
+
var MAPPING_LINE = /^\s*[\w-]+:(?:\s|$)/u;
|
|
388
|
+
var GITHUB_SCRIPT_USES = /^\s*(?:-\s+)?uses:\s*['"]?actions\/github-script@/u;
|
|
389
|
+
function indentOf2(line) {
|
|
390
|
+
return /^\s*/u.exec(line)?.[0].length ?? 0;
|
|
391
|
+
}
|
|
392
|
+
function isContent2(line) {
|
|
393
|
+
const trimmed = line.trim();
|
|
394
|
+
return trimmed !== "" && !trimmed.startsWith("#");
|
|
395
|
+
}
|
|
396
|
+
function parentOf(lines, index, column) {
|
|
397
|
+
for (let j = index - 1; j >= 0; j -= 1) {
|
|
398
|
+
const line = lines[j] ?? "";
|
|
399
|
+
if (isContent2(line) && indentOf2(line) < column) return j;
|
|
400
|
+
}
|
|
401
|
+
return -1;
|
|
402
|
+
}
|
|
403
|
+
function isGithubScriptStep(lines, withIndex) {
|
|
404
|
+
const stepStart = parentOf(lines, withIndex, indentOf2(lines[withIndex] ?? ""));
|
|
405
|
+
const stepLine = lines[stepStart] ?? "";
|
|
406
|
+
if (!stepLine.trimStart().startsWith("-")) return false;
|
|
407
|
+
const dashColumn = indentOf2(stepLine);
|
|
408
|
+
for (let j = stepStart; j < lines.length; j += 1) {
|
|
409
|
+
const line = lines[j] ?? "";
|
|
410
|
+
if (j > stepStart && isContent2(line) && indentOf2(line) <= dashColumn) break;
|
|
411
|
+
if (GITHUB_SCRIPT_USES.test(line)) return true;
|
|
412
|
+
}
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
function safeInputNames(lines) {
|
|
416
|
+
const safe = /* @__PURE__ */ new Set();
|
|
417
|
+
lines.forEach((line, index) => {
|
|
418
|
+
if (!/^\s*inputs:\s*(?:#.*)?$/u.test(line)) return;
|
|
419
|
+
const column = indentOf2(line);
|
|
420
|
+
let nameColumn = -1;
|
|
421
|
+
let name = "";
|
|
422
|
+
for (const child of lines.slice(index + 1)) {
|
|
423
|
+
if (!isContent2(child)) continue;
|
|
424
|
+
const childColumn = indentOf2(child);
|
|
425
|
+
if (childColumn <= column) break;
|
|
426
|
+
if (nameColumn === -1) nameColumn = childColumn;
|
|
427
|
+
if (childColumn === nameColumn) {
|
|
428
|
+
name = /^\s*['"]?([\w-]+)['"]?:/u.exec(child)?.[1] ?? "";
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
const type = /^\s*type:\s*['"]?(\w+)/u.exec(stripYamlComment(child))?.[1];
|
|
432
|
+
if (type !== void 0 && SAFE_INPUT_TYPES.has(type) && name !== "") safe.add(name);
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
return safe;
|
|
436
|
+
}
|
|
437
|
+
function scriptLines(lines, index, keyColumn) {
|
|
438
|
+
const inline = SCRIPT_KEY.exec(lines[index] ?? "")?.[4] ?? "";
|
|
439
|
+
const block = BLOCK_INDICATOR.test(inline.trim());
|
|
440
|
+
const quoted = /^['"]/u.test(inline.trim());
|
|
441
|
+
const keep = (text) => block || quoted ? text : stripYamlComment(text);
|
|
442
|
+
const collected = block ? [] : [{ line: index + 1, text: keep(inline) }];
|
|
443
|
+
for (let j = index + 1; j < lines.length; j += 1) {
|
|
444
|
+
const line = lines[j] ?? "";
|
|
445
|
+
if (line.trim() !== "" && indentOf2(line) <= keyColumn) break;
|
|
446
|
+
collected.push({ line: j + 1, text: keep(line) });
|
|
447
|
+
}
|
|
448
|
+
const first = collected.find((entry) => entry.text.trim() !== "");
|
|
449
|
+
if (inline.trim() === "" && first !== void 0 && MAPPING_LINE.test(first.text)) return [];
|
|
450
|
+
return collected;
|
|
451
|
+
}
|
|
452
|
+
function taintedContexts(expression, checkInputs, checkStepOutputs, safeInputs) {
|
|
453
|
+
const found = [];
|
|
454
|
+
for (const pattern of TAINTED_CONTEXTS) {
|
|
455
|
+
const match = pattern.exec(expression);
|
|
456
|
+
if (match !== null) found.push(match[0]);
|
|
457
|
+
}
|
|
458
|
+
if (checkInputs) {
|
|
459
|
+
for (const match of expression.matchAll(INPUT_CONTEXT)) {
|
|
460
|
+
if (!safeInputs.has(match[1] ?? "")) found.push(match[0]);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (checkStepOutputs) {
|
|
464
|
+
const match = STEP_OUTPUT_CONTEXT.exec(expression);
|
|
465
|
+
if (match !== null) found.push(match[0]);
|
|
466
|
+
}
|
|
467
|
+
return found;
|
|
468
|
+
}
|
|
469
|
+
function checkWorkflowTemplateInjection(file, text, options = {}) {
|
|
470
|
+
const checkInputs = options.checkInputs ?? true;
|
|
471
|
+
const checkStepOutputs = options.checkStepOutputs ?? false;
|
|
472
|
+
const lines = text.split("\n");
|
|
473
|
+
const safeInputs = checkInputs ? safeInputNames(lines) : /* @__PURE__ */ new Set();
|
|
474
|
+
const violations = [];
|
|
475
|
+
let skipThrough = -1;
|
|
476
|
+
lines.forEach((line, index) => {
|
|
477
|
+
const match = index > skipThrough ? SCRIPT_KEY.exec(line) : null;
|
|
478
|
+
if (match === null) return;
|
|
479
|
+
const keyColumn = (match[1]?.length ?? 0) + (match[2]?.length ?? 0);
|
|
480
|
+
const key = match[3] ?? "run";
|
|
481
|
+
if (key === "script") {
|
|
482
|
+
const parent = parentOf(lines, index, keyColumn);
|
|
483
|
+
if (!/^\s*with:\s*(?:#.*)?$/u.test(lines[parent] ?? "")) return;
|
|
484
|
+
if (!isGithubScriptStep(lines, parent)) return;
|
|
485
|
+
}
|
|
486
|
+
const where = key === "run" ? "`run:` script" : "`actions/github-script` `script:`";
|
|
487
|
+
const script = scriptLines(lines, index, keyColumn);
|
|
488
|
+
skipThrough = (script[script.length - 1]?.line ?? index + 1) - 1;
|
|
489
|
+
for (const entry of script) {
|
|
490
|
+
for (const expression of entry.text.matchAll(EXPRESSION)) {
|
|
491
|
+
const contexts = taintedContexts(
|
|
492
|
+
expression[1] ?? "",
|
|
493
|
+
checkInputs,
|
|
494
|
+
checkStepOutputs,
|
|
495
|
+
safeInputs
|
|
496
|
+
);
|
|
497
|
+
if (contexts.length === 0) continue;
|
|
498
|
+
violations.push({
|
|
499
|
+
file,
|
|
500
|
+
rule: RULE_ID3,
|
|
501
|
+
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.`,
|
|
502
|
+
line: entry.line
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
return violations;
|
|
508
|
+
}
|
|
509
|
+
function createGithubActionsNoTemplateInjectionRule(options = {}) {
|
|
510
|
+
const workflowGlobs = options.workflowGlobs ?? DEFAULT_WORKFLOW_GLOBS;
|
|
511
|
+
const actionGlobs = options.actionGlobs ?? DEFAULT_ACTION_GLOBS;
|
|
512
|
+
const skipDirs = options.skipDirs ?? DEFAULT_SKIP_DIRS;
|
|
513
|
+
const ciCritical = options.ciCritical ?? true;
|
|
514
|
+
return {
|
|
515
|
+
id: RULE_ID3,
|
|
516
|
+
category: "ci",
|
|
517
|
+
ciCritical,
|
|
518
|
+
description: "GitHub Actions `run:` and github-script bodies never expand attacker-controllable `${{ }}` context (issue/PR titles, comments, branch names); pass it through `env:` instead.",
|
|
519
|
+
run(ctx) {
|
|
520
|
+
const files = globFiles(
|
|
521
|
+
(p) => ctx.glob(p),
|
|
522
|
+
[...workflowGlobs, ...actionGlobs],
|
|
523
|
+
skipDirs
|
|
524
|
+
);
|
|
525
|
+
return files.flatMap(
|
|
526
|
+
(file) => checkWorkflowTemplateInjection(file, ctx.read(file) ?? "", options)
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
247
532
|
// src/rules/github-actions-runner-pinned.ts
|
|
248
|
-
var
|
|
533
|
+
var RULE_ID4 = "github-actions-runner-pinned";
|
|
249
534
|
var RUNS_ON = /^(\s*)runs-on:\s*(.*)$/u;
|
|
250
535
|
var DEFAULT_FLOATING_LABEL = /^[\w.-]+-latest$/u;
|
|
251
536
|
function runnerLabels(lines, start, indent) {
|
|
@@ -277,7 +562,7 @@ function checkWorkflowRunnersPinned(file, text, floatingLabel = DEFAULT_FLOATING
|
|
|
277
562
|
if (floatingLabel.test(label)) {
|
|
278
563
|
violations.push({
|
|
279
564
|
file,
|
|
280
|
-
rule:
|
|
565
|
+
rule: RULE_ID4,
|
|
281
566
|
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.`,
|
|
282
567
|
line: index + 1
|
|
283
568
|
});
|
|
@@ -291,7 +576,7 @@ function createGithubActionsRunnerPinnedRule(options = {}) {
|
|
|
291
576
|
const floatingLabel = options.floatingLabel ?? DEFAULT_FLOATING_LABEL;
|
|
292
577
|
const ciCritical = options.ciCritical ?? true;
|
|
293
578
|
return {
|
|
294
|
-
id:
|
|
579
|
+
id: RULE_ID4,
|
|
295
580
|
category: "ci",
|
|
296
581
|
ciCritical,
|
|
297
582
|
description: "GitHub Actions jobs must run on a pinned runner image (for example ubuntu-24.04), never a *-latest label.",
|
|
@@ -304,7 +589,7 @@ function createGithubActionsRunnerPinnedRule(options = {}) {
|
|
|
304
589
|
}
|
|
305
590
|
|
|
306
591
|
// src/rules/github-actions-sha-pinned.ts
|
|
307
|
-
var
|
|
592
|
+
var RULE_ID5 = "github-actions-sha-pinned";
|
|
308
593
|
var USES_LINE = /^\s*(?:-\s+)?uses:\s*['"]?([^\s'"#]+)['"]?\s*(#.*)?$/u;
|
|
309
594
|
var FULL_SHA = /^[0-9a-f]{40}$/u;
|
|
310
595
|
var DOCKER_DIGEST = /@sha256:[0-9a-f]{64}$/u;
|
|
@@ -319,7 +604,7 @@ function checkWorkflowActionsPinned(file, text) {
|
|
|
319
604
|
}
|
|
320
605
|
const where = `line ${index + 1}`;
|
|
321
606
|
const report = (message) => {
|
|
322
|
-
violations.push({ file, rule:
|
|
607
|
+
violations.push({ file, rule: RULE_ID5, message, line: index + 1 });
|
|
323
608
|
};
|
|
324
609
|
if (ref.startsWith("docker://")) {
|
|
325
610
|
if (!DOCKER_DIGEST.test(ref)) {
|
|
@@ -347,7 +632,7 @@ function createGithubActionsShaPinnedRule(options = {}) {
|
|
|
347
632
|
const workflowGlobs = options.workflowGlobs ?? DEFAULT_WORKFLOW_GLOBS;
|
|
348
633
|
const ciCritical = options.ciCritical ?? true;
|
|
349
634
|
return {
|
|
350
|
-
id:
|
|
635
|
+
id: RULE_ID5,
|
|
351
636
|
category: "ci",
|
|
352
637
|
ciCritical,
|
|
353
638
|
description: "GitHub Actions `uses:` refs must be pinned to a 40-character commit SHA with a `# vN` comment (local ./ actions exempt).",
|
|
@@ -572,7 +857,7 @@ function createPackageShapeRule(options = {}) {
|
|
|
572
857
|
}
|
|
573
858
|
|
|
574
859
|
// src/rules/security-scanner-version-parity.ts
|
|
575
|
-
var
|
|
860
|
+
var RULE_ID6 = "security-scanner-version-parity";
|
|
576
861
|
function createSecurityScannerVersionParityRule(options = {}) {
|
|
577
862
|
const scanner = options.scanner ?? "gitleaks";
|
|
578
863
|
const versionVariable = options.versionVariable ?? "GITLEAKS_VERSION";
|
|
@@ -597,7 +882,7 @@ function createSecurityScannerVersionParityRule(options = {}) {
|
|
|
597
882
|
const runtimeCheck = new RegExp(`\\b${name}\\s+version\\b`, "u");
|
|
598
883
|
const mentionsScanner = new RegExp(name, "iu");
|
|
599
884
|
return {
|
|
600
|
-
id:
|
|
885
|
+
id: RULE_ID6,
|
|
601
886
|
category: "ci",
|
|
602
887
|
ciCritical,
|
|
603
888
|
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.`,
|
|
@@ -616,7 +901,7 @@ function createSecurityScannerVersionParityRule(options = {}) {
|
|
|
616
901
|
}
|
|
617
902
|
const violations = [];
|
|
618
903
|
const report = (file, message) => {
|
|
619
|
-
violations.push({ file, rule:
|
|
904
|
+
violations.push({ file, rule: RULE_ID6, message });
|
|
620
905
|
};
|
|
621
906
|
if (ciVersions.size > 1) {
|
|
622
907
|
report(
|
|
@@ -669,7 +954,7 @@ function createSecurityScannerVersionParityRule(options = {}) {
|
|
|
669
954
|
}
|
|
670
955
|
|
|
671
956
|
// src/rules/service-image-digest-pin.ts
|
|
672
|
-
var
|
|
957
|
+
var RULE_ID7 = "service-image-digest-pin";
|
|
673
958
|
var DIGEST2 = /@sha256:[0-9a-f]{64}$/u;
|
|
674
959
|
var KEY_LINE = /^(\s*)(?:-\s+)?([\w-]+):\s*(.*)$/u;
|
|
675
960
|
var DEFAULT_COMPOSE_GLOBS = anywhereGlobs(
|
|
@@ -677,7 +962,7 @@ var DEFAULT_COMPOSE_GLOBS = anywhereGlobs(
|
|
|
677
962
|
(stem) => ["", ".*", "-*"].flatMap((infix) => ["yml", "yaml"].map((ext) => `${stem}${infix}.${ext}`))
|
|
678
963
|
)
|
|
679
964
|
);
|
|
680
|
-
function
|
|
965
|
+
function indentOf3(line) {
|
|
681
966
|
return /^\s*/u.exec(line)?.[0].length ?? 0;
|
|
682
967
|
}
|
|
683
968
|
function workflowImages(text) {
|
|
@@ -729,7 +1014,7 @@ function composeImages(text) {
|
|
|
729
1014
|
if (line.trim() === "" || line.trimStart().startsWith("#")) {
|
|
730
1015
|
continue;
|
|
731
1016
|
}
|
|
732
|
-
const indent =
|
|
1017
|
+
const indent = indentOf3(line);
|
|
733
1018
|
if (indent === 0) {
|
|
734
1019
|
break;
|
|
735
1020
|
}
|
|
@@ -763,7 +1048,7 @@ function violationsFor(file, images, allowUnpinned) {
|
|
|
763
1048
|
const reference = image.split("@")[0];
|
|
764
1049
|
return {
|
|
765
1050
|
file,
|
|
766
|
-
rule:
|
|
1051
|
+
rule: RULE_ID7,
|
|
767
1052
|
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.`,
|
|
768
1053
|
line
|
|
769
1054
|
};
|
|
@@ -776,7 +1061,7 @@ function createServiceImageDigestPinRule(options = {}) {
|
|
|
776
1061
|
const allowUnpinned = new Set(options.allowUnpinned ?? []);
|
|
777
1062
|
const ciCritical = options.ciCritical ?? true;
|
|
778
1063
|
return {
|
|
779
|
-
id:
|
|
1064
|
+
id: RULE_ID7,
|
|
780
1065
|
category: "ci",
|
|
781
1066
|
ciCritical,
|
|
782
1067
|
description: "Workflow service and container images, and docker-compose images, must be pinned by `@sha256:` digest (a service that builds locally is exempt).",
|
|
@@ -1061,6 +1346,8 @@ var RULE_FACTORIES = {
|
|
|
1061
1346
|
"canonical-helpers-single-home": createCanonicalHelpersSingleHomeRule,
|
|
1062
1347
|
"dockerfile-base-image-digest-pin": createDockerfileBaseImageDigestPinRule,
|
|
1063
1348
|
"file-size-ratchet": createFileSizeRatchetRule,
|
|
1349
|
+
"github-actions-least-privilege-permissions": createGithubActionsLeastPrivilegePermissionsRule,
|
|
1350
|
+
"github-actions-no-template-injection": createGithubActionsNoTemplateInjectionRule,
|
|
1064
1351
|
"github-actions-runner-pinned": createGithubActionsRunnerPinnedRule,
|
|
1065
1352
|
"github-actions-sha-pinned": createGithubActionsShaPinnedRule,
|
|
1066
1353
|
"layer-rank": createLayerRankRule,
|
|
@@ -1088,6 +1375,8 @@ export {
|
|
|
1088
1375
|
createCanonicalHelpersSingleHomeRule,
|
|
1089
1376
|
createDockerfileBaseImageDigestPinRule,
|
|
1090
1377
|
createFileSizeRatchetRule,
|
|
1378
|
+
createGithubActionsLeastPrivilegePermissionsRule,
|
|
1379
|
+
createGithubActionsNoTemplateInjectionRule,
|
|
1091
1380
|
createGithubActionsRunnerPinnedRule,
|
|
1092
1381
|
createGithubActionsShaPinnedRule,
|
|
1093
1382
|
createLayerRankRule,
|
package/dist/prisma.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
globFiles
|
|
3
|
-
} from "./chunk-Z7TXSZR4.js";
|
|
4
1
|
import {
|
|
5
2
|
firstOptionOf,
|
|
6
3
|
resolveRules,
|
|
7
4
|
severityOf
|
|
8
5
|
} from "./chunk-VFCX3QKZ.js";
|
|
6
|
+
import {
|
|
7
|
+
globFiles
|
|
8
|
+
} from "./chunk-OYFQKSJN.js";
|
|
9
9
|
|
|
10
10
|
// src/prisma/prisma-method-surface.ts
|
|
11
11
|
import { PRISMA_READ_METHODS, PRISMA_WRITE_METHODS } from "@noctcore/eslint-plugin-prisma";
|