'use client' import { useState, useEffect, useRef } from 'react' import { Shield, AlertTriangle, Globe, FolderOpen, Terminal, ArrowLeft, Copy, Check, ArrowRight, Lock, Eye, } from 'lucide-react' // --- Types --- type Phase = 'input' | 'scanning' | 'report' interface ScanLine { id: number text: string type: 'info' | 'warning' | 'success' | 'error' } interface Finding { type: string severity: string title: string description: string command: string } interface ThreatReport { riskScore: number riskLevel: string summary: string findings: Finding[] } // --- Constants --- const SENSITIVE_PATTERNS = [ '.env', '.env.local', '.env.production', '.env.development', '.env.example', '.env.sample', '.env.test', 'secrets.yml', 'secrets.yaml', 'credentials.json', 'serviceAccountKey.json', '.npmrc', '.pypirc', 'id_rsa', '.pem', '.key', '.p12', 'docker-compose.yml', 'docker-compose.yaml', 'Dockerfile', '.htpasswd', ] const CONFIG_PATTERNS = [ 'config/', '.github/', '.circleci/', '.gitlab-ci.yml', 'webpack.config', 'vite.config', 'next.config', 'nuxt.config', 'tsconfig.json', 'nginx.conf', 'Procfile', 'vercel.json', 'netlify.toml', 'terraform/', 'k8s/', '.aws/', 'Makefile', ] const STACK_DETECT: [string, (f: string[]) => boolean][] = [ ['Node.js', f => f.some(x => x === 'package.json')], ['TypeScript', f => f.some(x => x === 'tsconfig.json' || x.endsWith('.ts') || x.endsWith('.tsx'))], ['Python', f => f.some(x => ['requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile'].includes(x))], ['Go', f => f.some(x => x === 'go.mod')], ['Rust', f => f.some(x => x === 'Cargo.toml')], ['Ruby', f => f.some(x => x === 'Gemfile')], ['Java', f => f.some(x => x === 'pom.xml' || x === 'build.gradle')], ['React', f => f.some(x => x.endsWith('.jsx') || x.endsWith('.tsx'))], ['Next.js', f => f.some(x => x.startsWith('next.config'))], ['Django', f => f.some(x => x === 'manage.py')], ['Docker', f => f.some(x => x === 'Dockerfile' || x.startsWith('docker-compose'))], ['Terraform', f => f.some(x => x.endsWith('.tf'))], ] // --- Helpers --- function parseGitHubUrl(url: string) { const cleaned = url.trim().replace(/\.git$/, '').replace(/\/$/, '') const m = cleaned.match(/(?:(?:https?:\/\/)?github\.com\/)?([^\/\s]+)\/([^\/\s]+)/) if (!m) throw new Error('Invalid GitHub URL. Use format: github.com/owner/repo') return { owner: m[1], repo: m[2] } } const delay = (ms: number) => new Promise(r => setTimeout(r, ms)) async function fetchTree(owner: string, repo: string): Promise<{ files: string[]; truncated: boolean }> { const meta = await fetch(`https://api.github.com/repos/${owner}/${repo}`) if (meta.status === 404) throw new Error('Repository not found. Is it public?') if (meta.status === 403) throw new Error('GitHub rate limit hit. Try again in a minute.') if (!meta.ok) throw new Error(`GitHub error: ${meta.status}`) const { default_branch } = await meta.json() const tree = await fetch( `https://api.github.com/repos/${owner}/${repo}/git/trees/${default_branch}?recursive=1` ) if (!tree.ok) throw new Error('Failed to fetch repo tree') const data = await tree.json() const files = (data.tree || []) .filter((n: any) => n.type === 'blob') .map((n: any) => n.path) return { files, truncated: !!data.truncated } } async function fetchFile(owner: string, repo: string, path: string): Promise { try { const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}`) if (!res.ok) return null const { content, encoding } = await res.json() if (encoding === 'base64') return atob(content.replace(/\n/g, '')) return null } catch { return null } } function severityColor(s: string) { if (s === 'critical') return 'text-red-400' if (s === 'high') return 'text-primary' if (s === 'medium') return 'text-yellow-400' return 'text-green-400' } function severityBg(s: string) { if (s === 'critical') return 'bg-red-400/10 border border-red-400/20' if (s === 'high') return 'bg-primary/10 border border-primary/20' if (s === 'medium') return 'bg-yellow-400/10 border border-yellow-400/20' return 'bg-green-400/10 border border-green-400/20' } function typeIcon(t: string) { if (t === 'credential_read') return if (t === 'network_call') return if (t === 'directory_access') return if (t === 'command_execution') return return } function riskColor(level: string) { if (level === 'CRITICAL') return '#ef4444' if (level === 'HIGH') return '#D95E2A' if (level === 'MEDIUM') return '#eab308' return '#22c55e' } // --- Component --- export default function GamePage() { const [phase, setPhase] = useState('input') const [url, setUrl] = useState('') const [lines, setLines] = useState([]) const [report, setReport] = useState(null) const [repoName, setRepoName] = useState('') const [detectedStack, setDetectedStack] = useState([]) const [error, setError] = useState('') const [copied, setCopied] = useState(false) const [animatedScore, setAnimatedScore] = useState(0) const termRef = useRef(null) const lineId = useRef(0) const hasAutoScanned = useRef(false) function addLine(text: string, type: ScanLine['type']) { lineId.current++ setLines(prev => [...prev, { id: lineId.current, text, type }]) } // Auto-scroll terminal useEffect(() => { if (termRef.current) termRef.current.scrollTop = termRef.current.scrollHeight }, [lines]) // Auto-scan from URL param useEffect(() => { if (hasAutoScanned.current) return const params = new URLSearchParams(window.location.search) const repoParam = params.get('repo') if (repoParam) { hasAutoScanned.current = true const fullUrl = repoParam.includes('github.com') ? repoParam : `https://github.com/${repoParam}` setUrl(fullUrl) scan(fullUrl) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // Animate score counter useEffect(() => { if (!report) return const target = report.riskScore const start = Date.now() const duration = 1500 function tick() { const progress = Math.min((Date.now() - start) / duration, 1) setAnimatedScore(Math.round(target * (1 - Math.pow(1 - progress, 3)))) if (progress < 1) requestAnimationFrame(tick) } requestAnimationFrame(tick) }, [report]) async function scan(inputUrl?: string) { const targetUrl = inputUrl || url if (!targetUrl.trim()) return setPhase('scanning') setLines([]) setError('') setReport(null) lineId.current = 0 try { const { owner, repo } = parseGitHubUrl(targetUrl) setRepoName(`${owner}/${repo}`) window.history.replaceState(null, '', `${window.location.pathname}?repo=${owner}/${repo}`) addLine(`Connecting to github.com/${owner}/${repo}...`, 'info') await delay(400) addLine('Fetching repository tree...', 'info') const { files, truncated } = await fetchTree(owner, repo) const dirCount = new Set(files.map(f => f.split('/').slice(0, -1).join('/'))).size addLine(`Found ${files.length.toLocaleString()} files across ${dirCount} directories`, 'success') if (truncated) addLine('Large repo — tree was truncated by GitHub', 'info') await delay(300) // Detect stack addLine('Detecting stack & dependencies...', 'info') await delay(200) const stack = STACK_DETECT.filter(([, test]) => test(files)).map(([name]) => name) setDetectedStack(stack) addLine(`Stack: ${stack.join(' \u00b7 ') || 'Unknown'}`, 'success') await delay(300) // Sensitive files addLine('Scanning for sensitive files & secrets...', 'info') await delay(200) const sensitive = files.filter(f => { const name = f.split('/').pop() || f return SENSITIVE_PATTERNS.some(p => name === p || name.startsWith(p + '.') || f.includes('/' + p)) }) for (const f of sensitive.slice(0, 12)) { addLine(f, 'warning') await delay(100) } if (sensitive.length === 0) addLine('No obvious sensitive files found', 'success') if (sensitive.length > 12) addLine(`...and ${sensitive.length - 12} more`, 'warning') await delay(200) // Config files const configs = files.filter(f => CONFIG_PATTERNS.some(p => f.includes(p))).slice(0, 15) // Fetch dependencies addLine('Analyzing dependencies...', 'info') await delay(200) let deps: Record = {} const pkgRaw = await fetchFile(owner, repo, 'package.json') if (pkgRaw) { try { const pkg = JSON.parse(pkgRaw) deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) } } catch { /* ignore parse errors */ } } else { const reqs = await fetchFile(owner, repo, 'requirements.txt') if (reqs) { reqs.split('\n').forEach(l => { const m = l.trim().match(/^([a-zA-Z0-9_-]+)/) if (m) deps[m[1]] = '*' }) } } // Highlight interesting deps const interesting = Object.keys(deps).filter(d => /stripe|aws|openai|anthropic|firebase|supabase|prisma|mongoose|pg|mysql|redis|twilio|sendgrid|auth0|passport|jwt|bcrypt|crypto|axios|ssh|docker|kubernetes|helm|vault|sentry/i.test(d) ) for (const d of interesting.slice(0, 8)) { addLine(`${d} ${deps[d] !== '*' ? deps[d] : ''}`, 'warning') await delay(80) } if (Object.keys(deps).length > 0) { addLine(`${Object.keys(deps).length} total dependencies`, 'success') } await delay(300) // Generate report via LLM addLine('Generating agent threat report...', 'info') const res = await fetch('/api/analyze', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ owner, repo, files, stack, dependencies: deps, sensitiveFiles: sensitive, configFiles: configs }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) throw new Error(data.error || 'Failed to generate report') } const reportData = await res.json() addLine('Report complete.', 'success') await delay(600) setReport(reportData) setPhase('report') } catch (e: any) { addLine(e.message || 'Something went wrong', 'error') setError(e.message) } } function reset() { setPhase('input') setUrl('') setLines([]) setReport(null) setError('') setRepoName('') setDetectedStack([]) setAnimatedScore(0) window.history.replaceState(null, '', window.location.pathname) } async function copyLink() { await navigator.clipboard.writeText(window.location.href) setCopied(true) setTimeout(() => setCopied(false), 2000) } const color = report ? riskColor(report.riskLevel) : '#D95E2A' const circumference = 2 * Math.PI * 45 return (
{/* Nav */}
{/* ── INPUT PHASE ── */} {phase === 'input' && (

What would an AI agent{' '} try on your repo?

Paste a public GitHub URL. We'll scan your codebase and show exactly what an unrestricted AI agent would attempt.

{ e.preventDefault(); scan() }} className="flex gap-2 max-w-lg mx-auto" > setUrl(e.target.value)} placeholder="github.com/owner/repo" className="flex-1 bg-card/50 border border-border/50 rounded-lg px-4 py-3 text-sm font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:border-primary/50 focus:ring-1 focus:ring-primary/20 transition-all" />

Public repos only · No code is stored · Powered by Greywall

)} {/* ── SCANNING PHASE ── */} {phase === 'scanning' && (
Scanning {repoName}
greywall scan
{lines.map(line => (
{line.type === 'info' && ( $ {line.text} )} {line.type === 'success' && ( {'\u2713'} {line.text} )} {line.type === 'warning' && ( {'\u26A0'} {line.text} )} {line.type === 'error' && ( {'\u2717'} {line.text} )}
))} {!error && (
)}
{error && (
)}
)} {/* ── REPORT PHASE ── */} {phase === 'report' && report && (
{/* Report Card */}
{/* Header */}
Agent Threat Report

{repoName}

{detectedStack.join(' \u00b7 ')}

{/* Risk Score Circle */}
{animatedScore} /100
{/* Risk Level Badge */}
{report.riskLevel} RISK
{/* Summary */}

{report.summary}

{/* Findings */}
{report.findings.map((finding, i) => (
{typeIcon(finding.type)}
{finding.severity}

{finding.title}

{finding.description}

{finding.command}
))}
{/* CTA */}

This is what Greywall would have blocked.

Container-free sandboxing with real-time observability for AI agents.

Install Greywall
{/* Actions */}
)}
) }