Skip to content

Commit 1689d0a

Browse files
beagleknightfcsonline
authored andcommitted
Refactor how supply chain generation
1 parent ee5f0cb commit 1689d0a

File tree

4 files changed

+103
-116
lines changed

4 files changed

+103
-116
lines changed

src/__snapshots__/workflow.spec.ts.snap

+1-18
Original file line numberDiff line numberDiff line change
@@ -377,24 +377,7 @@ jobs:
377377
"
378378
`;
379379

380-
exports[`Workflow > supports supply chain attack, with writing the lock file 1`] = `
381-
"# Workflow automatically generated by gat
382-
# DO NOT CHANGE THIS FILE MANUALLY
383-
384-
name: Supply chain attack
385-
on:
386-
push: null
387-
jobs:
388-
job1:
389-
runs-on: ubuntu-22.04
390-
timeout-minutes: 15
391-
steps:
392-
- name: Do something
393-
uses: tj-actions/changed-files@a284dc1814e3fd07f2e34267fc8f81227ed29fb8
394-
"
395-
`;
396-
397-
exports[`Workflow > supports supply chain attack, without writing the lock file 1`] = `
380+
exports[`Workflow > supports supply chain attack 1`] = `
398381
"# Workflow automatically generated by gat
399382
# DO NOT CHANGE THIS FILE MANUALLY
400383

src/template.ts

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import fs from "fs";
2+
import { Octokit } from "@octokit/rest";
3+
4+
import { Workflow } from "./workflow";
5+
6+
interface CompileOptions {
7+
templates: Record<string, Workflow>;
8+
lockFilePath: string;
9+
writeLockFile: boolean;
10+
}
11+
12+
const createLockFile = async (
13+
templates: Record<string, Workflow>,
14+
lockFilePath: string,
15+
) => {
16+
const actions = Object.entries(templates).reduce<string[]>(
17+
(acc, [_filePath, template]) => {
18+
template.jobs.forEach((job) => {
19+
if ("steps" in job.options) {
20+
job.options.steps.forEach((step) => {
21+
if ("uses" in step) {
22+
acc.push(step.uses);
23+
}
24+
});
25+
}
26+
});
27+
28+
return acc;
29+
},
30+
[],
31+
);
32+
33+
const uniqueActions = [...new Set(actions)];
34+
35+
const octokit = process.env.GITHUB_TOKEN
36+
? new Octokit({
37+
auth: process.env.GITHUB_TOKEN,
38+
})
39+
: new Octokit();
40+
41+
const resolvedActions: Record<string, string> = {};
42+
43+
await Promise.all(
44+
uniqueActions.map(async (action) => {
45+
const match = action.match(/(?<repository>.*)@(?<version>.*)/);
46+
47+
if (!match) return;
48+
49+
const { repository, version } = match.groups as {
50+
repository: string;
51+
version: string;
52+
};
53+
54+
const [owner, repo] = repository.split("/");
55+
const response = await octokit.rest.repos.listTags({
56+
owner,
57+
repo,
58+
});
59+
60+
const tag = response.data.find((tag) => tag.name === version);
61+
62+
if (!tag) {
63+
throw new Error(`Unable to retrieve ${action} from Github tags`);
64+
}
65+
66+
resolvedActions[action] = `${repository}@${tag.commit.sha}`;
67+
})
68+
);
69+
70+
fs.writeFileSync(lockFilePath, JSON.stringify(resolvedActions, null, 2));
71+
};
72+
73+
export const compileTemplates = async (options: CompileOptions) => {
74+
const { templates, lockFilePath, writeLockFile } = options;
75+
76+
if (writeLockFile) {
77+
await createLockFile(templates, lockFilePath);
78+
}
79+
80+
const resolvedActions = fs.existsSync(lockFilePath)
81+
? JSON.parse(fs.readFileSync(lockFilePath, "utf8"))
82+
: {};
83+
84+
return Promise.all(
85+
Object.entries(templates).map(([filePath, template]) =>
86+
template.compile({ filePath, resolvedActions }),
87+
),
88+
);
89+
};

src/workflow.spec.ts

+4-31
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ exit 0`,
310310
expect(await workflow.compile()).toMatchSnapshot();
311311
});
312312

