From 5459b98ca041d9542e6bf312cd9f6127762543fe Mon Sep 17 00:00:00 2001 From: Henry Mercer Date: Wed, 17 Dec 2025 10:25:46 +0000 Subject: [PATCH] Add simple artifact scanner for tests only --- src/artifact-scanner.test.ts | 80 ++++----------------- src/artifact-scanner.ts | 132 ++++++++++++----------------------- 2 files changed, 58 insertions(+), 154 deletions(-) diff --git a/src/artifact-scanner.test.ts b/src/artifact-scanner.test.ts index 5e3480dc5..084ff2341 100644 --- a/src/artifact-scanner.test.ts +++ b/src/artifact-scanner.test.ts @@ -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(", ")}`, - ); -}); diff --git a/src/artifact-scanner.ts b/src/artifact-scanner.ts index 8301432ae..d4514012a 100644 --- a/src/artifact-scanner.ts +++ b/src/artifact-scanner.ts @@ -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 { 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 { - logger.info("Starting security scan for GitHub tokens in debug artifacts..."); +): Promise { + 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; }