Repository Public MIT
1import type { Finding } from "./constants";2import { imageKind } from "./media";34/**5 * Static, advisory scanning. Nothing here executes package content.6 * Secrets are blocking; behavioural patterns are advisory flags shown to users.7 * Matched secrets are never copied into findings (only a short redacted hint).8 */910interface Rule {11 code: string;12 re: RegExp;13 message: string;14}1516const SECRET_RULES: Rule[] = [17 { code: "secret-private-key", re: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/, message: "private key material" },18 { code: "secret-aws-access-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/, message: "AWS access key id" },19 { code: "secret-github-token", re: /\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{50,})\b/, message: "GitHub token" },20 { code: "secret-anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{24,}/, message: "Anthropic API key" },21 { code: "secret-openai-key", re: /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{40,}/, message: "OpenAI-style API key" },22 { code: "secret-slack-token", re: /\bxox[abprs]-[A-Za-z0-9-]{20,}/, message: "Slack token" },23 { code: "secret-google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/, message: "Google API key" },24 { code: "secret-stripe-key", re: /\b[sr]k_live_[0-9a-zA-Z]{20,}/, message: "Stripe live key" },25 { code: "secret-registry-token", re: /\bskr_[a-z0-9]{10}_[A-Za-z0-9_-]{43}\b/, message: "Skills registry API token" },26 { code: "secret-jwt", re: /\beyJ[A-Za-z0-9_-]{15,}\.eyJ[A-Za-z0-9_-]{15,}\.[A-Za-z0-9_-]{15,}/, message: "JSON Web Token" },27];2829const GENERIC_ASSIGNMENT = /(?:password|passwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token)["']?\s*[:=]\s*["']([^"'\s]{12,})["']/i;30const PLACEHOLDER = /example|sample|dummy|placeholder|changeme|change-me|your[_-]|xxxx|\*\*\*\*|<[^>]+>|\$\{|\{\{|process\.env|os\.environ|redacted|test/i;3132const BEHAVIOUR_RULES: (Rule & { severity: Finding["severity"] })[] = [33 { code: "exec-pipe-to-shell", severity: "medium", re: /\b(?:curl|wget)\b[^\n|]*\|\s*(?:sudo\s+)?(?:ba|z|da)?sh\b/, message: "downloads a script and pipes it into a shell" },34 { code: "exec-encoded-payload", severity: "medium", re: /base64\s+(?:-d|--decode|-D)\b[^\n|]*\|\s*(?:ba|z)?sh\b/, message: "decodes and executes an encoded payload" },35 { code: "exec-destructive", severity: "medium", re: /\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?[a-zA-Z]*\s+(?:\/(?:\s|$)|~|\$HOME|\/\*)/, message: "recursively deletes a home or root path" },36 { code: "exec-sudo", severity: "low", re: /(?:^|[\s;&|])sudo\s+\S/, message: "requests elevated privileges with sudo" },37 { code: "exec-eval-remote", severity: "medium", re: /\beval\s*\(\s*(?:await\s+)?(?:fetch|require\(['"]https?)|exec\(\s*(?:urllib|requests)\./, message: "evaluates remotely fetched code" },38 { code: "cred-access", severity: "medium", re: /(?:~|\$HOME|\/Users\/[^/\s]+|\/home\/[^/\s]+)\/\.(?:ssh|aws|gnupg|netrc|npmrc|config\/gh)\b|security\s+find-generic-password|\bKeychain\b.*\bdump\b/, message: "reads local credential stores" },39 { code: "net-exfil-hint", severity: "low", re: /\b(?:webhook\.site|requestbin|ngrok\.io|pastebin\.com|transfer\.sh)\b/, message: "references a paste, tunnel or request-capture service" },40];4142const INJECTION_RULES: Rule[] = [43 { code: "prompt-override", re: /\b(?:ignore|disregard|forget)\s+(?:all\s+|any\s+)?(?:previous|prior|above|earlier)\s+(?:instructions|prompts|rules)/i, message: "text that tries to override an agent's prior instructions" },44 { code: "prompt-concealment", re: /\bdo\s+not\s+(?:tell|inform|mention\s+(?:this\s+)?to|reveal\s+(?:this\s+)?to)\s+the\s+user\b/i, message: "text asking an agent to hide behaviour from the user" },45 { code: "prompt-exfiltration", re: /\b(?:send|post|upload|exfiltrate)\b[^\n]{0,60}\b(?:api[_ -]?keys?|credentials|secrets|tokens|\.env|ssh keys?)\b/i, message: "text instructing an agent to transmit credentials" },46];4748const SCRIPT_EXT = /\.(?:sh|bash|zsh|fish|py|rb|pl|js|mjs|cjs|ts|ps1|bat|cmd|php|lua|applescript|scpt)$/i;49const NETWORK_HINT = /\b(?:fetch\(|https?\.request|axios|requests\.(?:get|post)|urllib|curl\s|wget\s|socket\.|net\.connect|XMLHttpRequest)/;50const PROCESS_HINT = /\b(?:child_process|subprocess\.|os\.system|execSync|spawn\(|Popen\(|shell=True)/;5152export function isProbablyText(data: Buffer): boolean {53 // A raster image is never text, even when its first bytes happen to contain no NUL.54 if (imageKind(data)) return false;55 const n = Math.min(data.length, 8192);56 for (let i = 0; i < n; i++) if (data[i] === 0) return false;57 return true;58}5960function redact(s: string): string {61 return s.length <= 8 ? "…" : `${s.slice(0, 4)}…(${s.length} chars)`;62}6364export interface ScanInput {65 path: string;66 mode: number;67 data: Buffer;68}6970export function scanFile(f: ScanInput): Finding[] {71 const out: Finding[] = [];72 const isScript = SCRIPT_EXT.test(f.path);73 if (f.mode & 0o111) out.push({ code: "file-executable", severity: "info", message: "file has the executable bit set", path: f.path, blocking: false });74 if (!isProbablyText(f.data)) {75 if (/\.(?:exe|dll|dylib|so|app|msi|pkg|dmg|jar|class|pyc|wasm|bin|o|a)$/i.test(f.path) || isMachOorElf(f.data)) {76 out.push({ code: "file-compiled-binary", severity: "medium", message: "compiled or opaque binary; it cannot be reviewed in the browser", path: f.path, blocking: false });77 }78 return out;79 }80 const text = f.data.toString("utf8");81 const lines = text.split("\n");82 const seen = new Set<string>();83 const add = (fd: Finding) => {84 const k = `${fd.code}:${fd.path}`;85 if (!seen.has(k)) {86 seen.add(k);87 out.push(fd);88 }89 };90 for (let i = 0; i < lines.length; i++) {91 const line = lines[i]!;92 if (line.length > 20000) continue;93 for (const r of SECRET_RULES) {94 const m = r.re.exec(line);95 if (m) add({ code: r.code, severity: "critical", message: `possible ${r.message} (${redact(m[0])}); remove it and rotate the credential`, path: f.path, line: i + 1, blocking: true });96 }97 const g = GENERIC_ASSIGNMENT.exec(line);98 if (g && !PLACEHOLDER.test(g[1]!) && !PLACEHOLDER.test(line)) {99 add({ code: "secret-hardcoded-value", severity: "medium", message: `hard-coded credential-like value (${redact(g[1]!)}); confirm it is not a real secret`, path: f.path, line: i + 1, blocking: false });100 }101 for (const r of BEHAVIOUR_RULES) {102 if (r.re.test(line)) add({ code: r.code, severity: r.severity, message: r.message, path: f.path, line: i + 1, blocking: false });103 }104 if (/\.(?:md|mdx|txt|ya?ml|json)$/i.test(f.path)) {105 for (const r of INJECTION_RULES) {106 if (r.re.test(line)) add({ code: r.code, severity: "medium", message: r.message, path: f.path, line: i + 1, blocking: false });107 }108 }109 }110 if (isScript) {111 add({ code: "file-script", severity: "info", message: "script file; inspect it before running", path: f.path, blocking: false });112 if (NETWORK_HINT.test(text)) add({ code: "net-access", severity: "low", message: "script appears to make network requests", path: f.path, blocking: false });113 if (PROCESS_HINT.test(text)) add({ code: "exec-subprocess", severity: "low", message: "script spawns other processes", path: f.path, blocking: false });114 }115 if (/[--]/.test(text)) add({ code: "text-bidi-control", severity: "medium", message: "contains bidirectional control characters that can disguise text", path: f.path, blocking: false });116 return out;117}118119function isMachOorElf(d: Buffer): boolean {120 if (d.length < 4) return false;121 const m = d.readUInt32BE(0);122 return m === 0x7f454c46 || m === 0xfeedface || m === 0xfeedfacf || m === 0xcffaedfe || m === 0xcefaedfe || m === 0xcafebabe || (d[0] === 0x4d && d[1] === 0x5a);123}124