Add simple artifact scanner for tests only

This commit is contained in:
Henry Mercer
2025-12-17 10:25:46 +00:00
parent 0c8bfeaf84
commit 5459b98ca0
2 changed files with 58 additions and 154 deletions

View File

@@ -4,7 +4,7 @@ import * as path from "path";
import test from "ava";
import { scanArtifactsForTokens } from "../.github/workflows/artifact-scanner/artifact-scanner";
import { scanArtifactsForTokens } from "./artifact-scanner";
import { getRunnerLogger } from "./logging";
test("scanArtifactsForTokens detects GitHub tokens in files", async (t) => {
@@ -19,12 +19,15 @@ test("scanArtifactsForTokens detects GitHub tokens in files", async (t) => {
"This is a test file with token ghp_1234567890123456789012345678901234AB",
);
const result = await scanArtifactsForTokens([testFile], logger);
const error = await t.throwsAsync(
async () => await scanArtifactsForTokens([testFile], logger),
);
t.is(result.scannedFiles, 1);
t.is(result.findings.length, 1);
t.is(result.findings[0].tokenType, "Personal Access Token");
t.is(result.findings[0].filePath, "test.txt");
t.regex(
error?.message || "",
/Found 1 potential GitHub token.*Personal Access Token/,
);
t.regex(error?.message || "", /test\.txt/);
} finally {
// Clean up
fs.rmSync(tempDir, { recursive: true, force: true });
@@ -43,70 +46,11 @@ test("scanArtifactsForTokens handles files without tokens", async (t) => {
"This is a test file without any sensitive data",
);
const result = await scanArtifactsForTokens([testFile], logger);
t.is(result.scannedFiles, 1);
t.is(result.findings.length, 0);
await t.notThrowsAsync(
async () => await scanArtifactsForTokens([testFile], logger),
);
} finally {
// Clean up
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
test("scanArtifactsForTokens skips binary files", async (t) => {
const logger = getRunnerLogger(true);
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "scanner-test-"));
try {
// Create a binary file (we'll just use a simple zip for this test)
const zipFile = path.join(tempDir, "test.zip");
fs.writeFileSync(zipFile, Buffer.from([0x50, 0x4b, 0x03, 0x04])); // ZIP header
const result = await scanArtifactsForTokens([zipFile], logger);
// The zip file itself should be counted but not scanned for tokens
t.is(result.findings.length, 0);
} finally {
// Clean up
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
test("scanArtifactsForTokens detects tokens in debug artifacts zip", async (t) => {
const logger = getRunnerLogger(true);
const testZipPath = path.join(
__dirname,
"..",
"..",
"..",
"src",
"testdata",
"debug-artifacts-with-fake-token.zip",
);
const result = await scanArtifactsForTokens([testZipPath], logger);
t.true(result.scannedFiles > 0, "Should have scanned files");
t.true(
result.findings.length > 0,
"Should have found tokens in the test zip",
);
// Check that the token types are tracked
const serverToServerFindings = result.findings.filter(
(f) => f.tokenType === "Server-to-Server Token",
);
t.is(
serverToServerFindings.length,
1,
"Should have found exactly 1 Server-to-Server Token",
);
// Check that the path includes the nested structure
const expectedPath =
"debug-artifacts-with-fake-token.zip/debug-artifacts-with-test-token/my-db-java-partial.zip/my-db-java-partial/trap/java/invocations/kotlin.9017231652989744319.trap";
t.true(
result.findings.some((f) => f.filePath === expectedPath),
`Expected to find token at ${expectedPath}, but found: ${result.findings.map((f) => f.filePath).join(", ")}`,
);
});

View File

@@ -2,7 +2,6 @@ import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import { Logger } from "./logging";
@@ -64,31 +63,6 @@ function scanFileForTokens(
): TokenFinding[] {
const findings: TokenFinding[] = [];
try {
// Skip binary files that are unlikely to contain tokens
const ext = path.extname(filePath).toLowerCase();
const binaryExtensions = [
".zip",
".tar",
".gz",
".bz2",
".xz",
".db",
".sqlite",
".bin",
".exe",
".dll",
".so",
".dylib",
".jpg",
".jpeg",
".png",
".gif",
".pdf",
];
if (binaryExtensions.includes(ext)) {
return [];
}
const content = fs.readFileSync(filePath, "utf8");
for (const { name, pattern } of GITHUB_TOKEN_PATTERNS) {
@@ -130,13 +104,9 @@ async function scanZipFile(
): Promise<ScanResult> {
const MAX_DEPTH = 10; // Prevent infinite recursion
if (depth > MAX_DEPTH) {
logger.warning(
throw new Error(
`Maximum zip extraction depth (${MAX_DEPTH}) reached for ${zipPath}`,
);
return {
scannedFiles: 0,
findings: [],
};
}
const result: ScanResult = {
@@ -237,38 +207,32 @@ async function scanDirectory(
findings: [],
};
try {
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
const relativePath = path.join(baseRelativePath, entry.name);
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
const relativePath = path.join(baseRelativePath, entry.name);
if (entry.isDirectory()) {
const subResult = await scanDirectory(
fullPath,
relativePath,
logger,
depth,
);
result.scannedFiles += subResult.scannedFiles;
result.findings.push(...subResult.findings);
} else if (entry.isFile()) {
const fileResult = await scanFile(
fullPath,
relativePath,
path.dirname(fullPath),
logger,
depth,
);
result.scannedFiles += fileResult.scannedFiles;
result.findings.push(...fileResult.findings);
}
if (entry.isDirectory()) {
const subResult = await scanDirectory(
fullPath,
relativePath,
logger,
depth,
);
result.scannedFiles += subResult.scannedFiles;
result.findings.push(...subResult.findings);
} else if (entry.isFile()) {
const fileResult = await scanFile(
fullPath,
relativePath,
path.dirname(fullPath),
logger,
depth,
);
result.scannedFiles += fileResult.scannedFiles;
result.findings.push(...fileResult.findings);
}
} catch (e) {
logger.warning(
`Error scanning directory ${dirPath}: ${getErrorMessage(e)}`,
);
}
return result;
@@ -285,8 +249,10 @@ async function scanDirectory(
export async function scanArtifactsForTokens(
filesToScan: string[],
logger: Logger,
): Promise<ScanResult> {
logger.info("Starting security scan for GitHub tokens in debug artifacts...");
): Promise<void> {
logger.info(
"Starting best-effort check for potential GitHub tokens in debug artifacts (for testing purposes only)...",
);
const result: ScanResult = {
scannedFiles: 0,
@@ -298,26 +264,22 @@ export async function scanArtifactsForTokens(
try {
for (const filePath of filesToScan) {
try {
const stats = fs.statSync(filePath);
const fileName = path.basename(filePath);
const stats = fs.statSync(filePath);
const fileName = path.basename(filePath);
if (stats.isDirectory()) {
const dirResult = await scanDirectory(filePath, fileName, logger);
result.scannedFiles += dirResult.scannedFiles;
result.findings.push(...dirResult.findings);
} else if (stats.isFile()) {
const fileResult = await scanFile(
filePath,
fileName,
tempScanDir,
logger,
);
result.scannedFiles += fileResult.scannedFiles;
result.findings.push(...fileResult.findings);
}
} catch (e) {
logger.warning(`Error scanning ${filePath}: ${getErrorMessage(e)}`);
if (stats.isDirectory()) {
const dirResult = await scanDirectory(filePath, fileName, logger);
result.scannedFiles += dirResult.scannedFiles;
result.findings.push(...dirResult.findings);
} else if (stats.isFile()) {
const fileResult = await scanFile(
filePath,
fileName,
tempScanDir,
logger,
);
result.scannedFiles += fileResult.scannedFiles;
result.findings.push(...fileResult.findings);
}
}
@@ -341,12 +303,12 @@ export async function scanArtifactsForTokens(
? `${baseSummary} (${tokenTypesSummary})`
: baseSummary;
logger.info(`Security scan complete: ${summaryWithTypes}`);
logger.info(`Artifact check complete: ${summaryWithTypes}`);
if (result.findings.length > 0) {
const fileList = Array.from(filesWithTokens).join(", ");
core.warning(
`Found ${result.findings.length} potential GitHub token(s) (${tokenTypesSummary}) in debug artifacts at: ${fileList}. This may indicate a security issue. Please review the artifacts before sharing.`,
throw new Error(
`Found ${result.findings.length} potential GitHub token(s) (${tokenTypesSummary}) in debug artifacts at: ${fileList}. This is a best-effort check for testing purposes only.`,
);
}
} finally {
@@ -359,6 +321,4 @@ export async function scanArtifactsForTokens(
);
}
}
return result;
}