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.
- package/dist/cli/index.js +56 -15
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
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) ?
|
|
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.
|
|
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",
|