mandrel-platform 1.1.0 → 1.3.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 +69 -12
- package/config/stryker.base.json +7 -2
- package/package.json +1 -1
- package/scripts/audit-check.mjs +331 -6
- package/scripts/audit-check.test.mjs +382 -1
- package/scripts/check-action-pins.mjs +87 -15
- package/scripts/check-action-pins.test.mjs +103 -4
- package/scripts/check-affected-mode.test.mjs +5 -51
- package/scripts/check-codeql-gating.test.mjs +649 -0
- package/scripts/check-destructive-migration.mjs +277 -11
- package/scripts/check-destructive-migration.test.mjs +334 -0
- package/scripts/check-environments-isolation-audit.test.mjs +212 -0
- package/scripts/check-fail-fast-attribution.test.mjs +90 -4
- package/scripts/check-first-party-pin-freshness.mjs +648 -0
- package/scripts/check-first-party-pin-freshness.test.mjs +624 -0
- package/scripts/check-gitleaks-allowlist.test.mjs +312 -0
- package/scripts/check-osv-scan-mode.test.mjs +5 -50
- package/scripts/check-release-type.mjs +591 -0
- package/scripts/check-release-type.test.mjs +678 -0
- package/scripts/check-setup-toolchain-store.test.mjs +139 -0
- package/scripts/check-toolchain-cache-default.test.mjs +308 -0
- package/scripts/lib/yaml-step.mjs +109 -0
- package/scripts/lib/yaml-step.test.mjs +156 -0
- package/scripts/osv-report-gate.test.mjs +289 -0
- package/scripts/runner-env-drift.test.mjs +554 -0
- package/scripts/stryker-base-config.test.mjs +256 -0
- package/templates/runbooks/runner-provisioning.md +50 -6
- package/templates/runner/check-runner-env-drift.sh +248 -0
|
@@ -10,6 +10,12 @@
|
|
|
10
10
|
* a full content scan with both a SHA-pinned (pass) and a tag-pinned (fail)
|
|
11
11
|
* fixture. Pure helpers + a temp-dir fixture keep the whole pipeline offline.
|
|
12
12
|
*
|
|
13
|
+
* The first-party ratchet block (Story #354 audit) covers what used to be a
|
|
14
|
+
* three-way blind spot: a self-reference on a moving ref passed this lint (as
|
|
15
|
+
* exempt), the portability lint's Rule 3 (which skips non-SHA refs), and
|
|
16
|
+
* check-first-party-pin-freshness.mjs (which files it under a non-failing
|
|
17
|
+
* informational note) all at once.
|
|
18
|
+
*
|
|
13
19
|
* Run: node scripts/check-action-pins.test.mjs (or `node --test scripts/`)
|
|
14
20
|
*/
|
|
15
21
|
|
|
@@ -138,7 +144,7 @@ test("scanContent fails a tag-pinned third-party uses", () => {
|
|
|
138
144
|
assert.match(violations[0].reason, /not a full 40-char commit SHA/);
|
|
139
145
|
});
|
|
140
146
|
|
|
141
|
-
test("scanContent ignores
|
|
147
|
+
test("scanContent ignores local, docker, and comment lines", () => {
|
|
142
148
|
const yaml = [
|
|
143
149
|
"# uses: actions/checkout@v4 (this is a comment example, must be ignored)",
|
|
144
150
|
" steps:",
|
|
@@ -146,11 +152,102 @@ test("scanContent ignores first-party, local, docker, and comment lines", () =>
|
|
|
146
152
|
" - uses: ./.github/actions/local-thing",
|
|
147
153
|
" - uses: docker://alpine:3.19",
|
|
148
154
|
].join("\n");
|
|
149
|
-
const
|
|
150
|
-
assert.equal(scanned, 0); // none are third-party
|
|
155
|
+
const res = scanContent(yaml, "wf.yml");
|
|
156
|
+
assert.equal(res.scanned, 0); // none are third-party
|
|
157
|
+
assert.deepEqual(res.violations, []);
|
|
158
|
+
// The first-party ref IS counted now, and passes because it is SHA-pinned.
|
|
159
|
+
// A local `./path` and a `docker://` ref remain exempt from both classes.
|
|
160
|
+
assert.equal(res.firstPartyScanned, 1);
|
|
161
|
+
assert.deepEqual(res.firstPartyViolations, []);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// First-party SHA ratchet (Story #354 audit)
|
|
166
|
+
//
|
|
167
|
+
// A first-party self-reference on a moving ref used to be green on all three
|
|
168
|
+
// first-party guards at once: this ratchet skipped it as exempt, the
|
|
169
|
+
// portability lint's Rule 3 skips any ref that is not already a 40-hex SHA,
|
|
170
|
+
// and check-first-party-pin-freshness.mjs files it under an informational
|
|
171
|
+
// note that never fails. These tests pin the hole shut.
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
test("scanContent flags a branch-pinned first-party self-reference", () => {
|
|
175
|
+
const yaml = " - uses: dsj1984/mandrel-platform/.github/actions/gitleaks-scan@main";
|
|
176
|
+
const { firstPartyViolations, firstPartyScanned, violations } = scanContent(yaml, "wf.yml");
|
|
177
|
+
assert.equal(firstPartyScanned, 1);
|
|
178
|
+
assert.equal(firstPartyViolations.length, 1);
|
|
179
|
+
assert.equal(firstPartyViolations[0].line, 1);
|
|
180
|
+
assert.equal(firstPartyViolations[0].owner, "dsj1984/mandrel-platform");
|
|
181
|
+
assert.match(firstPartyViolations[0].reason, /not a full 40-char commit SHA/);
|
|
182
|
+
// Never misfiled as a third-party violation — the remediation differs.
|
|
151
183
|
assert.deepEqual(violations, []);
|
|
152
184
|
});
|
|
153
185
|
|
|
186
|
+
test("scanContent flags a tag-pinned and a short-SHA first-party self-reference", () => {
|
|
187
|
+
const yaml = [
|
|
188
|
+
" - uses: dsj1984/mandrel-platform/.github/workflows/pr-quality.yml@v1.1.0",
|
|
189
|
+
` - uses: dsj1984/mandrel-platform/.github/actions/osv-scan@${SHORT}`,
|
|
190
|
+
].join("\n");
|
|
191
|
+
const { firstPartyViolations } = scanContent(yaml, "wf.yml");
|
|
192
|
+
assert.equal(firstPartyViolations.length, 2);
|
|
193
|
+
assert.deepEqual(
|
|
194
|
+
firstPartyViolations.map((v) => v.line),
|
|
195
|
+
[1, 2],
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("scanContent ratchets a bare first-party self-reference carrying no subpath", () => {
|
|
200
|
+
// No subpath means no manifest for the freshness checker to resolve, but the
|
|
201
|
+
// ref still moves — so the ratchet must still apply.
|
|
202
|
+
const yaml = " - uses: dsj1984/mandrel-platform@main";
|
|
203
|
+
const { firstPartyViolations, firstPartyScanned } = scanContent(yaml, "wf.yml");
|
|
204
|
+
assert.equal(firstPartyScanned, 1);
|
|
205
|
+
assert.equal(firstPartyViolations.length, 1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("scanContent honours --first-party-owner when ratcheting a fork's self-refs", () => {
|
|
209
|
+
const yaml = " - uses: my-org/my-fork/.github/actions/thing@main";
|
|
210
|
+
const asFirstParty = scanContent(yaml, "wf.yml", "my-org/my-fork");
|
|
211
|
+
assert.equal(asFirstParty.firstPartyViolations.length, 1);
|
|
212
|
+
assert.deepEqual(asFirstParty.violations, []);
|
|
213
|
+
|
|
214
|
+
// Under the default owner the SAME line is a third-party violation instead —
|
|
215
|
+
// still caught, just under the other class.
|
|
216
|
+
const asThirdParty = scanContent(yaml, "wf.yml");
|
|
217
|
+
assert.equal(asThirdParty.violations.length, 1);
|
|
218
|
+
assert.deepEqual(asThirdParty.firstPartyViolations, []);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("runLint / runCli are red on a branch-pinned first-party self-reference", () => {
|
|
222
|
+
const root = fixtureRepo({
|
|
223
|
+
workflow: [
|
|
224
|
+
" steps:",
|
|
225
|
+
` - uses: actions/checkout@${SHA} # v4`,
|
|
226
|
+
" - uses: dsj1984/mandrel-platform/.github/actions/gitleaks-scan@main",
|
|
227
|
+
].join("\n"),
|
|
228
|
+
});
|
|
229
|
+
try {
|
|
230
|
+
const res = runLint({
|
|
231
|
+
cwd: root,
|
|
232
|
+
workflowsDir: ".github/workflows",
|
|
233
|
+
actionsDir: ".github/actions",
|
|
234
|
+
firstPartyOwner: "dsj1984/mandrel-platform",
|
|
235
|
+
});
|
|
236
|
+
assert.equal(res.ok, false);
|
|
237
|
+
assert.equal(res.firstPartyViolations.length, 1);
|
|
238
|
+
assert.deepEqual(res.violations, []); // the third-party pin is fine
|
|
239
|
+
|
|
240
|
+
const errs = [];
|
|
241
|
+
const code = runCli(["--cwd", root], { log: () => {}, err: (m) => errs.push(m) });
|
|
242
|
+
assert.equal(code, 1);
|
|
243
|
+
assert.ok(errs.some((m) => /first-party self-reference\(s\) pinned to a moving ref/.test(m)));
|
|
244
|
+
// The remedy names the blast radius, not just the rule.
|
|
245
|
+
assert.ok(errs.some((m) => /inherited by every consumer/.test(m)));
|
|
246
|
+
} finally {
|
|
247
|
+
rmSync(root, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
154
251
|
test("scanContent flags a short-SHA third-party pin", () => {
|
|
155
252
|
const yaml = ` - uses: actions/setup-node@${SHORT}`;
|
|
156
253
|
const { violations } = scanContent(yaml, "wf.yml");
|
|
@@ -205,7 +302,9 @@ test("runLint is green when every third-party action is SHA-pinned", () => {
|
|
|
205
302
|
firstPartyOwner: "dsj1984/mandrel-platform",
|
|
206
303
|
});
|
|
207
304
|
assert.equal(res.ok, true);
|
|
208
|
-
assert.equal(res.scanned, 1); // only the third-party checkout counts
|
|
305
|
+
assert.equal(res.scanned, 1); // only the third-party checkout counts here
|
|
306
|
+
assert.equal(res.firstPartyScanned, 1); // the self-reference is counted separately
|
|
307
|
+
assert.deepEqual(res.firstPartyViolations, []);
|
|
209
308
|
} finally {
|
|
210
309
|
rmSync(root, { recursive: true, force: true });
|
|
211
310
|
}
|
|
@@ -40,45 +40,18 @@ import { join, resolve, dirname } from "node:path";
|
|
|
40
40
|
import { fileURLToPath } from "node:url";
|
|
41
41
|
import { tmpdir } from "node:os";
|
|
42
42
|
|
|
43
|
+
import { stepByName, runScript } from "./lib/yaml-step.mjs";
|
|
44
|
+
|
|
43
45
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
44
46
|
const WORKFLOW = ".github/workflows/pr-quality.yml";
|
|
45
47
|
const content = readFileSync(join(repoRoot, WORKFLOW), "utf8");
|
|
46
48
|
|
|
47
49
|
// ---------------------------------------------------------------------------
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
50
|
+
// Local extraction helper. Step-block and `run:` extraction are shared via
|
|
51
|
+
// scripts/lib/yaml-step.mjs; only the `if:` reader is specific to this suite,
|
|
52
|
+
// which is the only one asserting on step gating.
|
|
51
53
|
// ---------------------------------------------------------------------------
|
|
52
54
|
|
|
53
|
-
function stepByName(text, name) {
|
|
54
|
-
const lines = text.split("\n");
|
|
55
|
-
const nameIdx = lines.findIndex((l) => /^\s+(- )?name:\s/.test(l) && l.includes(name));
|
|
56
|
-
assert.notEqual(nameIdx, -1, `step "${name}" not found`);
|
|
57
|
-
let start = -1;
|
|
58
|
-
for (let i = nameIdx; i >= 0; i--) {
|
|
59
|
-
if (/^\s*-\s/.test(lines[i])) {
|
|
60
|
-
start = i;
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
assert.notEqual(start, -1, `opening bullet for step "${name}" not found`);
|
|
65
|
-
const bulletIndent = lines[start].match(/^(\s*)/)[1].length;
|
|
66
|
-
let end = lines.length;
|
|
67
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
68
|
-
if (/^\s*$/.test(lines[i])) continue;
|
|
69
|
-
const indent = lines[i].match(/^(\s*)/)[1].length;
|
|
70
|
-
if (indent < bulletIndent) {
|
|
71
|
-
end = i;
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
if (indent === bulletIndent && /^\s*-\s/.test(lines[i])) {
|
|
75
|
-
end = i;
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return lines.slice(start, end).join("\n");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
55
|
/** The (first) `if:` expression on a step block, trimmed. */
|
|
83
56
|
function ifCondition(stepBlock) {
|
|
84
57
|
const m = stepBlock.match(/^\s+if:\s*(.+?)\s*$/m);
|
|
@@ -86,25 +59,6 @@ function ifCondition(stepBlock) {
|
|
|
86
59
|
return m[1].trim();
|
|
87
60
|
}
|
|
88
61
|
|
|
89
|
-
/** The dedented body of the step's `run: |` block scalar. */
|
|
90
|
-
function runScript(stepBlock) {
|
|
91
|
-
const lines = stepBlock.split("\n");
|
|
92
|
-
const start = lines.findIndex((l) => /^\s+run:\s*\|\s*$/.test(l));
|
|
93
|
-
assert.notEqual(start, -1, "`run: |` block not found");
|
|
94
|
-
const runIndent = lines[start].match(/^(\s*)/)[1].length;
|
|
95
|
-
const body = [];
|
|
96
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
97
|
-
if (/^\s*$/.test(lines[i])) {
|
|
98
|
-
body.push("");
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const indent = lines[i].match(/^(\s*)/)[1].length;
|
|
102
|
-
if (indent <= runIndent) break;
|
|
103
|
-
body.push(lines[i].slice(runIndent + 2));
|
|
104
|
-
}
|
|
105
|
-
return body.join("\n");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
62
|
// ---------------------------------------------------------------------------
|
|
109
63
|
// 1. GATING — coverage-gate steps gated OUT of affected mode; bypass step gated IN
|
|
110
64
|
// ---------------------------------------------------------------------------
|