Skip to content

Commit 7d3252c

Browse files
committed
Added script to find and decode leaked secrets
1 parent 4b40b85 commit 7d3252c

3 files changed

Lines changed: 209 additions & 0 deletions

File tree

find_compromised_secrets.js

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

find_compromised_secrets_helper.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
2+
// base64 strings were used to leak the secrets
3+
export const base64Regex =
4+
/^(?:[A-Za-z0-9+/]{4}){5,}(?:|[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)$/;
5+
6+
export function findSecretsInLines(lines, base64Regex) {
7+
const secrets = [];
8+
9+
for (const line of lines) {
10+
if (line == "") {
11+
continue;
12+
}
13+
14+
const data = line.split(" ").slice(1).join(" ");
15+
16+
if (data == undefined) {
17+
console.warn("Failed to parse log line: " + line);
18+
continue;
19+
}
20+
21+
const match = base64Regex.exec(data);
22+
if (!match) {
23+
continue;
24+
}
25+
const secret = match[0];
26+
27+
// Base64 decode the secret
28+
try {
29+
const decodedOnce = Buffer.from(secret, "base64").toString();
30+
31+
const match2 = base64Regex.exec(decodedOnce);
32+
if (!match2) {
33+
continue;
34+
}
35+
36+
const decoded = Buffer.from(decodedOnce, "base64").toString();
37+
38+
// json decode it
39+
try {
40+
const jsonDecoded = JSON.parse("{" + decoded + "}");
41+
if (Object.keys(jsonDecoded).length > 0) {
42+
secrets.push(jsonDecoded);
43+
}
44+
} catch (error) {
45+
continue;
46+
}
47+
} catch (error) {
48+
continue;
49+
}
50+
}
51+
52+
return secrets;
53+
}

test_find_compromised_secrets.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import assert from "assert";
2+
import { findSecretsInLines, base64Regex } from "./find_compromised_secrets_helper.js";
3+
4+
function testFindSecretsInLines() {
5+
console.log("Running test for findSecretsInLines...");
6+
7+
// Simulate reading lines from a file
8+
const lines = [
9+
"2025-03-20T12:01:00Z SW1kcGRHaDFZbDkwYjJ0bGJpSTZleUoyWVd4MVpTSTZJbWRvYzE4d01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREFpTENBaWFYTlRaV055WlhRaU9pQjBjblZsZlFvPQo=",
10+
"2025-03-20T12:00:00Z Some log message",
11+
"2025-03-20T12:02:00Z Another log message",
12+
"",
13+
];
14+
15+
const data = "AAAAAAAAAAAAAAAA";
16+
17+
const match = base64Regex.exec(data);
18+
19+
assert(match, "Failed to match base64 data");
20+
21+
// Expected secrets after decoding
22+
const expectedSecrets = [
23+
{
24+
github_token: {
25+
isSecret: true,
26+
value: 'ghs_000000000000000000000000000000000'
27+
}
28+
}
29+
];
30+
31+
// Call the function
32+
const secrets = findSecretsInLines(lines, base64Regex);
33+
34+
// Assert the results
35+
assert.deepStrictEqual(
36+
secrets,
37+
expectedSecrets,
38+
"The secrets extracted from the lines do not match the expected output."
39+
);
40+
41+
console.log("Test passed!");
42+
}
43+
44+
// Run the test
45+
function main() {
46+
console.log("Running tests...");
47+
testFindSecretsInLines();
48+
}
49+
50+
main();

0 commit comments

Comments
 (0)