vaspera 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/agents/adversary/config.d.ts +92 -0
  3. package/dist/agents/adversary/config.d.ts.map +1 -0
  4. package/dist/agents/adversary/config.js +361 -0
  5. package/dist/agents/adversary/config.js.map +1 -0
  6. package/dist/agents/adversary/index.d.ts +34 -0
  7. package/dist/agents/adversary/index.d.ts.map +1 -0
  8. package/dist/agents/adversary/index.js +756 -0
  9. package/dist/agents/adversary/index.js.map +1 -0
  10. package/dist/agents/adversary/types.d.ts +351 -0
  11. package/dist/agents/adversary/types.d.ts.map +1 -0
  12. package/dist/agents/adversary/types.js +12 -0
  13. package/dist/agents/adversary/types.js.map +1 -0
  14. package/dist/agents/index.d.ts +1 -0
  15. package/dist/agents/index.d.ts.map +1 -1
  16. package/dist/agents/index.js +2 -0
  17. package/dist/agents/index.js.map +1 -1
  18. package/dist/certification/consensus.test.js +2 -0
  19. package/dist/certification/consensus.test.js.map +1 -1
  20. package/dist/certification/store.d.ts.map +1 -1
  21. package/dist/certification/store.js +4 -0
  22. package/dist/certification/store.js.map +1 -1
  23. package/dist/certification/types.d.ts +2 -2
  24. package/dist/certification/types.d.ts.map +1 -1
  25. package/dist/certification/types.js +2 -0
  26. package/dist/certification/types.js.map +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/sbom/provenance.test.js +2 -2
  29. package/dist/sbom/provenance.test.js.map +1 -1
  30. package/dist/scanners/agent/manifest-audit.d.ts.map +1 -1
  31. package/dist/scanners/agent/manifest-audit.js +30 -18
  32. package/dist/scanners/agent/manifest-audit.js.map +1 -1
  33. package/dist/scanners/dependencies.d.ts.map +1 -1
  34. package/dist/scanners/dependencies.js +1 -2
  35. package/dist/scanners/dependencies.js.map +1 -1
  36. package/package.json +11 -2
