vaspera 2.5.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.
- package/CHANGELOG.md +62 -0
- package/dist/agents/adversary/config.d.ts +92 -0
- package/dist/agents/adversary/config.d.ts.map +1 -0
- package/dist/agents/adversary/config.js +361 -0
- package/dist/agents/adversary/config.js.map +1 -0
- package/dist/agents/adversary/index.d.ts +34 -0
- package/dist/agents/adversary/index.d.ts.map +1 -0
- package/dist/agents/adversary/index.js +756 -0
- package/dist/agents/adversary/index.js.map +1 -0
- package/dist/agents/adversary/types.d.ts +351 -0
- package/dist/agents/adversary/types.d.ts.map +1 -0
- package/dist/agents/adversary/types.js +12 -0
- package/dist/agents/adversary/types.js.map +1 -0
- package/dist/agents/agent-integrity.test.d.ts +5 -0
- package/dist/agents/agent-integrity.test.d.ts.map +1 -0
- package/dist/agents/agent-integrity.test.js +364 -0
- package/dist/agents/agent-integrity.test.js.map +1 -0
- package/dist/agents/agent-privacy.test.d.ts +5 -0
- package/dist/agents/agent-privacy.test.d.ts.map +1 -0
- package/dist/agents/agent-privacy.test.js +373 -0
- package/dist/agents/agent-privacy.test.js.map +1 -0
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +2 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/certification/consensus.test.js +2 -0
- package/dist/certification/consensus.test.js.map +1 -1
- package/dist/certification/store.d.ts.map +1 -1
- package/dist/certification/store.js +4 -0
- package/dist/certification/store.js.map +1 -1
- package/dist/certification/types.d.ts +2 -2
- package/dist/certification/types.d.ts.map +1 -1
- package/dist/certification/types.js +2 -0
- package/dist/certification/types.js.map +1 -1
- package/dist/compliance/mapper.d.ts.map +1 -1
- package/dist/compliance/mapper.js +2 -2
- package/dist/compliance/mapper.js.map +1 -1
- package/dist/compliance/nist-800-53.d.ts +34 -0
- package/dist/compliance/nist-800-53.d.ts.map +1 -0
- package/dist/compliance/nist-800-53.js +664 -0
- package/dist/compliance/nist-800-53.js.map +1 -0
- package/dist/config/flags.test.d.ts +5 -0
- package/dist/config/flags.test.d.ts.map +1 -0
- package/dist/config/flags.test.js +489 -0
- package/dist/config/flags.test.js.map +1 -0
- package/dist/enterprise/policy/opa.test.js +4 -1
- package/dist/enterprise/policy/opa.test.js.map +1 -1
- package/dist/http-server.js +2 -1
- package/dist/http-server.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/observability/otel.test.d.ts +5 -0
- package/dist/observability/otel.test.d.ts.map +1 -0
- package/dist/observability/otel.test.js +269 -0
- package/dist/observability/otel.test.js.map +1 -0
- package/dist/plugins/loader.test.d.ts +5 -0
- package/dist/plugins/loader.test.d.ts.map +1 -0
- package/dist/plugins/loader.test.js +337 -0
- package/dist/plugins/loader.test.js.map +1 -0
- package/dist/sbom/provenance.test.js +2 -2
- package/dist/sbom/provenance.test.js.map +1 -1
- package/dist/scanners/agent/manifest-audit.d.ts.map +1 -1
- package/dist/scanners/agent/manifest-audit.js +30 -18
- package/dist/scanners/agent/manifest-audit.js.map +1 -1
- package/dist/scanners/dependencies.d.ts.map +1 -1
- package/dist/scanners/dependencies.js +1 -2
- package/dist/scanners/dependencies.js.map +1 -1
- package/package.json +12 -3
|
@@ -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
|