tamperward 2.25.1 → 2.25.2

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.
Files changed (2) hide show
  1. package/dist/cli/index.js +56 -15
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -526,7 +526,22 @@ function defaultPolicy(version = 1) {
526
526
  "**/codecov.yml",
527
527
  "**/.codecov.yml"
528
528
  ],
529
- ci: [".github/workflows/**"],
529
+ // A workflow's checks can live in a composite action it `uses:` (the workflow
530
+ // keeps `uses: ./.github/actions/test` while the action's `run: npm test`
531
+ // becomes `echo ok`), and in the entry file of a CI system that is not GitHub
532
+ // Actions at all. Each is read by ci-tampering: the action's `runs.steps` get
533
+ // the workflow's removal/neutralisation pass, the other systems' entry files
534
+ // the generic check-line pass and no trigger logic (issue #437).
535
+ ci: [
536
+ ".github/workflows/**",
537
+ ".github/actions/**/action.y?(a)ml",
538
+ ".gitlab-ci.yml",
539
+ ".circleci/config.yml",
540
+ "Jenkinsfile",
541
+ "azure-pipelines.yml",
542
+ "bitbucket-pipelines.yml",
543
+ ".travis.yml"
544
+ ],
530
545
  // Recorded expected outputs. An assertion stored as data is still an assertion;
531
546
  // rewriting it from current output is the snapshot-update move the affordance
532
547
  // experiment measured at a 70% attempt / 100% through rate (snapshot-rewrite).
@@ -5951,6 +5966,8 @@ function survives(removedCore, isUses, afterCores, addedCores) {
5951
5966
  if (w && (canonical(w[1]) === r || canonical(w[1]).startsWith(r + " "))) neutralised ??= raw;
5952
5967
  }
5953
5968
  }
5969
+ const maskOf = (args) => (" " + args).match(NEUTRALISING_SUFFIX)?.[0]?.trim() ?? null;
5970
+ const priorMask = r.includes(" ") ? maskOf(r.slice(r.indexOf(" ") + 1)) : null;
5954
5971
  let respelled = false;
5955
5972
  for (const raw of addedCores) {
5956
5973
  if (!kind || checkKind(raw) !== kind) continue;
@@ -5965,7 +5982,9 @@ function survives(removedCore, isUses, afterCores, addedCores) {
5965
5982
  continue;
5966
5983
  }
5967
5984
  const args = a.slice(a.indexOf(" ") + 1 || a.length);
5968
- if (a.includes(" ") && (NEUTRALISING_SUFFIX.test(" " + args) || narrowed(args) || runnerNarrowed(a, args))) neutralised ??= raw;
5985
+ const mask = maskOf(args);
5986
+ const newMask = mask !== null && mask !== priorMask;
5987
+ if (a.includes(" ") && (newMask || narrowed(args) || runnerNarrowed(a, args))) neutralised ??= raw;
5969
5988
  else respelled = true;
5970
5989
  }
5971
5990
  if (respelled) return { state: "kept" };
@@ -6189,7 +6208,7 @@ function isActiveWorkflow(path) {
6189
6208
  return /(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(path);
6190
6209
  }
6191
6210
  function commandCore(line) {
6192
- return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses):\s*/, "").replace(/\s+/g, " ").trim();
6211
+ return line.trim().replace(/^-\s*/, "").replace(/^(?:run|uses|script|command|before_script|after_script|sh|bash|pwsh|powershell):\s*/, "").replace(/\s+/g, " ").trim();
6193
6212
  }
6194
6213
  function expressionOf(raw) {
6195
6214
  const v = uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim();
@@ -6405,6 +6424,12 @@ function triggerNarrowings(before, after, opts) {
6405
6424
  }
6406
6425
  return out3;
6407
6426
  }
