SPB Git

Repository Public MIT

scan.ts 7.6 KB 124 lines typescript

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
Files main
Menu

Theme