@@ -0,0 +1,756 @@
1
+ /**
2
+ * Adversary Agent - Main Orchestrator
3
+ *
4
+ * The Adversary agent is a mythos-class ethical hacker that uses real
5
+ * Claude API reasoning to find vulnerabilities that pattern-based scanners
6
+ * miss. It coordinates four analysis phases:
7
+ *
8
+ * 1. Reconnaissance - Technology stack detection, framework identification
9
+ * 2. Attack Surface - Entry points, trust boundaries, data flows
10
+ * 3. Exploitation - LLM-powered vulnerability discovery with PoCs
11
+ * 4. Chaining - Multi-vulnerability attack path discovery
12
+ *
13
+ * @module agents/adversary
14
+ */
15
+ import { randomUUID } from "crypto";
16
+ import { glob } from "glob";
17
+ import { readFile, stat } from "fs/promises";
18
+ import * as path from "path";
19
+ import Anthropic from "@anthropic-ai/sdk";
20
+ import { DEFAULT_INCLUDE_PATTERNS, DEFAULT_EXCLUDE_PATTERNS, SECURITY_RELEVANT_PATTERNS, MODEL_PRICING, } from "./config.js";
21
+ // Re-export types and config
22
+ export * from "./types.js";
23
+ export * from "./config.js";
24
+ // ============================================================================
25
+ // Claude API Integration
26
+ // ============================================================================
27
+ let anthropicClient = null;
28
+ /**
29
+ * Initialize Anthropic client
30
+ */
31
+ function getAnthropicClient() {
32
+ if (!anthropicClient) {
33
+ const apiKey = process.env.ANTHROPIC_API_KEY;
34
+ if (!apiKey) {
35
+ throw new Error("ANTHROPIC_API_KEY environment variable is required for adversary analysis. " +
36
+ "Set it to your Claude API key to enable LLM-powered vulnerability discovery.");
37
+ }
38
+ anthropicClient = new Anthropic({ apiKey });
39
+ }
40
+ return anthropicClient;
41
+ }
42
+ /**
43
+ * Send prompt to Claude and get response
44
+ */
45
+ async function analyzeWithClaude(prompt, model) {
46
+ const client = getAnthropicClient();
47
+ const response = await client.messages.create({
48
+ model: model,
49
+ max_tokens: 8192,
50
+ system: prompt.systemPrompt,
51
+ messages: [
52
+ {
53
+ role: "user",
54
+ content: `${prompt.instructions}\n\n## Code Context\n\`\`\`\n${prompt.codeContext}\n\`\`\`\n\n${prompt.outputFormat}`,
55
+ },
56
+ ],
57
+ });
58
+ const content = response.content[0];
59
+ if (content.type !== "text") {
60
+ throw new Error("Unexpected response type from Claude");
61
+ }
62
+ return {
63
+ content: content.text,
64
+ tokensUsed: {
65
+ input: response.usage.input_tokens,
66
+ output: response.usage.output_tokens,
67
+ },
68
+ model: response.model,
69
+ stopReason: response.stop_reason,
70
+ };
71
+ }
72
+ // ============================================================================
73
+ // File Discovery
74
+ // ============================================================================
75
+ /**
76
+ * Discover files for analysis
77
+ */
78
+ async function discoverFiles(projectPath, config) {
79
+ const includePatterns = config.includePatterns || DEFAULT_INCLUDE_PATTERNS;
80
+ const excludePatterns = config.excludePatterns || DEFAULT_EXCLUDE_PATTERNS;
81
+ const allFiles = [];
82
+ for (const pattern of includePatterns) {
83
+ const matches = await glob(pattern, {
84
+ cwd: projectPath,
85
+ ignore: excludePatterns,
86
+ nodir: true,
87
+ absolute: true,
88
+ });
89
+ allFiles.push(...matches);
90
+ }
91
+ // Deduplicate
92
+ const uniqueFiles = [...new Set(allFiles)];
93
+ // Prioritize security-relevant files
94
+ const prioritized = uniqueFiles.sort((a, b) => {
95
+ const aSecurityRelevant = SECURITY_RELEVANT_PATTERNS.some((p) => a.includes(p.replace("**/*", "").replace("*", "")));
96
+ const bSecurityRelevant = SECURITY_RELEVANT_PATTERNS.some((p) => b.includes(p.replace("**/*", "").replace("*", "")));
97
+ if (aSecurityRelevant && !bSecurityRelevant)
98
+ return -1;
99
+ if (!aSecurityRelevant && bSecurityRelevant)
100
+ return 1;
101
+ return 0;
102
+ });
103
+ // Limit to max files
104
+ const maxFiles = config.maxFiles || 100;
105
+ return prioritized.slice(0, maxFiles);
106
+ }
107
+ /**
108
+ * Read file content safely
109
+ */
110
+ async function readFileContent(filePath) {
111
+ try {
112
+ const content = await readFile(filePath, "utf-8");
113
+ // Truncate very large files
114
+ if (content.length > 50000) {
115
+ return content.slice(0, 50000) + "\n\n... [truncated]";
116
+ }
117
+ return content;
118
+ }
119
+ catch {
120
+ return "";
121
+ }
122
+ }
123
+ // ============================================================================
124
+ // Phase 1: Reconnaissance
125
+ // ============================================================================
126
+ /**
127
+ * Detect technology stack from code
128
+ */
129
+ function detectTechStack(files, contents) {
130
+ const stack = {
131
+ language: "unknown",
132
+ };
133
+ // Language detection by file extensions
134
+ const extCounts = {};
135
+ for (const file of files) {
136
+ const ext = path.extname(file).toLowerCase();
137
+ extCounts[ext] = (extCounts[ext] || 0) + 1;
138
+ }
139
+ const topExt = Object.entries(extCounts).sort((a, b) => b[1] - a[1])[0];
140
+ if (topExt) {
141
+ const langMap = {
142
+ ".ts": "TypeScript",
143
+ ".tsx": "TypeScript",
144
+ ".js": "JavaScript",
145
+ ".jsx": "JavaScript",
146
+ ".py": "Python",
147
+ ".go": "Go",
148
+ ".rs": "Rust",
149
+ ".java": "Java",
150
+ ".rb": "Ruby",
151
+ ".php": "PHP",
152
+ };
153
+ stack.language = langMap[topExt[0]] || "unknown";
154
+ }
155
+ // Framework detection from content
156
+ const allContent = [...contents.values()].join("\n");
157
+ // Node.js/JavaScript frameworks
158
+ if (allContent.includes("from 'next'") || allContent.includes("'next/")) {
159
+ stack.framework = "Next.js";
160
+ stack.runtime = "Node.js";
161
+ }
162
+ else if (allContent.includes("from 'express'") || allContent.includes("require('express')")) {
163
+ stack.framework = "Express";
164
+ stack.runtime = "Node.js";
165
+ }
166
+ else if (allContent.includes("from '@hono/")) {
167
+ stack.framework = "Hono";
168
+ stack.runtime = "Node.js/Bun";
169
+ }
170
+ else if (allContent.includes("from 'fastify'")) {
171
+ stack.framework = "Fastify";
172
+ stack.runtime = "Node.js";
173
+ }
174
+ // Python frameworks
175
+ if (allContent.includes("from fastapi") || allContent.includes("import fastapi")) {
176
+ stack.framework = "FastAPI";
177
+ stack.runtime = "Python";
178
+ }
179
+ else if (allContent.includes("from flask") || allContent.includes("import flask")) {
180
+ stack.framework = "Flask";
181
+ stack.runtime = "Python";
182
+ }
183
+ else if (allContent.includes("from django") || allContent.includes("import django")) {
184
+ stack.framework = "Django";
185
+ stack.runtime = "Python";
186
+ }
187
+ // Database detection
188
+ const databases = [];
189
+ if (allContent.includes("postgres") || allContent.includes("pg.Pool") || allContent.includes("psycopg")) {
190
+ databases.push("PostgreSQL");
191
+ }
192
+ if (allContent.includes("mongodb") || allContent.includes("mongoose")) {
193
+ databases.push("MongoDB");
194
+ }
195
+ if (allContent.includes("redis") || allContent.includes("ioredis")) {
196
+ databases.push("Redis");
197
+ }
198
+ if (allContent.includes("mysql")) {
199
+ databases.push("MySQL");
200
+ }
201
+ if (databases.length > 0) {
202
+ stack.database = databases;
203
+ }
204
+ // Auth detection
205
+ const auth = [];
206
+ if (allContent.includes("jwt") || allContent.includes("jsonwebtoken")) {
207
+ auth.push("JWT");
208
+ }
209
+ if (allContent.includes("oauth") || allContent.includes("OAuth")) {
210
+ auth.push("OAuth");
211
+ }
212
+ if (allContent.includes("session") || allContent.includes("express-session")) {
213
+ auth.push("session-based");
214
+ }
215
+ if (allContent.includes("@clerk/") || allContent.includes("clerk")) {
216
+ auth.push("Clerk");
217
+ }
218
+ if (auth.length > 0) {
219
+ stack.auth = auth;
220
+ }
221
+ // API types
222
+ const apis = [];
223
+ if (allContent.includes("graphql") || allContent.includes("GraphQL")) {
224
+ apis.push("graphql");
225
+ }
226
+ if (allContent.includes("grpc") || allContent.includes("proto")) {
227
+ apis.push("grpc");
228
+ }
229
+ if (allContent.includes("WebSocket") || allContent.includes("ws://") || allContent.includes("socket.io")) {
230
+ apis.push("websocket");
231
+ }
232
+ // REST is assumed as default
233
+ apis.push("rest");
234
+ stack.apis = apis;
235
+ // Cloud detection
236
+ if (allContent.includes("vercel") || allContent.includes("@vercel/")) {
237
+ stack.cloud = "Vercel";
238
+ }
239
+ else if (allContent.includes("aws-sdk") || allContent.includes("@aws-sdk/")) {
240
+ stack.cloud = "AWS";
241
+ }
242
+ else if (allContent.includes("@google-cloud/")) {
243
+ stack.cloud = "GCP";
244
+ }
245
+ else if (allContent.includes("@azure/")) {
246
+ stack.cloud = "Azure";
247
+ }
248
+ return stack;
249
+ }
250
+ /**
251
+ * Run reconnaissance phase
252
+ */
253
+ async function runReconnaissance(projectPath, files, config) {
254
+ const startTime = Date.now();
255
+ // Read file contents
256
+ const contents = new Map();
257
+ for (const file of files.slice(0, 50)) { // Limit for recon
258
+ const content = await readFileContent(file);
259
+ if (content) {
260
+ contents.set(file, content);
261
+ }
262
+ }
263
+ // Detect tech stack
264
+ const techStack = detectTechStack(files, contents);
265
+ // Find entry points (basic pattern matching)
266
+ const entryPoints = [];
267
+ for (const [file, content] of contents) {
268
+ // Express-style routes
269
+ const routePatterns = [
270
+ /app\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/g,
271
+ /router\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/g,
272
+ ];
273
+ for (const pattern of routePatterns) {
274
+ let match;
275
+ while ((match = pattern.exec(content)) !== null) {
276
+ const lineNumber = content.slice(0, match.index).split("\n").length;
277
+ entryPoints.push({
278
+ type: "route",
279
+ path: match[2],
280
+ methods: [match[1].toUpperCase()],
281
+ file: file,
282
+ line: lineNumber,
283
+ authRequired: false, // Would need deeper analysis
284
+ inputs: [],
285
+ riskScore: 50,
286
+ });
287
+ }
288
+ }
289
+ // Next.js API routes
290
+ if (file.includes("/api/") && (file.endsWith(".ts") || file.endsWith(".js"))) {
291
+ const methodMatch = content.match(/export\s+(async\s+)?function\s+(GET|POST|PUT|DELETE|PATCH)/g);
292
+ if (methodMatch) {
293
+ const methods = methodMatch.map((m) => m.replace(/export\s+(async\s+)?function\s+/, ""));
294
+ const relativePath = file.replace(projectPath, "").replace(/\.(ts|js)$/, "");
295
+ entryPoints.push({
296
+ type: "endpoint",
297
+ path: relativePath.replace("/src/app", "").replace("/pages", ""),
298
+ methods,
299
+ file,
300
+ line: 1,
301
+ authRequired: false,
302
+ inputs: [],
303
+ riskScore: 50,
304
+ });
305
+ }
306
+ }
307
+ }
308
+ return {
309
+ techStack,
310
+ filesAnalyzed: [...contents.keys()],
311
+ entryPoints,
312
+ trustBoundaries: [], // Would need deeper analysis
313
+ thirdParty: [], // Would need dependency analysis
314
+ duration: Date.now() - startTime,
315
+ };
316
+ }
317
+ // ============================================================================
318
+ // Phase 2: Attack Surface
319
+ // ============================================================================
320
+ /**
321
+ * Run attack surface analysis phase
322
+ */
323
+ async function runAttackSurfaceAnalysis(projectPath, files, recon, config) {
324
+ const startTime = Date.now();
325
+ // For now, use the entry points from recon
326
+ // In full implementation, this would use Claude to analyze data flows
327
+ // Identify high-risk areas
328
+ const highRiskAreas = [];
329
+ // Check for auth-related files
330
+ const authFiles = files.filter((f) => f.includes("auth") || f.includes("login") || f.includes("session"));
331
+ if (authFiles.length > 0) {
332
+ highRiskAreas.push({
333
+ area: "Authentication",
334
+ reason: "Authentication logic is a prime target for bypass attacks",
335
+ files: authFiles,
336
+ });
337
+ }
338
+ // Check for database-related files
339
+ const dbFiles = files.filter((f) => f.includes("db") || f.includes("database") || f.includes("query") || f.includes("sql"));
340
+ if (dbFiles.length > 0) {
341
+ highRiskAreas.push({
342
+ area: "Database Access",
343
+ reason: "Database queries may be vulnerable to injection attacks",
344
+ files: dbFiles,
345
+ });
346
+ }
347
+ // Check for file handling
348
+ const fileHandlingFiles = files.filter((f) => f.includes("upload") || f.includes("download") || f.includes("file"));
349
+ if (fileHandlingFiles.length > 0) {
350
+ highRiskAreas.push({
351
+ area: "File Handling",
352
+ reason: "File operations may lead to path traversal or arbitrary file access",
353
+ files: fileHandlingFiles,
354
+ });
355
+ }
356
+ return {
357
+ entryPoints: recon.entryPoints,
358
+ highRiskAreas,
359
+ dataFlows: [], // Would need Claude analysis
360
+ permissionBoundaries: [], // Would need deeper analysis
361
+ duration: Date.now() - startTime,
362
+ };
363
+ }
364
+ // ============================================================================
365
+ // Phase 3: Exploitation
366
+ // ============================================================================
367
+ /**
368
+ * Build system prompt for adversarial analysis
369
+ */
370
+ function buildAdversarySystemPrompt(config) {
371
+ return `You are an expert ethical hacker and security researcher performing authorized vulnerability assessment.
372
+
373
+ ## Your Mission
374
+ Analyze code with an attacker's mindset. Your goal is to find vulnerabilities that pattern-based scanners miss - logic flaws, subtle injection vectors, authentication bypasses, and novel attack paths.
375
+
376
+ ## Analysis Approach
377
+ 1. **Think like an attacker**: Ask "How would I exploit this?"
378
+ 2. **Consider context**: Understand the application flow and trust boundaries
379
+ 3. **Look for subtle issues**: Not just obvious bugs, but design flaws and edge cases
380
+ 4. **Chain vulnerabilities**: Consider how multiple issues could be combined
381
+
382
+ ## Focus Areas
383
+ ${config.focusAreas.map((area) => `- ${area}: ${getAreaDescription(area)}`).join("\n")}
384
+
385
+ ## Output Requirements
386
+ - Be specific with file paths and line numbers
387
+ - Provide clear attack scenarios that demonstrate exploitability
388
+ - Assign realistic severity and confidence scores
389
+ - Suggest concrete remediations
390
+
391
+ ## Aggressiveness Level: ${config.aggressiveness}
392
+ ${config.aggressiveness === "passive" ? "Focus on finding issues, do not generate exploits." :
393
+ config.aggressiveness === "active" ? "Generate proof-of-concept exploits where possible." :
394
+ "Deep analysis - generate full exploits and attack chains."}
395
+
396
+ Remember: This is authorized security testing. Be thorough and think creatively.`;
397
+ }
398
+ /**
399
+ * Get description for focus area
400
+ */
401
+ function getAreaDescription(area) {
402
+ const descriptions = {
403
+ "web-app": "XSS, CSRF, clickjacking, CORS misconfigurations",
404
+ "api": "GraphQL/REST abuse, rate limiting bypasses, BOLA/IDOR",
405
+ "auth": "Authentication bypass, session fixation, OAuth flaws",
406
+ "injection": "SQL, NoSQL, XXE, SSTI, command injection",
407
+ "llm": "Prompt injection, jailbreaks, data extraction",
408
+ "infra": "Container escapes, privilege escalation, cloud misconfig",
409
+ "crypto": "Weak algorithms, key management, timing attacks",
410
+ "data-flow": "Data exposure, exfiltration paths, logging issues",
411
+ "supply-chain": "Dependency vulnerabilities, build pipeline attacks",
412
+ };
413
+ return descriptions[area] || "General security issues";
414
+ }
415
+ /**
416
+ * Parse findings from Claude response
417
+ */
418
+ function parseFindingsFromResponse(response, file, config) {
419
+ const findings = [];
420
+ try {
421
+ // Try to parse JSON if response is structured
422
+ const jsonMatch = response.match(/\[[\s\S]*\]/);
423
+ if (jsonMatch) {
424
+ const parsed = JSON.parse(jsonMatch[0]);
425
+ if (Array.isArray(parsed)) {
426
+ for (const item of parsed) {
427
+ if (item.title && item.severity) {
428
+ findings.push({
429
+ id: `adv-${randomUUID().slice(0, 8)}`,
430
+ title: item.title,
431
+ description: item.description || item.title,
432
+ severity: item.severity,
433
+ confidence: item.confidence || 70,
434
+ category: item.category || "code-quality",
435
+ focusArea: item.focusArea || config.focusAreas[0] || "web-app",
436
+ file: item.file || file,
437
+ line: item.line || 1,
438
+ codeSnippet: item.codeSnippet || "",
439
+ attackScenario: item.attackScenario || "",
440
+ exploitability: item.exploitability || "moderate",
441
+ chainPotential: item.chainPotential || [],
442
+ mitreAttackTechniques: item.mitreAttackTechniques || [],
443
+ cweIds: item.cweIds || [],
444
+ recommendation: item.recommendation || "Review and fix the identified issue",
445
+ aiReasoning: item.aiReasoning || "",
446
+ modelUsed: config.model,
447
+ });
448
+ }
449
+ }
450
+ }
451
+ }
452
+ }
453
+ catch {
454
+ // If JSON parsing fails, try to extract findings from text
455
+ // This is a fallback for unstructured responses
456
+ }
457
+ return findings;
458
+ }
459
+ /**
460
+ * Run exploitation phase with Claude analysis
461
+ */
462
+ async function runExploitation(projectPath, files, recon, attackSurface, config) {
463
+ const startTime = Date.now();
464
+ const findings = [];
465
+ let totalTokens = 0;
466
+ // Prioritize high-risk files
467
+ const prioritizedFiles = [
468
+ ...attackSurface.highRiskAreas.flatMap((area) => area.files),
469
+ ...files.filter((f) => !attackSurface.highRiskAreas.some((area) => area.files.includes(f))),
470
+ ].slice(0, config.maxFiles || 50);
471
+ // Analyze each file with Claude
472
+ for (const file of prioritizedFiles) {
473
+ const content = await readFileContent(file);
474
+ if (!content || content.length < 100)
475
+ continue;
476
+ try {
477
+ const prompt = {
478
+ systemPrompt: buildAdversarySystemPrompt(config),
479
+ codeContext: content,
480
+ instructions: `Analyze this file for security vulnerabilities. Focus on ${config.focusAreas.join(", ")}.
481
+
482
+ File: ${path.relative(projectPath, file)}
483
+ Technology Stack: ${JSON.stringify(recon.techStack)}
484
+
485
+ Look for:
486
+ - Input validation issues
487
+ - Authentication/authorization flaws
488
+ - Injection vulnerabilities
489
+ - Logic errors
490
+ - Cryptographic weaknesses
491
+ - Race conditions
492
+ - Information disclosure`,
493
+ outputFormat: `Return findings as a JSON array:
494
+ [{
495
+ "title": "Vulnerability Title",
496
+ "description": "Detailed description",
497
+ "severity": "critical|high|medium|low",
498
+ "confidence": 0-100,
499
+ "category": "category-name",
500
+ "focusArea": "focus-area",
501
+ "line": line_number,
502
+ "codeSnippet": "relevant code",
503
+ "attackScenario": "Step-by-step attack",
504
+ "exploitability": "trivial|easy|moderate|hard|expert",
505
+ "cweIds": ["CWE-XX"],
506
+ "recommendation": "How to fix"
507
+ }]
508
+
509
+ If no vulnerabilities found, return empty array [].`,
510
+ };
511
+ const response = await analyzeWithClaude(prompt, config.model);
512
+ totalTokens += response.tokensUsed.input + response.tokensUsed.output;
513
+ const fileFindings = parseFindingsFromResponse(response.content, file, config);
514
+ findings.push(...fileFindings);
515
+ // Check time limit
516
+ if (Date.now() - startTime > config.maxAnalysisTime) {
517
+ break;
518
+ }
519
+ }
520
+ catch (error) {
521
+ // Log error but continue with other files
522
+ console.error(`Error analyzing ${file}:`, error);
523
+ }
524
+ }
525
+ return {
526
+ findings,
527
+ filesAnalyzed: prioritizedFiles.length,
528
+ tokensUsed: totalTokens,
529
+ duration: Date.now() - startTime,
530
+ };
531
+ }
532
+ // ============================================================================
533
+ // Phase 4: Chaining
534
+ // ============================================================================
535
+ /**
536
+ * Analyze findings for exploit chains
537
+ */
538
+ async function runChainingAnalysis(findings, config) {
539
+ const startTime = Date.now();
540
+ const chains = [];
541
+ const chainedFindingIds = new Set();
542
+ // Simple chaining logic - look for related findings
543
+ // In full implementation, this would use Claude for deeper analysis
544
+ // Group findings by category
545
+ const byCategory = new Map();
546
+ for (const finding of findings) {
547
+ const category = finding.category;
548
+ if (!byCategory.has(category)) {
549
+ byCategory.set(category, []);
550
+ }
551
+ byCategory.get(category).push(finding);
552
+ }
553
+ // Look for common chains
554
+ // XSS + Session = Session Hijacking
555
+ const xssFindings = findings.filter((f) => f.category === "xss");
556
+ const sessionFindings = findings.filter((f) => f.category === "session-management" || f.category === "authentication");
557
+ if (xssFindings.length > 0 && sessionFindings.length > 0) {
558
+ const chain = {
559
+ id: `chain-${randomUUID().slice(0, 8)}`,
560
+ name: "XSS to Session Hijacking",
561
+ description: "Cross-site scripting can be chained with session vulnerabilities to hijack user sessions",
562
+ findingIds: [...xssFindings.map((f) => f.id), ...sessionFindings.map((f) => f.id)],
563
+ combinedSeverity: "critical",
564
+ impact: "Full account takeover through stolen session credentials",
565
+ mitreChain: ["T1189", "T1539"],
566
+ likelihood: "high",
567
+ };
568
+ chains.push(chain);
569
+ xssFindings.forEach((f) => chainedFindingIds.add(f.id));
570
+ sessionFindings.forEach((f) => chainedFindingIds.add(f.id));
571
+ }
572
+ // SQL Injection + Auth Bypass = Full DB Access
573
+ const sqlFindings = findings.filter((f) => f.category === "sql-injection");
574
+ const authFindings = findings.filter((f) => f.category === "auth-bypass");
575
+ if (sqlFindings.length > 0 && authFindings.length > 0) {
576
+ const chain = {
577
+ id: `chain-${randomUUID().slice(0, 8)}`,
578
+ name: "SQL Injection to Database Compromise",
579
+ description: "SQL injection combined with authentication bypass enables complete database access",
580
+ findingIds: [...sqlFindings.map((f) => f.id), ...authFindings.map((f) => f.id)],
581
+ combinedSeverity: "critical",
582
+ impact: "Complete database compromise including sensitive data exfiltration",
583
+ mitreChain: ["T1190", "T1078", "T1005"],
584
+ likelihood: "high",
585
+ };
586
+ chains.push(chain);
587
+ sqlFindings.forEach((f) => chainedFindingIds.add(f.id));
588
+ authFindings.forEach((f) => chainedFindingIds.add(f.id));
589
+ }
590
+ // SSRF + Internal API = Internal Service Access
591
+ const ssrfFindings = findings.filter((f) => f.category === "ssrf");
592
+ const apiFindings = findings.filter((f) => f.category === "api-security");
593
+ if (ssrfFindings.length > 0 && apiFindings.length > 0) {
594
+ const chain = {
595
+ id: `chain-${randomUUID().slice(0, 8)}`,
596
+ name: "SSRF to Internal Service Compromise",
597
+ description: "Server-side request forgery can access internal APIs and services",
598
+ findingIds: [...ssrfFindings.map((f) => f.id), ...apiFindings.map((f) => f.id)],
599
+ combinedSeverity: "critical",
600
+ impact: "Access to internal services, potential cloud metadata exposure",
601
+ mitreChain: ["T1190", "T1552"],
602
+ likelihood: "medium",
603
+ };
604
+ chains.push(chain);
605
+ ssrfFindings.forEach((f) => chainedFindingIds.add(f.id));
606
+ apiFindings.forEach((f) => chainedFindingIds.add(f.id));
607
+ }
608
+ // Isolated findings (not part of any chain)
609
+ const isolatedFindings = findings
610
+ .filter((f) => !chainedFindingIds.has(f.id))
611
+ .map((f) => f.id);
612
+ return {
613
+ chains,
614
+ isolatedFindings,
615
+ duration: Date.now() - startTime,
616
+ };
617
+ }
618
+ // ============================================================================
619
+ // Main Entry Point
620
+ // ============================================================================
621
+ /**
622
+ * Run full adversary analysis
623
+ */
624
+ export async function runAdversaryAnalysis(projectPath, config) {
625
+ const analysisId = `adv-${randomUUID().slice(0, 12)}`;
626
+ const startTime = Date.now();
627
+ try {
628
+ // Validate project path
629
+ const stats = await stat(projectPath);
630
+ if (!stats.isDirectory()) {
631
+ throw new Error(`Project path is not a directory: ${projectPath}`);
632
+ }
633
+ // Discover files
634
+ const files = await discoverFiles(projectPath, config);
635
+ if (files.length === 0) {
636
+ return {
637
+ success: false,
638
+ analysisId,
639
+ projectPath,
640
+ config,
641
+ findings: [],
642
+ chains: [],
643
+ totalTokensUsed: 0,
644
+ totalDuration: Date.now() - startTime,
645
+ error: "No analyzable files found in project",
646
+ recommendations: [],
647
+ };
648
+ }
649
+ // Phase 1: Reconnaissance
650
+ const recon = await runReconnaissance(projectPath, files, config);
651
+ // Phase 2: Attack Surface
652
+ const attackSurface = await runAttackSurfaceAnalysis(projectPath, files, recon, config);
653
+ // Phase 3: Exploitation
654
+ const exploitation = await runExploitation(projectPath, files, recon, attackSurface, config);
655
+ // Phase 4: Chaining (if enabled)
656
+ let chaining;
657
+ if (config.enableChaining && exploitation.findings.length > 1) {
658
+ chaining = await runChainingAnalysis(exploitation.findings, config);
659
+ }
660
+ // Generate recommendations
661
+ const recommendations = [];
662
+ const criticalCount = exploitation.findings.filter((f) => f.severity === "critical").length;
663
+ const highCount = exploitation.findings.filter((f) => f.severity === "high").length;
664
+ if (criticalCount > 0) {
665
+ recommendations.push(`CRITICAL: ${criticalCount} critical vulnerabilities require immediate remediation.`);
666
+ }
667
+ if (highCount > 0) {
668
+ recommendations.push(`HIGH: ${highCount} high-severity issues should be fixed before deployment.`);
669
+ }
670
+ if (chaining && chaining.chains.length > 0) {
671
+ recommendations.push(`WARNING: ${chaining.chains.length} exploit chains discovered that could amplify impact.`);
672
+ }
673
+ // Focus area specific recommendations
674
+ for (const area of config.focusAreas) {
675
+ const areaFindings = exploitation.findings.filter((f) => f.focusArea === area);
676
+ if (areaFindings.length > 0) {
677
+ recommendations.push(`${area.toUpperCase()}: ${areaFindings.length} issues found. Review ${getAreaDescription(area)}.`);
678
+ }
679
+ }
680
+ return {
681
+ success: true,
682
+ analysisId,
683
+ projectPath,
684
+ config,
685
+ recon,
686
+ attackSurface,
687
+ exploitation,
688
+ chaining,
689
+ findings: exploitation.findings,
690
+ chains: chaining?.chains || [],
691
+ totalTokensUsed: exploitation.tokensUsed,
692
+ totalDuration: Date.now() - startTime,
693
+ recommendations,
694
+ };
695
+ }
696
+ catch (error) {
697
+ return {
698
+ success: false,
699
+ analysisId,
700
+ projectPath,
701
+ config,
702
+ findings: [],
703
+ chains: [],
704
+ totalTokensUsed: 0,
705
+ totalDuration: Date.now() - startTime,
706
+ error: error instanceof Error ? error.message : String(error),
707
+ recommendations: [],
708
+ };
709
+ }
710
+ }
711
+ /**
712
+ * Convert adversary findings to certification findings
713
+ */
714
+ export function adversaryToFindings(result) {
715
+ return result.findings.map((f) => ({
716
+ id: f.id,
717
+ severity: f.severity,
718
+ category: f.category,
719
+ description: `${f.title}: ${f.description}`,
720
+ evidence: `File: ${f.file}:${f.line}\nCode: ${f.codeSnippet.slice(0, 200)}\nAttack: ${f.attackScenario}`,
721
+ confidence: f.confidence,
722
+ verifications: [],
723
+ created_at: new Date().toISOString(),
724
+ scanner_source: "adversary",
725
+ metadata: {
726
+ focusArea: f.focusArea,
727
+ exploitability: f.exploitability,
728
+ cweIds: f.cweIds,
729
+ mitreAttackTechniques: f.mitreAttackTechniques,
730
+ recommendation: f.recommendation,
731
+ aiReasoning: f.aiReasoning,
732
+ modelUsed: f.modelUsed,
733
+ hasPoC: !!f.proofOfConcept,
734
+ },
735
+ }));
736
+ }
737
+ /**
738
+ * Estimate cost for adversary analysis
739
+ */
740
+ export function estimateAdversaryCost(filesCount, config) {
741
+ // Rough estimates based on typical analysis patterns
742
+ const tokensPerFile = config.aggressiveness === "aggressive" ? 8000 :
743
+ config.aggressiveness === "active" ? 5000 : 3000;
744
+ const estimatedTokens = filesCount * tokensPerFile;
745
+ const pricing = MODEL_PRICING[config.model];
746
+ // Assume 70% input, 30% output
747
+ const inputTokens = estimatedTokens * 0.7;
748
+ const outputTokens = estimatedTokens * 0.3;
749
+ const inputCost = (inputTokens / 1_000_000) * pricing.input;
750
+ const outputCost = (outputTokens / 1_000_000) * pricing.output;
751
+ return {
752
+ estimatedCost: Math.round((inputCost + outputCost) * 100) / 100,
753
+ estimatedTokens: Math.round(estimatedTokens),
754
+ };
755
+ }
756
+ //# sourceMappingURL=index.js.map