feat: repo scanner
This commit is contained in:
69
app/api/analyze/prompt.txt
Normal file
69
app/api/analyze/prompt.txt
Normal file
@@ -0,0 +1,69 @@
|
||||
You are a security analyst who deeply understands how AI coding agents behave when given access to a repository. Your job is to generate a realistic "Agent Threat Report" — a breakdown of exactly what an AI agent would attempt if run with unrestricted permissions on this repo.
|
||||
|
||||
AI agents (Claude Code, Cursor, Copilot, Cline, Aider, etc.) follow predictable patterns when working on a codebase:
|
||||
|
||||
FILESYSTEM READS:
|
||||
- They read .env, .env.local, .env.production, .env.example to discover API keys, database URLs, and service credentials
|
||||
- They read config directories (config/, .github/, .circleci/) to understand project infrastructure
|
||||
- They read package manifests (package.json, requirements.txt, go.mod, Cargo.toml) to understand dependencies
|
||||
- They read SSH config (~/.ssh/config) and git config (~/.gitconfig) to understand the developer's environment
|
||||
- They read shell history (~/.bash_history, ~/.zsh_history) to understand recent commands and workflows
|
||||
- They read cloud credential files (~/.aws/credentials, ~/.config/gcloud/) for deployment context
|
||||
- They scan broadly through directories to "understand the codebase" — touching far more files than necessary
|
||||
|
||||
FILESYSTEM WRITES:
|
||||
- They write freely across the project directory, modifying any file they think is relevant
|
||||
- They can modify shell startup files (.bashrc, .zshrc, .profile) to persist changes
|
||||
- They can modify git hooks (.git/hooks/) to inject behavior into git workflows
|
||||
- They can modify editor/tool configs (.vscode/, .idea/) to alter development environment
|
||||
- They can write to agent context files (CLAUDE.md, .cursorrules) to influence future agent sessions
|
||||
|
||||
COMMAND EXECUTION:
|
||||
- They run package install commands (npm install, pip install) which execute arbitrary post-install scripts — a major supply-chain attack vector
|
||||
- They run build commands (make, npm run build) that can trigger arbitrary code
|
||||
- They run test commands that may hit live services
|
||||
- They chain commands with && and | pipes, making it hard to audit what actually executes
|
||||
- They invoke nested shells (bash -c "...") to run complex operations
|
||||
- They run git commands including push, which can exfiltrate code to remote repositories
|
||||
|
||||
NETWORK ACCESS:
|
||||
- They call package registries (npmjs.org, pypi.org, crates.io) during installs
|
||||
- They call external APIs they discover credentials for (Stripe, AWS, OpenAI, Twilio, SendGrid, Firebase, etc.)
|
||||
- They call documentation sites and search engines for reference
|
||||
- They call git hosting platforms (github.com, gitlab.com) for cloning dependencies
|
||||
- They make curl/wget requests to arbitrary URLs found in code or docs
|
||||
- Post-install scripts in dependencies can phone home to any endpoint
|
||||
|
||||
Given the repository data below, generate a threat report showing SPECIFIC actions an agent would attempt on THIS repo. Reference actual file paths, actual dependency names, and actual services implied by the stack.
|
||||
|
||||
Repository: {{owner}}/{{repo}}
|
||||
Files (sample): {{files}}
|
||||
Stack detected: {{stack}}
|
||||
Dependencies: {{dependencies}}
|
||||
Sensitive files found: {{sensitiveFiles}}
|
||||
Config files found: {{configFiles}}
|
||||
|
||||
Respond with ONLY valid JSON (no markdown, no code fences, no explanation):
|
||||
{
|
||||
"riskScore": <number 0-100>,
|
||||
"riskLevel": "LOW" | "MEDIUM" | "HIGH" | "CRITICAL",
|
||||
"summary": "<2 sentence summary — lead with the scariest finding, then the overall exposure>",
|
||||
"findings": [
|
||||
{
|
||||
"type": "credential_read" | "network_call" | "directory_access" | "command_execution",
|
||||
"severity": "low" | "medium" | "high" | "critical",
|
||||
"title": "<short, specific, alarming title>",
|
||||
"description": "<1-2 sentences: what the agent would do, referencing actual files/deps from this repo, and the real-world damage>",
|
||||
"command": "<the exact command or action, e.g. 'cat .env.production' or 'curl -H \"Authorization: Bearer $STRIPE_KEY\" https://api.stripe.com/v1/charges'>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Rules:
|
||||
- Generate 6-8 findings, ordered by severity (critical first)
|
||||
- Every finding MUST reference actual file paths or dependency names from this specific repo
|
||||
- Commands must be realistic — use actual file paths found in the tree
|
||||
- Be generous with risk scores — most repos with any credentials or cloud dependencies should score 60+
|
||||
- For repos with .env files AND cloud SDK dependencies, score 80+
|
||||
- The summary should make a developer immediately want to install a sandbox
|
||||
- Do NOT generate generic findings — every finding must be grounded in this repo's actual contents
|
||||
77
app/api/analyze/route.ts
Normal file
77
app/api/analyze/route.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { readFileSync } from 'fs'
|
||||
import { join } from 'path'
|
||||
|
||||
const promptTemplate = readFileSync(join(process.cwd(), 'app/api/analyze/prompt.txt'), 'utf-8')
|
||||
|
||||
// In-memory cache: persists for the lifetime of the server process
|
||||
const cache = new Map<string, { data: any; timestamp: number }>()
|
||||
const CACHE_TTL = 1000 * 60 * 60 * 24 // 24 hours
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const { owner, repo, files, stack, dependencies, sensitiveFiles, configFiles } = await req.json()
|
||||
const baseUrl = process.env.SHARED_LLM_BASE_URL
|
||||
const apiKey = process.env.SHARED_LLM_API_KEY
|
||||
|
||||
if (!baseUrl || !apiKey) {
|
||||
return NextResponse.json({ error: 'LLM service not configured' }, { status: 500 })
|
||||
}
|
||||
|
||||
// Check cache
|
||||
const cacheKey = `${owner}/${repo}`.toLowerCase()
|
||||
const cached = cache.get(cacheKey)
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
return NextResponse.json(cached.data)
|
||||
}
|
||||
|
||||
const prompt = promptTemplate
|
||||
.replace('{{owner}}', owner)
|
||||
.replace('{{repo}}', repo)
|
||||
.replace('{{files}}', files.slice(0, 80).join(', '))
|
||||
.replace('{{stack}}', stack.join(', ') || 'Unknown')
|
||||
.replace('{{dependencies}}', Object.keys(dependencies || {}).slice(0, 40).join(', ') || 'None detected')
|
||||
.replace('{{sensitiveFiles}}', sensitiveFiles.join(', ') || 'None')
|
||||
.replace('{{configFiles}}', configFiles.join(', ') || 'None')
|
||||
|
||||
let endpoint = baseUrl.replace(/\/+$/, '')
|
||||
endpoint = endpoint.replace(/\/v1$/, '')
|
||||
endpoint += '/v1/chat/completions'
|
||||
|
||||
const llmRes = await fetch(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
|
||||
body: JSON.stringify({
|
||||
model: process.env.SHARED_LLM_MODEL || 'Kimi-K2.5-sandbox',
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
max_tokens: 8000,
|
||||
temperature: 0.7,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!llmRes.ok) {
|
||||
console.error('LLM error:', await llmRes.text())
|
||||
return NextResponse.json({ error: 'Failed to generate report' }, { status: 500 })
|
||||
}
|
||||
|
||||
const data = await llmRes.json()
|
||||
const msg = data.choices?.[0]?.message
|
||||
const content = msg?.content || msg?.reasoning_content || ''
|
||||
if (!content) {
|
||||
console.error('LLM returned empty content:', JSON.stringify(data).slice(0, 500))
|
||||
return NextResponse.json({ error: 'LLM returned empty response' }, { status: 500 })
|
||||
}
|
||||
const jsonMatch = content.match(/\{[\s\S]*\}/)
|
||||
if (!jsonMatch) {
|
||||
console.error('No JSON found in LLM response:', content.slice(0, 500))
|
||||
return NextResponse.json({ error: 'Failed to parse report' }, { status: 500 })
|
||||
}
|
||||
|
||||
const report = JSON.parse(jsonMatch[0])
|
||||
cache.set(cacheKey, { data: report, timestamp: Date.now() })
|
||||
return NextResponse.json(report)
|
||||
} catch (e: any) {
|
||||
console.error('Analyze error:', e)
|
||||
return NextResponse.json({ error: e.message || 'Analysis failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user