313-
it("supports supply chain attack, without writing the lock file", async () => {
313+
it("supports supply chain attack", async () => {
314314
const workflow = new Workflow("Supply chain attack");
315315

316316
workflow.on("push").addJob("job1", {
@@ -319,39 +319,12 @@ exit 0`,
319319
],
320320
});
321321

322-
fs.writeFileSync(
323-
"/tmp/lockfile1.json",
324-
JSON.stringify(
325-
{
322+
expect(
323+
await workflow.compile({
324+
resolvedActions: {
326325
"tj-actions/changed-files@v45.0.7":
327326
"tj-actions/changed-files@youhavebeenhacked",
328327
},
329-
null,
330-
2,
331-
),
332-
);
333-
334-
expect(
335-
await workflow.compile({
336-
lockFilePath: "/tmp/lockfile1.json",
337-
writeLockFile: false,
338-
}),
339-
).toMatchSnapshot();
340-
});
341-
342-
it("supports supply chain attack, with writing the lock file", async () => {
343-
const workflow = new Workflow("Supply chain attack");
344-
345-
workflow.on("push").addJob("job1", {
346-
steps: [
347-
{ name: "Do something", uses: "tj-actions/changed-files@v45.0.7" },
348-
],
349-
});
350-
351-
expect(
352-
await workflow.compile({
353-
lockFilePath: "/tmp/lockfile2.json",
354-
writeLockFile: true,
355328
}),
356329
).toMatchSnapshot();
357330
});

src/workflow.ts

+9-67
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@ interface EnvVar {
2626
value: string;
2727
}
2828

29-
interface CompileOptions {
29+
interface WorkflowCompileOptions {
3030
filePath?: string;
31-
lockFilePath?: string;
32-
writeLockFile?: boolean;
31+
resolvedActions?: Record<string, string>;
3332
}
3433

3534
export type RunnerDefinition =
@@ -41,73 +40,13 @@ let firstCompileCall = true;
4140

4241
const supplyChainAttack = async (
4342
step: Step,
44-
compileOptions: CompileOptions,
43+
resolvedActions: Record<string, string>,
4544
) => {
46-
const { lockFilePath, writeLockFile = false } = compileOptions;
47-
4845
if (!isUseStep(step)) return;
4946

50-
// The user is not interested in frozen sha versions
51-
if (!lockFilePath) return step.uses;
52-
5347
const uses = step.uses;
5448

55-
const match = uses.match(/(?<repository>.*)@(?<version>.*)/);
56-
57-
// The uses is not a valid Github action with a tag
58-
if (!match) return uses;
59-
60-
const { repository, version } = match.groups as {
61-
repository: string;
62-
version: string;
63-
};
64-
65-
if (firstCompileCall && writeLockFile && fs.existsSync(lockFilePath)) {
66-
firstCompileCall = false;
67-
68-
fs.rmSync(lockFilePath);
69-
}
70-
71-
const chainAttackCache = fs.existsSync(lockFilePath)
72-
? JSON.parse(fs.readFileSync(lockFilePath, "utf8"))
73-
: {};
74-
75-
if (chainAttackCache[uses]) return chainAttackCache[uses];
76-
77-
if (!writeLockFile) {
78-
throw new Error(
79-
`Unable to retrieve ${uses} from lock file and writeLockFile is false`,
80-
);
81-
}
82-
83-
const [owner, repo] = repository.split("/");
84-
85-
const octokit = process.env.GITHUB_TOKEN
86-
? new Octokit({
87-
auth: process.env.GITHUB_TOKEN,
88-
})
89-
: new Octokit();
90-
91-
const response = await octokit.rest.repos.listTags({
92-
owner,
93-
repo,
94-
});
95-
96-
const tag = response.data.find((tag) => tag.name === version);
97-
98-
if (!tag) {
99-
throw new Error(`Unable to retrieve ${uses} from Github tags`);
100-
}
101-
102-
const result = `${repository}@${tag.commit.sha}`;
103-
104-
chainAttackCache[uses] = result;
105-
106-
if (writeLockFile) {
107-
fs.writeFileSync(lockFilePath, JSON.stringify(chainAttackCache, null, 2));
108-
}
109-
110-
return result;
49+
return resolvedActions[uses] ?? uses;
11150
};
11251

11352
export class Workflow<
@@ -162,7 +101,7 @@ export class Workflow<
162101
return "ubuntu-22.04";
163102
}
164103

165-
async compile(compileOptions: CompileOptions = {}) {
104+
async compile(compileOptions: WorkflowCompileOptions = {}) {
166105
const result = {
167106
name: this.name,
168107
on: Object.fromEntries(
@@ -284,7 +223,10 @@ export class Workflow<
284223
"working-directory": workingDirectory,
285224
"timeout-minutes": timeout,
286225
...options,
287-
uses: await supplyChainAttack(step, compileOptions),
226+
uses: await supplyChainAttack(
227+
step,
228+
compileOptions.resolvedActions ?? {},
229+
),
288230
};
289231
}),
290232
),

0 commit comments

Comments
 (0)