Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

security-scan.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

security-scan.tsBlame213 lines · 1 contributor
3ef4c9dClaude1/**
2 * Security + secret scanner.
3 * Runs on every push (via post-receive) AND every PR.
4 * Combines fast regex detection for secrets with an AI-powered semantic review
5 * for risky patterns (SSRF, SQL injection, XSS, unsafe deserialisation, etc).
6 */
7
8import { getAnthropic, MODEL_SONNET, extractText, parseJsonResponse, isAiAvailable } from "./ai-client";
9
10export interface SecretFinding {
11 type: string;
12 file: string;
13 line: number;
14 snippet: string;
15 severity: "critical" | "high" | "medium" | "low";
16}
17
18export interface SecurityFinding {
19 type: string;
20 file: string;
21 line?: number;
22 description: string;
23 severity: "critical" | "high" | "medium" | "low";
24 suggestion?: string;
25}
26
27export interface ScanResult {
28 secrets: SecretFinding[];
29 securityIssues: SecurityFinding[];
30 summary: string;
31 passed: boolean;
32}
33
34interface SecretPattern {
35 type: string;
36 regex: RegExp;
37 severity: SecretFinding["severity"];
38}
39
40// High-signal secret detectors. Ordered most-specific first.
41export const SECRET_PATTERNS: SecretPattern[] = [
42 { type: "AWS Access Key", regex: /\b(AKIA|ASIA|AIDA|AROA)[0-9A-Z]{16}\b/, severity: "critical" },
43 { type: "AWS Secret Key", regex: /aws(.{0,20})?(secret|access)?(.{0,20})?['\"]([A-Za-z0-9/+=]{40})['\"]/i, severity: "critical" },
44 { type: "GitHub Token", regex: /\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,251}\b/, severity: "critical" },
45 { type: "Anthropic API Key", regex: /\bsk-ant-(api03|admin01)-[A-Za-z0-9_-]{80,}\b/, severity: "critical" },
46 { type: "OpenAI API Key", regex: /\bsk-(proj-|live-)?[A-Za-z0-9_-]{32,}\b/, severity: "critical" },
47 { type: "Stripe Key", regex: /\b(sk_live_|rk_live_|pk_live_)[A-Za-z0-9]{24,}\b/, severity: "critical" },
48 { type: "Slack Token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, severity: "high" },
49 { type: "Google API Key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/, severity: "high" },
50 { type: "SendGrid Key", regex: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/, severity: "high" },
51 { type: "Twilio Key", regex: /\bSK[0-9a-fA-F]{32}\b/, severity: "high" },
52 { type: "Generic API Token", regex: /(?:api[_-]?key|apikey|access[_-]?token|secret)["'\s:=]+["']?([A-Za-z0-9_\-]{24,})["']?/i, severity: "medium" },
53 { type: "Private Key (PEM)", regex: /-----BEGIN (RSA |OPENSSH |EC |DSA |PGP )?PRIVATE KEY-----/, severity: "critical" },
54 { type: "JWT", regex: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/, severity: "medium" },
55 { type: "Postgres URL", regex: /\bpostgres(?:ql)?:\/\/[^:\s]+:[^@\s]+@[^/\s]+\/\S+/, severity: "high" },
56 { type: "Mongo URL", regex: /\bmongodb(?:\+srv)?:\/\/[^:\s]+:[^@\s]+@[^/\s]+\/\S+/, severity: "high" },
57];
58
59// Paths we skip entirely (noise, binaries, generated files)
60const SKIP_PATHS = [
61 /(^|\/)\.git\//,
62 /(^|\/)node_modules\//,
63 /(^|\/)vendor\//,
64 /(^|\/)dist\//,
65 /(^|\/)build\//,
66 /(^|\/)\.next\//,
67 /(^|\/)\.cache\//,
68 /\.(png|jpg|jpeg|gif|webp|ico|svg|pdf|mp4|mov|wasm|woff2?|ttf|eot|map)$/i,
69 /(^|\/)bun\.lock(b)?$/,
70 /(^|\/)package-lock\.json$/,
71 /(^|\/)yarn\.lock$/,
72 /(^|\/)pnpm-lock\.yaml$/,
73];
74
75export function shouldSkipPath(path: string): boolean {
76 return SKIP_PATHS.some((re) => re.test(path));
77}
78
79/**
80 * Fast local regex-based secret scanner. No network, no Claude — safe to run
81 * on every push regardless of whether the AI key is configured.
82 */
83export function scanForSecrets(
84 files: Array<{ path: string; content: string }>
85): SecretFinding[] {
86 const findings: SecretFinding[] = [];
87 for (const file of files) {
88 if (shouldSkipPath(file.path)) continue;
89 const lines = file.content.split("\n");
90 for (let i = 0; i < lines.length; i++) {
91 const line = lines[i];
92 // Skip lines that look like placeholders / tests
93 if (
94 /example|placeholder|fake|dummy|your[-_]?api|xxxxx|testkey|changeme/i.test(
95 line
96 )
97 ) {
98 continue;
99 }
100 for (const pattern of SECRET_PATTERNS) {
101 if (pattern.regex.test(line)) {
102 findings.push({
103 type: pattern.type,
104 file: file.path,
105 line: i + 1,
106 snippet: line.trim().slice(0, 200),
107 severity: pattern.severity,
108 });
109 break; // one finding per line
110 }
111 }
112 }
113 }
114 return findings;
115}
116
117/**
118 * Ask Claude to review a diff or snapshot for security issues.
119 * Returns structured findings; safe to call without AI key (returns empty).
120 */
121export async function aiSecurityScan(
122 repoFullName: string,
123 diffOrSnapshot: string
124): Promise<SecurityFinding[]> {
125 if (!isAiAvailable()) return [];
126 const client = getAnthropic();
127
128 try {
129 const message = await client.messages.create({
130 model: MODEL_SONNET,
131 max_tokens: 2048,
132 messages: [
133 {
134 role: "user",
135 content: `You are a security auditor reviewing code on the repository "${repoFullName}".
136
137Analyse the following code for high-signal security issues:
138- Injection (SQL, command, LDAP, XPath)
139- Cross-site scripting (XSS)
140- Insecure deserialisation
141- SSRF / unvalidated redirects
142- Path traversal
143- Broken authentication / authorisation (e.g. missing access checks)
144- Insecure cryptography (weak hashing for passwords, hard-coded IVs)
145- Race conditions with security impact
146- Insufficient input validation at a trust boundary
147
148Do NOT report low-risk style issues, noisy defensive-coding suggestions, or theoretical risks without a plausible trigger.
149
150Respond ONLY with JSON of shape:
151{
152 "findings": [
153 { "type": "SQL Injection", "file": "src/x.ts", "line": 42, "severity": "high", "description": "...", "suggestion": "..." }
154 ]
155}
156
157If the code is clean, return { "findings": [] }.
158
159\`\`\`
160${diffOrSnapshot.slice(0, 80000)}
161\`\`\``,
162 },
163 ],
164 });
165
166 const text = extractText(message);
167 const parsed = parseJsonResponse<{ findings: SecurityFinding[] }>(text);
168 if (!parsed || !Array.isArray(parsed.findings)) return [];
169 // Normalise severity
170 return parsed.findings.map((f) => ({
171 ...f,
172 severity: (["critical", "high", "medium", "low"].includes(f.severity as string)
173 ? f.severity
174 : "medium") as SecurityFinding["severity"],
175 }));
176 } catch (err) {
177 console.error("[security-scan] AI scan failed:", err);
178 return [];
179 }
180}
181
182/**
183 * Run full security scan: regex secrets + (optional) AI security review.
184 */
185export async function runSecurityScan(
186 repoFullName: string,
187 files: Array<{ path: string; content: string }>,
188 diffText?: string
189): Promise<ScanResult> {
190 const secrets = scanForSecrets(files);
191 const securityIssues = diffText
192 ? await aiSecurityScan(repoFullName, diffText)
193 : [];
194
195 const criticalSecrets = secrets.filter((s) => s.severity === "critical").length;
196 const criticalIssues = securityIssues.filter((i) => i.severity === "critical").length;
197 const highIssues = securityIssues.filter((i) => i.severity === "high").length;
198
199 const passed = criticalSecrets === 0 && criticalIssues === 0 && highIssues === 0;
200
201 const parts: string[] = [];
202 if (secrets.length) parts.push(`${secrets.length} secret${secrets.length === 1 ? "" : "s"}`);
203 if (securityIssues.length)
204 parts.push(
205 `${securityIssues.length} security issue${securityIssues.length === 1 ? "" : "s"}`
206 );
207 const summary =
208 parts.length === 0
209 ? "No issues detected"
210 : `Found ${parts.join(" + ")} (${criticalSecrets + criticalIssues} critical, ${highIssues} high)`;
211
212 return { secrets, securityIssues, summary, passed };
213}