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
claude/adoring-hopper-5x74bqclaude/affectionate-feynman-ykrf1hclaude/architecture-audit-design-wxprenclaude/build-status-update-3MXsfclaude/charming-meitner-mllb5rclaude/compare-gate-gluecron-s4mFQclaude/confident-faraday-tikcwbclaude/continue-work-XMTlIclaude/crontech-gluecron-deploy-7MIECclaude/crontech-platform-setup-SeKfwclaude/design-2026claude/ecstatic-ptolemy-jMdigclaude/enhance-github-integration-QNHdGclaude/fix-aa-loop-issue-PonMQclaude/fix-actions-and-processclaude/fix-desktop-errors-XqoW8claude/fix-red-workflowsclaude/fix-website-access-6FKJNclaude/gatetest-integration-hardeningclaude/github-audit-improvements-bDFr9claude/gluecron-launch-status-FoMRlclaude/hopeful-lamport-olfCTclaude/issue-to-pr-and-protectionsclaude/jolly-heisenberg-2sg1Qclaude/launch-preparation-QmTb6claude/new-session-xk1l7claude/plan-platform-architecture-kkN4yclaude/platform-analysis-roadmap-1nUGLclaude/platform-launch-assessment-8dWV8claude/polish-platform-release-AeDrUclaude/resume-previous-work-KzyLwclaude/review-crontech-handoff-qYEVqclaude/review-project-completeness-lHhS2claude/review-readme-docs-ulqPKclaude/serene-edison-rj87weclaude/setup-multi-repo-dev-BCwNQclaude/ship-fixes-and-tests-Jvz1cclaude/site-audit-competitive-pctlwgclaude/site-migration-vercel-XstpKclaude/standalone-product-repos-XHFTDcopilot/feat-smart-empty-states-keyboard-first-enhancementcopilot/feat-smart-morning-digest-review-context-restorecopilot/fix-and-process-workflowscopilot/update-ai-powered-code-reviewfeat/debt-mapfeat/push-policy-codeowners-hardeningfeat/smart-digest-contextfeat/stage-impactfeat/t1-secret-migrationfeat/u-polishfeat/w-self-hostfeat/w2-claude-configfix/agent-journey-orphan-sweepgatetest/auto-fix-1776586424172gatetest/auto-fix-1776586534814gatetest/auto-fix-1776590685143gatetest/auto-fix-1776590808199mainops/redeploy-retriggerstyle/dxt-cta-themeworktree-agent-a3377aad30d55da26worktree-agent-a7ef607b7ee1d6c74
security-scan.ts7.3 KB · 213 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
/**
 * Security + secret scanner.
 * Runs on every push (via post-receive) AND every PR.
 * Combines fast regex detection for secrets with an AI-powered semantic review
 * for risky patterns (SSRF, SQL injection, XSS, unsafe deserialisation, etc).
 */

import { getAnthropic, MODEL_SONNET, extractText, parseJsonResponse, isAiAvailable } from "./ai-client";

export interface SecretFinding {
  type: string;
  file: string;
  line: number;
  snippet: string;
  severity: "critical" | "high" | "medium" | "low";
}

export interface SecurityFinding {
  type: string;
  file: string;
  line?: number;
  description: string;
  severity: "critical" | "high" | "medium" | "low";
  suggestion?: string;
}

export interface ScanResult {
  secrets: SecretFinding[];
  securityIssues: SecurityFinding[];
  summary: string;
  passed: boolean;
}

interface SecretPattern {
  type: string;
  regex: RegExp;
  severity: SecretFinding["severity"];
}

