-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathfind_compromised_secrets.js
More file actions
114 lines (89 loc) · 3.14 KB
/
find_compromised_secrets.js
File metadata and controls
114 lines (89 loc) · 3.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/* Script to find compromised secrets in an Actions workflow run.
Only relevant to a particular series of incidents, where a malicious actor
pushed a commit to a repository that contained a workflow that leaked secrets
into the logs.
They were doubly-Base64 encoded, so we need to spot Base64 strings and decode them.
*/
import { Octokit } from "@octokit/rest";
import fs from "fs";
import AdmZip from "adm-zip";
import { findSecretsInLines } from "./find_compromised_secrets_utils.js";
// Initialize Octokit with a personal access token
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN, // Set your GitHub token in an environment variable
baseUrl: process.env.GITHUB_BASE_URL, // Set the GitHub base URL, e.g. for Enterprise Server, in an env var
});
// Helper function to extract secrets leaked into workflow logs
async function extractSecretsFromLogs(logUrl) {
try {
const response = await octokit.request(`GET ${logUrl}`, {
headers: { Accept: "application/vnd.github+json" },
});
// get the zip file content
const zipBuffer = Buffer.from(response.data);
// Unzip the file
const zip = new AdmZip(zipBuffer);
const logEntries = zip.getEntries();
const secrets = [];
// Iterate through each file in the zip
for (const entry of logEntries) {
if (!entry.isDirectory) {
const fileName = entry.entryName;
if (fileName.startsWith("0_")) {
const logContent = entry.getData().toString("utf8");
let lines = logContent.split("\n");
secrets.push(...findSecretsInLines(lines));
}
}
}
return secrets;
} catch (error) {
console.error(`Failed to fetch logs from ${logUrl}:`, error.message);
return [];
}
}
async function main() {
// Parse CLI arguments
const args = process.argv.slice(2);
if (args.length > 0) {
const script_name = process.argv[1].split("/").pop();
console.error(`Usage: node ${script_name} < <SLJSON input file>`);
return;
}
// read the actions runs from STDIN, in single-line JSON format
const actions_run_lines = fs.readFileSync(0).toString().split("\n");
const all_secrets = [];
for (const line of actions_run_lines) {
if (line == "" || !line.startsWith("{")) {
continue;
}
try {
const actions_run = JSON.parse(line);
const owner = actions_run.org;
const repo = actions_run.repo;
const run_id = actions_run.run_id;
console.log(`Processing actions run ${owner}/${repo}#${run_id}...`);
// get the logs for the run
const logUrl = `/repos/${owner}/${repo}/actions/runs/${run_id}/logs`;
const secrets = await extractSecretsFromLogs(logUrl);
console.log(
`Found ${secrets.length} secrets in log for ${owner}/${repo}#${run_id}`
);
for (const secret of secrets) {
console.log(secret);
}
all_secrets.push(...secrets);
} catch (error) {
console.error(`Failed to parse line: ${line}`);
continue;
}
}
for (const secret of all_secrets) {
// write to a file
fs.appendFileSync(
"compromised_secrets.sljson",
JSON.stringify(secret) + "\n"
);
}
}
await main();