6427
+ function localUses(line) {
6428
+ const wf = line.match(REUSED_WORKFLOW)?.[1];
6429
+ if (wf) return [wf];
6430
+ const action = line.match(LOCAL_ACTION)?.[1];
6431
+ return action ? [`${action}/action.yml`, `${action}/action.yaml`] : [];
6432
+ }
6408
6433
  function literalValue(raw) {
6409
6434
  const v = foldExpressions(uncommented(raw).replace(/^(['"])(.*)\1$/, "$2").trim());
6410
6435
  return v.includes(EXPR_TOKEN) || v.includes(MATRIX_TOKEN) ? null : v;
@@ -6439,7 +6464,7 @@ function ancestorOf(lines, i) {
6439
6464
  while (h >= 0 && (lines[h].trim() === "" || /^\s*#/.test(lines[h]) || indentOf(lines[h]) >= ind)) h--;
6440
6465
  return h >= 0 ? h : null;
6441
6466
  }
6442
- var RULE7, USES, CHECK, YAML_KEY, invokesCheck2, uncommented, SHELL_ERREXIT, indentOf, STEP_START, REPORTER_ACTION, usesCheck, stepHasCheck, jobHasCheck, NEUTRALISERS, LIST_KEYS, DEFAULT_BRANCH, ALL, CODE_EVENTS, PR_CODE_TYPES, pathsMatchSource, ignoresEverySource, CODE_PROBES, REUSED_WORKFLOW, MANIFEST, WORKDIR_LINE, REF_LINE, CHECKOUT, ciTampering;
6467
+ var RULE7, USES, CHECK, YAML_KEY, isCompositeAction, isGithubFile, GH_RUN_KEY, GENERIC_RUN_KEY, invokesCheck2, uncommented, SHELL_ERREXIT, indentOf, STEP_START, REPORTER_ACTION, usesCheck, stepHasCheck, jobHasCheck, NEUTRALISERS, LIST_KEYS, DEFAULT_BRANCH, ALL, CODE_EVENTS, PR_CODE_TYPES, pathsMatchSource, ignoresEverySource, CODE_PROBES, REUSED_WORKFLOW, LOCAL_ACTION, MANIFEST, WORKDIR_LINE, REF_LINE, CHECKOUT, ciTampering;
6443
6468
  var init_ci_tampering = __esm({
6444
6469
  "src/detectors/ci-tampering.ts"() {
6445
6470
  "use strict";
@@ -6456,6 +6481,10 @@ var init_ci_tampering = __esm({
6456
6481
  USES = /^\s*-\s*uses:/;
6457
6482
  CHECK = /\b(test|tests|lint|typecheck|type-check|tsc|eslint|jest|vitest|playwright|coverage|tamperward)\b/i;
6458
6483
  YAML_KEY = /^\s*-?\s*[A-Za-z_][\w-]*:\s*(?:$|\S)/;
6484
+ isCompositeAction = (path) => /(^|\/)\.github\/actions\/.+\/action\.ya?ml$/.test(path);
6485
+ isGithubFile = (path) => /(^|\/)\.github\//.test(path);
6486
+ GH_RUN_KEY = /^\s*-?\s*run:/;
6487
+ GENERIC_RUN_KEY = /^\s*-?\s*(?:run|script|command|before_script|after_script|sh|bash|pwsh|powershell):/;
6459
6488
  invokesCheck2 = (line) => invokesCheck(line) || INVOKES_TOOL.test(commandCore(line));
6460
6489
  uncommented = (v) => v.replace(/\s+#.*$/, "").trim();
6461
6490
  SHELL_ERREXIT = /(?:^|\s)-[a-zA-Z]*e[a-zA-Z]*(?:\s|$)|-o\s+errexit\b/;
@@ -6486,7 +6515,8 @@ var init_ci_tampering = __esm({
6486
6515
  pathsMatchSource = (globs4, sources) => sources.some((s) => admits(globs4, s));
6487
6516
  ignoresEverySource = (globs4, sources) => sources.length > 0 && sources.every((s) => admits(globs4, s));
6488
6517
  CODE_PROBES = ["src/index.ts", "src/index.js", "lib/index.js", "index.ts", "main.go", "src/main.py", "src/lib.rs", "packages/a/src/index.ts", "test/a.test.ts"];
6489
- REUSED_WORKFLOW = /^\s*uses:\s*\.\/(\.github\/workflows\/[^\s@]+)/;
6518
+ REUSED_WORKFLOW = /^\s*-?\s*uses:\s*\.\/(\.github\/workflows\/[^\s@]+)/;
6519
+ LOCAL_ACTION = /^\s*-?\s*uses:\s*\.\/(\.github\/actions\/[^\s@]+?)\/?\s*(?:#.*)?$/;
6490
6520
  MANIFEST = /(?:^|\/)(?:package\.json|pyproject\.toml|setup\.py|setup\.cfg|Cargo\.toml|go\.mod|Makefile|build\.gradle(?:\.kts)?|pom\.xml|Gemfile|deno\.jsonc?)$/;
6491
6521
  WORKDIR_LINE = /^\s*-?\s*working-directory:\s*(.+?)\s*$/;
6492
6522
  REF_LINE = /^\s*ref:\s*(.+?)\s*$/;
@@ -6516,7 +6546,8 @@ var init_ci_tampering = __esm({
6516
6546
  if (!isProtected(c.path, policy, "ci")) continue;
6517
6547
  const afterLines = c.after != null ? c.after.split("\n") : null;
6518
6548
  const addedAt = new Set(addedLines(c).map((l) => l.newLine).filter((n) => n != null));
6519
- for (const l of addedLines(c)) {
6549
+ const github = isGithubFile(c.path);
6550
+ for (const l of github ? addedLines(c) : []) {
6520
6551
  const coe = l.content.match(/^\s*-?\s*continue-on-error:\s*(.+?)\s*$/);
6521
6552
  const cond = l.content.match(/^\s*-?\s*if:\s*(.+?)\s*$/);
6522
6553
  if (!coe && !cond) continue;
@@ -6548,7 +6579,7 @@ var init_ci_tampering = __esm({
6548
6579
  );
6549
6580
  }
6550
6581
  }
6551
- if (afterLines) {
6582
+ if (afterLines && github) {
6552
6583
  for (const b of runBlocks(afterLines)) {
6553
6584
  if (!b.body.some(invokesCheck2)) continue;
6554
6585
  const propagates = b.body.some((l) => /\bexit\s+"?\$/.test(l));
@@ -6626,7 +6657,7 @@ var init_ci_tampering = __esm({
6626
6657
  })
6627
6658
  );
6628
6659
  }
6629
- if (c.before != null && c.after != null) {
6660
+ if (c.before != null && c.after != null && !isCompositeAction(c.path)) {
6630
6661
  triggerOpts ??= { defaultBranch: defaultBranch(ctx), sources: sourceProbes(ctx), hasBranch: (b) => branchExists(b, ctx) };
6631
6662
  for (const reason of triggerNarrowings(parseTriggers(c.before), parseTriggers(c.after), triggerOpts)) {
6632
6663
  out3.push(
@@ -6644,17 +6675,27 @@ var init_ci_tampering = __esm({
6644
6675
  const afterCores = (afterCommands ?? addedLines(c).map((l) => l.content)).map(commandCore);
6645
6676
  const addedCores = addedLines(c).map((l) => commandCore(afterCommands && l.newLine != null ? afterCommands[l.newLine - 1] : l.content));
6646
6677
  for (const l of afterLines ?? []) {
6647
- const reused = l.match(REUSED_WORKFLOW)?.[1];
6648
- const content = reused ? afterByPath.get(reused) : void 0;
6649
- if (content == null || reused === c.path) continue;
6650
- const cores = commandLines(content.split("\n")).map(commandCore);
6651
- afterCores.push(...cores);
6652
- addedCores.push(...cores);
6678
+ for (const reused of localUses(l)) {
6679
+ const content = afterByPath.get(reused);
6680
+ if (content == null || reused === c.path) continue;
6681
+ const cores = commandLines(content.split("\n")).map(commandCore);
6682
+ afterCores.push(...cores);
6683
+ addedCores.push(...cores);
6684
+ }
6685
+ }
6686
+ if (!github && c.after == null) {
6687
+ for (const o of changes) {
6688
+ if (o.kind !== "file" || o.op !== "add" || o.after == null || !isProtected(o.path, policy, "ci")) continue;
6689
+ const cores = commandLines(o.after.split("\n")).map(commandCore);
6690
+ afterCores.push(...cores);
6691
+ addedCores.push(...cores);
6692
+ }
6653
6693
  }
6694
+ const runKey = github ? GH_RUN_KEY : GENERIC_RUN_KEY;
6654
6695
  for (const l of removedLines(c)) {
6655
6696
  if (/^\s*#/.test(l.content)) continue;
6656
6697
  const isUsesLine = usesCheck(l.content);
6657
- const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? /^\s*-?\s*run:/.test(l.content) : true) && invokesCheck2(l.content);
6698
+ const isCheckCommand = !isUsesLine && !/^\s*-?\s*uses:/.test(l.content) && (YAML_KEY.test(l.content) ? runKey.test(l.content) : true) && invokesCheck2(l.content);
6658
6699
  if (!isUsesLine && !isCheckCommand) continue;
6659
6700
  const s = survives(commandCore(l.content), isUsesLine, afterCores, addedCores);
6660
6701
  if (s.state === "kept") continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tamperward",
3
- "version": "2.25.1",
3
+ "version": "2.25.2",
4
4
  "description": "The deterministic agent-integrity gate. One ruleset, evaluated on the actual diff/commands as a verdict, enforced everywhere a change can be made.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "hexrift",