import fs from 'fs'; import path from 'path'; import { Finding } from '../types.js'; import { loadConfig } from '../config.js'; interface ThreatModelOutput { summary: string; threats: Array<{ category: string; threat: string; risk: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'; mitigation: string; }>; } function gatherProjectContext(projectPath: string): string { const parts: string[] = []; // README for (const name of ['README.md', 'readme.md']) { const readmePath = path.join(projectPath, name); if (fs.existsSync(readmePath)) { const content = fs.readFileSync(readmePath, 'utf-8').slice(0, 3000); parts.push(`## README\n${content}`); break; } } // package.json const pkgPath = path.join(projectPath, 'package.json'); if (fs.existsSync(pkgPath)) { try { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); parts.push(`## package.json\nName: ${pkg.name}\nDescription: ${pkg.description}\nDependencies: ${Object.keys(pkg.dependencies || {}).join(', ')}`); } catch { /* skip */ } } // MANIFEST.json const manifestPath = path.join(projectPath, '.ai', 'handoff', 'MANIFEST.json'); if (fs.existsSync(manifestPath)) { try { const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); parts.push(`## AAHP Context\nPhase: ${manifest.last_session?.phase || 'unknown'}\nContext: ${manifest.quick_context || 'none'}`); } catch { /* skip */ } } return parts.join('\n\n'); } export async function scanThreatModel(projectPath: string): Promise { const findings: Finding[] = []; const config = loadConfig(); const apiKey = config.anthropicApiKey || process.env.ANTHROPIC_API_KEY; if (!apiKey) { findings.push({ module: 'threat-model', severity: 'INFO', title: 'Threat model skipped - no API key', description: 'Set ANTHROPIC_API_KEY or run `secure-dev-ai config --api-key ` to enable AI threat modeling.', remediation: 'Configure API key to enable threat model generation.', }); return findings; } const context = gatherProjectContext(projectPath); if (!context) return findings; try { const { default: Anthropic } = await import('@anthropic-ai/sdk'); const client = new Anthropic({ apiKey }); const response = await client.messages.create({ model: 'claude-haiku-4-5', max_tokens: 1024, messages: [{ role: 'user', content: `You are a security architect. Analyze this project and produce a concise STRIDE threat model. Project context: ${context} Respond ONLY with valid JSON matching this schema: { "summary": "one sentence description of main security risks", "threats": [ { "category": "Spoofing|Tampering|Repudiation|Information Disclosure|Denial of Service|Elevation of Privilege", "threat": "specific threat description", "risk": "CRITICAL|HIGH|MEDIUM|LOW", "mitigation": "specific mitigation step" } ] } Include 3-7 most important threats only. Be specific to this project.`, }], }); const text = response.content[0].type === 'text' ? response.content[0].text : ''; const jsonMatch = text.match(/\{[\s\S]*\}/); if (!jsonMatch) throw new Error('No JSON in response'); const model: ThreatModelOutput = JSON.parse(jsonMatch[0]); for (const threat of model.threats) { findings.push({ module: 'threat-model', severity: threat.risk, title: `[STRIDE:${threat.category}] ${threat.threat}`, description: `Threat model finding: ${threat.threat}`, remediation: threat.mitigation, }); } // Write THREAT-MODEL.md to project const threatModelPath = path.join(projectPath, 'THREAT-MODEL.md'); const mdContent = `# Threat Model - ${path.basename(projectPath)} > Auto-generated by secure-dev-ai on ${new Date().toISOString().split('T')[0]} > Based on STRIDE methodology ## Summary ${model.summary} ## Threats ${model.threats.map(t => `### [${t.category}] ${t.threat} - **Risk:** ${t.risk} - **Mitigation:** ${t.mitigation} `).join('\n')} --- *Regenerate: \`secure-dev-ai threat-model ${path.basename(projectPath)}\`* `; fs.writeFileSync(threatModelPath, mdContent); } catch (e) { findings.push({ module: 'threat-model', severity: 'INFO', title: 'Threat model generation failed', description: `Could not generate threat model: ${e instanceof Error ? e.message : String(e)}`, }); } return findings; }