// High-signal secret detectors. Ordered most-specific first.
export const SECRET_PATTERNS: SecretPattern[] = [
  { type: "AWS Access Key", regex: /\b(AKIA|ASIA|AIDA|AROA)[0-9A-Z]{16}\b/, severity: "critical" },
  { type: "AWS Secret Key", regex: /aws(.{0,20})?(secret|access)?(.{0,20})?['\"]([A-Za-z0-9/+=]{40})['\"]/i, severity: "critical" },
  { type: "GitHub Token", regex: /\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36,251}\b/, severity: "critical" },
  { type: "Anthropic API Key", regex: /\bsk-ant-(api03|admin01)-[A-Za-z0-9_-]{80,}\b/, severity: "critical" },
  { type: "OpenAI API Key", regex: /\bsk-(proj-|live-)?[A-Za-z0-9_-]{32,}\b/, severity: "critical" },
  { type: "Stripe Key", regex: /\b(sk_live_|rk_live_|pk_live_)[A-Za-z0-9]{24,}\b/, severity: "critical" },
  { type: "Slack Token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/, severity: "high" },
  { type: "Google API Key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/, severity: "high" },
  { type: "SendGrid Key", regex: /\bSG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}\b/, severity: "high" },
  { type: "Twilio Key", regex: /\bSK[0-9a-fA-F]{32}\b/, severity: "high" },
  { type: "Generic API Token", regex: /(?:api[_-]?key|apikey|access[_-]?token|secret)["'\s:=]+["']?([A-Za-z0-9_\-]{24,})["']?/i, severity: "medium" },
  { type: "Private Key (PEM)", regex: /-----BEGIN (RSA |OPENSSH |EC |DSA |PGP )?PRIVATE KEY-----/, severity: "critical" },
  { type: "JWT", regex: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/, severity: "medium" },
  { type: "Postgres URL", regex: /\bpostgres(?:ql)?:\/\/[^:\s]+:[^@\s]+@[^/\s]+\/\S+/, severity: "high" },
  { type: "Mongo URL", regex: /\bmongodb(?:\+srv)?:\/\/[^:\s]+:[^@\s]+@[^/\s]+\/\S+/, severity: "high" },
];

// Paths we skip entirely (noise, binaries, generated files)
const SKIP_PATHS = [
  /(^|\/)\.git\//,
  /(^|\/)node_modules\//,
  /(^|\/)vendor\//,
  /(^|\/)dist\//,
  /(^|\/)build\//,
  /(^|\/)\.next\//,
  /(^|\/)\.cache\//,
  /\.(png|jpg|jpeg|gif|webp|ico|svg|pdf|mp4|mov|wasm|woff2?|ttf|eot|map)$/i,
  /(^|\/)bun\.lock(b)?$/,
  /(^|\/)package-lock\.json$/,
  /(^|\/)yarn\.lock$/,
  /(^|\/)pnpm-lock\.yaml$/,
];

export function shouldSkipPath(path: string): boolean {
  return SKIP_PATHS.some((re) => re.test(path));
}

/**
 * Fast local regex-based secret scanner. No network, no Claude — safe to run
 * on every push regardless of whether the AI key is configured.
 */
export function scanForSecrets(
  files: Array<{ path: string; content: string }>
): SecretFinding[] {
  const findings: SecretFinding[] = [];
  for (const file of files) {
    if (shouldSkipPath(file.path)) continue;
    const lines = file.content.split("\n");
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      // Skip lines that look like placeholders / tests
      if (
        /example|placeholder|fake|dummy|your[-_]?api|xxxxx|testkey|changeme/i.test(
          line
        )
      ) {
        continue;
      }
      for (const pattern of SECRET_PATTERNS) {
        if (pattern.regex.test(line)) {
          findings.push({
            type: pattern.type,
            file: file.path,
            line: i + 1,
            snippet: line.trim().slice(0, 200),
            severity: pattern.severity,
          });
          break; // one finding per line
        }
      }
    }
  }
  return findings;
}

/**
 * Ask Claude to review a diff or snapshot for security issues.
 * Returns structured findings; safe to call without AI key (returns empty).
 */
export async function aiSecurityScan(
  repoFullName: string,
  diffOrSnapshot: string
): Promise<SecurityFinding[]> {
  if (!isAiAvailable()) return [];
  const client = getAnthropic();

  try {
    const message = await client.messages.create({
      model: MODEL_SONNET,
      max_tokens: 2048,
      messages: [
        {
          role: "user",
          content: `You are a security auditor reviewing code on the repository "${repoFullName}".

Analyse the following code for high-signal security issues:
- Injection (SQL, command, LDAP, XPath)
- Cross-site scripting (XSS)
- Insecure deserialisation
- SSRF / unvalidated redirects
- Path traversal
- Broken authentication / authorisation (e.g. missing access checks)
- Insecure cryptography (weak hashing for passwords, hard-coded IVs)
- Race conditions with security impact
- Insufficient input validation at a trust boundary

Do NOT report low-risk style issues, noisy defensive-coding suggestions, or theoretical risks without a plausible trigger.

Respond ONLY with JSON of shape:
{
  "findings": [
    { "type": "SQL Injection", "file": "src/x.ts", "line": 42, "severity": "high", "description": "...", "suggestion": "..." }
  ]
}

If the code is clean, return { "findings": [] }.

\`\`\`
${diffOrSnapshot.slice(0, 80000)}
\`\`\``,
        },
      ],
    });

    const text = extractText(message);
    const parsed = parseJsonResponse<{ findings: SecurityFinding[] }>(text);
    if (!parsed || !Array.isArray(parsed.findings)) return [];
    // Normalise severity
    return parsed.findings.map((f) => ({
      ...f,
      severity: (["critical", "high", "medium", "low"].includes(f.severity as string)
        ? f.severity
        : "medium") as SecurityFinding["severity"],
    }));
  } catch (err) {
    console.error("[security-scan] AI scan failed:", err);
    return [];
  }
}

/**
 * Run full security scan: regex secrets + (optional) AI security review.
 */
export async function runSecurityScan(
  repoFullName: string,
  files: Array<{ path: string; content: string }>,
  diffText?: string
): Promise<ScanResult> {
  const secrets = scanForSecrets(files);
  const securityIssues = diffText
    ? await aiSecurityScan(repoFullName, diffText)
    : [];

  const criticalSecrets = secrets.filter((s) => s.severity === "critical").length;
  const criticalIssues = securityIssues.filter((i) => i.severity === "critical").length;
  const highIssues = securityIssues.filter((i) => i.severity === "high").length;

  const passed = criticalSecrets === 0 && criticalIssues === 0 && highIssues === 0;

  const parts: string[] = [];
  if (secrets.length) parts.push(`${secrets.length} secret${secrets.length === 1 ? "" : "s"}`);
  if (securityIssues.length)
    parts.push(
      `${securityIssues.length} security issue${securityIssues.length === 1 ? "" : "s"}`
    );
  const summary =
    parts.length === 0
      ? "No issues detected"
      : `Found ${parts.join(" + ")} (${criticalSecrets + criticalIssues} critical, ${highIssues} high)`;

  return { secrets, securityIssues, summary, passed };
}