diff --git a/.github/workflows/codeguardian.yml b/.github/workflows/codeguardian.yml index ccc0fe3..c39a1f7 100644 --- a/.github/workflows/codeguardian.yml +++ b/.github/workflows/codeguardian.yml @@ -6,6 +6,8 @@ on: pull_request: branches: [ main ] + workflow_dispatch: + jobs: scan: name: Run CodeGuardian diff --git a/src/safeRegex.js b/src/safeRegex.js new file mode 100644 index 0000000..a022ba1 --- /dev/null +++ b/src/safeRegex.js @@ -0,0 +1,46 @@ +import { Worker, isMainThread, parentPort, workerData } from "node:worker_threads"; + +if (!isMainThread) { + // runs inside the isolated worker thread + const { line, pattern, flags } = workerData; + try { + const regex = new RegExp(pattern, flags || "g"); + const matched = regex.test(line); + parentPort.postMessage({ success: true, matched }); + } catch (error) { + parentPort.postMessage({ success: false, error: error.message }); + } +} + +/* + * runs on main thread + */ +export function matchWithTimeout(line, rule, timeoutMs = 50) { + return new Promise((resolve) => { + const worker = new Worker(new URL(import.meta.url), { + workerData: { line, pattern: rule.pattern, flags: rule.flags }, + }); + + // Kill the thread if it takes too long + const timeout = setTimeout(() => { + worker.terminate(); + resolve(false); + }, timeoutMs); + + worker.on("message", (msg) => { + clearTimeout(timeout); + worker.terminate(); + if (msg.success) { + resolve(msg.matched); + } else { + resolve(false); + } + }); + + worker.on("error", () => { + clearTimeout(timeout); + worker.terminate(); + resolve(false); + }); + }); +} diff --git a/src/scanner.js b/src/scanner.js index 18252cb..5f8f38e 100644 --- a/src/scanner.js +++ b/src/scanner.js @@ -6,6 +6,7 @@ import ignore from 'ignore'; import { execSync } from 'node:child_process'; import { styleText } from 'node:util'; import { findUnusedModules } from './unusedModuleDetector.js'; +import { matchWithTimeout } from './safeRegex.js'; const DEFAULT_CONFIG_FILES = ['.codeguardianrc.json', 'codeguardian.config.json']; @@ -70,22 +71,14 @@ function listFiles({ staged, ignoreFiles } = {}) { return entries.filter(e => !ig.ignores(e)); } -function findMatchesInFile(content, rules) { +async function findMatchesInFile(content, rules) { const lines = content.split(/\r?\n/); const findings = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; for (const rule of rules) { - let flags = 'g'; - if (rule.flags) flags = rule.flags; - let regex; - try { - regex = new RegExp(rule.pattern, flags); - } catch (err) { - // invalid regex, skip - continue; - } - if (regex.test(line)) { + const isMatch = await matchWithTimeout(line, rule, 50); + if (isMatch) { findings.push({ rule: rule.name || 'unnamed', lineNumber: i + 1, line: line.trim(), pattern: rule.pattern }); } } @@ -121,7 +114,7 @@ async function run({ configPath = null, staged = false, verbose = false } = {}) continue; } filesScanned++; - const fileFindings = findMatchesInFile(content, rules); + const fileFindings = await findMatchesInFile(content, rules); if (fileFindings.length > 0) { findings.push({ file, matches: fileFindings }); }