guardvibe 3.0.0 → 3.0.3

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.
@@ -101,6 +101,41 @@ function parseImports(file, content) {
101
101
  });
102
102
  }
103
103
  }
104
+ // CommonJS: const X = require('./mod') — default require
105
+ {
106
+ const re = /(?:const|let|var)\s+([\w$]+)\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
107
+ let m;
108
+ while ((m = re.exec(line)) !== null) {
109
+ imports.push({
110
+ importer: file,
111
+ source: normalizePath(file, m[2]),
112
+ names: new Map(),
113
+ defaultName: m[1].trim(),
114
+ line: i + 1,
115
+ });
116
+ }
117
+ }
118
+ // CommonJS: const { a, b } = require('./mod') — destructured require
119
+ {
120
+ const re = /(?:const|let|var)\s+\{([^}]+)\}\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
121
+ let m;
122
+ while ((m = re.exec(line)) !== null) {
123
+ const names = new Map();
124
+ for (const spec of m[1].split(",")) {
125
+ const parts = spec.trim().split(/\s*:\s*/);
126
+ const exported = parts[0].trim();
127
+ const local = (parts[1] ?? parts[0]).trim();
128
+ if (exported)
129
+ names.set(local, exported);
130
+ }
131
+ imports.push({
132
+ importer: file,
133
+ source: normalizePath(file, m[2]),
134
+ names,
135
+ line: i + 1,
136
+ });
137
+ }
138
+ }
104
139
  }
105
140
  return imports;
106
141
  }
@@ -151,6 +186,39 @@ function parseExports(file, content) {
151
186
  names.set(m[1], m[1]);
152
187
  }
153
188
  }
189
+ // CommonJS: module.exports = { a, b }
190
+ {
191
+ const re = /module\.exports\s*=\s*\{([^}]+)\}/;
192
+ const m = re.exec(line);
193
+ if (m) {
194
+ for (const spec of m[1].split(",")) {
195
+ const parts = spec.trim().split(/\s*:\s*/);
196
+ const name = parts[0].trim();
197
+ const local = (parts[1] ?? parts[0]).trim();
198
+ if (name)
199
+ names.set(name, local);
200
+ }
201
+ }
202
+ }
203
+ // CommonJS: module.exports = funcName (default export)
204
+ {
205
+ const re = /module\.exports\s*=\s*([\w$]+)\s*;?\s*$/;
206
+ const m = re.exec(line);
207
+ if (m && !line.includes("{")) {
208
+ hasDefault = true;
209
+ defaultLocal = m[1];
210
+ }
211
+ }
212
+ // CommonJS: exports.name = funcName
213
+ {
214
+ const re = /exports\.([\w$]+)\s*=\s*([\w$]+)/g;
215
+ let m;
216
+ while ((m = re.exec(line)) !== null) {
217
+ if (!line.startsWith("module.")) {
218
+ names.set(m[1], m[2]);
219
+ }
220
+ }
221
+ }
154
222
  }
155
223
  return { file, names, hasDefault, defaultLocal };
156
224
  }
@@ -242,6 +310,45 @@ function checkParamFlowsToSink(paramName, body, startLine) {
242
310
  }
243
311
  return null;
244
312
  }
313
+ // Check if a function parameter flows to a return statement (for return value taint tracking).
314
+ // Returns true only if the param (or a derived variable) IS the return value,
315
+ // not merely referenced inside a function call's arguments.
316
+ function checkParamFlowsToReturn(paramName, body) {
317
+ const lines = body.split("\n");
318
+ const taintedNames = new Set([paramName]);
319
+ const assignPattern = /(?:const|let|var)\s+([\w$]+)\s*=\s*(.*)/;
320
+ for (const line of lines) {
321
+ const m = assignPattern.exec(line);
322
+ if (m) {
323
+ for (const t of taintedNames) {
324
+ if (m[2].includes(t)) {
325
+ taintedNames.add(m[1]);
326
+ break;
327
+ }
328
+ }
329
+ }
330
+ }
331
+ for (const line of lines) {
332
+ const returnMatch = /\breturn\s+(.+?)[\s;]*$/.exec(line);
333
+ if (!returnMatch)
334
+ continue;
335
+ const returnExpr = returnMatch[1].trim();
336
+ // Direct return of tainted variable (e.g., "return trimmed;")
337
+ for (const t of taintedNames) {
338
+ if (returnExpr === t)
339
+ return true;
340
+ }
341
+ // Return of expression that uses tainted var directly (e.g., "return x + y;")
342
+ // but NOT as argument inside a function call (e.g., "return fn(x)" — x is consumed, not returned)
343
+ // Only match if tainted name appears outside parenthesized call args
344
+ const withoutCalls = returnExpr.replace(/\w+\s*\([^)]*\)/g, "");
345
+ for (const t of taintedNames) {
346
+ if (withoutCalls.includes(t))
347
+ return true;
348
+ }
349
+ }
350
+ return false;
351
+ }
245
352
  function findTaintedExports(files) {
246
353
  const taintedExports = [];
247
354
  for (const file of files) {
@@ -263,8 +370,25 @@ function findTaintedExports(files) {
263
370
  taintedParams.set(pIdx, paramAsTainted);
264
371
  }
265
372
  }
266
- if (taintedParams.size > 0) {
267
- taintedExports.push({ file: file.path, exportName: exportedName === "default" ? fn.name : exportedName, taintedParams });
373
+ // Check if any param flows to a return statement
374
+ const taintedReturnParams = [];
375
+ for (let pIdx = 0; pIdx < fn.params.length; pIdx++) {
376
+ const param = fn.params[pIdx];
377
+ if (!param)
378
+ continue;
379
+ if (checkParamFlowsToReturn(param, fn.body)) {
380
+ taintedReturnParams.push(pIdx);
381
+ }
382
+ }
383
+ const returnsTainted = taintedReturnParams.length > 0;
384
+ if (taintedParams.size > 0 || (returnsTainted && taintedReturnParams.length > 0)) {
385
+ taintedExports.push({
386
+ file: file.path,
387
+ exportName: exportedName === "default" ? fn.name : exportedName,
388
+ taintedParams,
389
+ returnsTainted: returnsTainted && taintedReturnParams.length > 0,
390
+ taintedReturnParams,
391
+ });
268
392
  }
269
393
  }
270
394
  }
@@ -310,7 +434,7 @@ function findTaintedCallSites(files, allImports, taintedExports) {
310
434
  let changed = true;
311
435
  let iterations = 0;
312
436
  const taintedSet = new Set(taintedVars.map(v => v.name));
313
- while (changed && iterations < 10) {
437
+ while (changed && iterations < 25) {
314
438
  changed = false;
315
439
  iterations++;
316
440
  for (let i = 0; i < lines.length; i++) {
@@ -353,6 +477,25 @@ function findTaintedCallSites(files, allImports, taintedExports) {
353
477
  if (!callPattern.test(lines[i]))
354
478
  continue;
355
479
  const args = extractCallArgs(lines[i], localName);
480
+ // Return value tracking: if function returnsTainted and a tainted arg is passed,
481
+ // mark the receiving variable as tainted
482
+ if (te.returnsTainted) {
483
+ const assignMatch = /(?:const|let|var)\s+([\w$]+)\s*=/.exec(lines[i]);
484
+ if (assignMatch) {
485
+ const receivingVar = assignMatch[1];
486
+ const hasTaintedArg = te.taintedReturnParams.some(pIdx => {
487
+ const arg = args[pIdx];
488
+ if (!arg)
489
+ return false;
490
+ return taintedVars.some(v => arg.includes(v.name)) ||
491
+ TAINT_SOURCES.some(src => { src.pattern.lastIndex = 0; return src.pattern.test(arg); });
492
+ });
493
+ if (hasTaintedArg && !taintedSet.has(receivingVar)) {
494
+ taintedSet.add(receivingVar);
495
+ taintedVars.push({ name: receivingVar, line: i + 1, sourceType: "return-propagated" });
496
+ }
497
+ }
498
+ }
356
499
  for (const [paramIdx, sinkInfo] of te.taintedParams) {
357
500
  const argAtIdx = args[paramIdx];
358
501
  if (!argAtIdx)
@@ -0,0 +1,33 @@
1
+ /**
2
+ * LLM-powered deep scan — sends suspicious code to an LLM API for
3
+ * semantic analysis of IDOR, business logic, race conditions, and
4
+ * other issues that pattern-matching alone cannot detect.
5
+ *
6
+ * Uses native fetch — no extra dependencies.
7
+ */
8
+ export interface DeepScanFinding {
9
+ type: string;
10
+ severity: "critical" | "high" | "medium" | "low";
11
+ description: string;
12
+ location: string;
13
+ fix: string;
14
+ }
15
+ /**
16
+ * Build a structured prompt for the LLM to analyze code.
17
+ */
18
+ export declare function buildDeepScanPrompt(code: string, language: string, existingFindings: string[]): string;
19
+ /**
20
+ * Parse LLM response into structured findings.
21
+ * Handles raw JSON, JSON in markdown code blocks, and malformed responses.
22
+ */
23
+ export declare function parseDeepScanResult(response: string): DeepScanFinding[];
24
+ /**
25
+ * Format deep scan findings as markdown or JSON.
26
+ */
27
+ export declare function formatDeepScanFindings(findings: DeepScanFinding[], format: "markdown" | "json"): string;
28
+ /**
29
+ * Call an LLM API for deep analysis. Uses native fetch.
30
+ * Supports Anthropic (ANTHROPIC_API_KEY) or OpenAI (OPENAI_API_KEY).
31
+ * Returns null if no API key is available.
32
+ */
33
+ export declare function callLLM(prompt: string): Promise<string | null>;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * LLM-powered deep scan — sends suspicious code to an LLM API for
3
+ * semantic analysis of IDOR, business logic, race conditions, and
4
+ * other issues that pattern-matching alone cannot detect.
5
+ *
6
+ * Uses native fetch — no extra dependencies.
7
+ */
8
+ const FOCUS_AREAS = [
9
+ "IDOR (Insecure Direct Object Reference) — can users access resources belonging to other users?",
10
+ "Business logic flaws — are there authorization bypasses, price manipulation, or state machine violations?",
11
+ "Race conditions — are there TOCTOU issues, double-spend, or concurrent mutation without locking?",
12
+ "Stale auth/session — are tokens validated on every request? Can expired sessions still perform actions?",
13
+ "Mass assignment — can users set fields they shouldn't (role, isAdmin, price)?",
14
+ "Privilege escalation — can a regular user perform admin actions through parameter manipulation?",
15
+ ];
16
+ /**
17
+ * Build a structured prompt for the LLM to analyze code.
18
+ */
19
+ export function buildDeepScanPrompt(code, language, existingFindings) {
20
+ const lines = [
21
+ "You are a senior application security engineer performing a deep code review.",
22
+ "Analyze the following code for security vulnerabilities that automated pattern-matching scanners miss.",
23
+ "",
24
+ "## Focus Areas",
25
+ "",
26
+ ];
27
+ for (const area of FOCUS_AREAS) {
28
+ lines.push(`- ${area}`);
29
+ }
30
+ lines.push("");
31
+ lines.push("## Code");
32
+ lines.push("");
33
+ lines.push(`Language: ${language}`);
34
+ lines.push("```");
35
+ lines.push(code);
36
+ lines.push("```");
37
+ if (existingFindings.length > 0) {
38
+ lines.push("");
39
+ lines.push("## Already Detected (by pattern scanner)");
40
+ lines.push("Do NOT repeat these — only report NEW findings:");
41
+ lines.push("");
42
+ for (const f of existingFindings) {
43
+ lines.push(`- ${f}`);
44
+ }
45
+ }
46
+ lines.push("");
47
+ lines.push("## Response Format");
48
+ lines.push("Return ONLY a JSON object with this structure:");
49
+ lines.push("```json");
50
+ lines.push(JSON.stringify({
51
+ findings: [{
52
+ type: "IDOR | race-condition | business-logic | stale-auth | mass-assignment | privilege-escalation",
53
+ severity: "critical | high | medium | low",
54
+ description: "Clear description of the vulnerability",
55
+ location: "line number or code reference",
56
+ fix: "Specific remediation guidance",
57
+ }],
58
+ }, null, 2));
59
+ lines.push("```");
60
+ lines.push("If no vulnerabilities found, return: { \"findings\": [] }");
61
+ return lines.join("\n");
62
+ }
63
+ /**
64
+ * Parse LLM response into structured findings.
65
+ * Handles raw JSON, JSON in markdown code blocks, and malformed responses.
66
+ */
67
+ export function parseDeepScanResult(response) {
68
+ if (!response || response.trim().length === 0)
69
+ return [];
70
+ let jsonStr = response.trim();
71
+ // Extract JSON from markdown code block
72
+ const codeBlockMatch = /```(?:json)?\s*\n?([\s\S]*?)\n?```/.exec(jsonStr);
73
+ if (codeBlockMatch) {
74
+ jsonStr = codeBlockMatch[1].trim();
75
+ }
76
+ try {
77
+ const parsed = JSON.parse(jsonStr);
78
+ if (!parsed.findings || !Array.isArray(parsed.findings))
79
+ return [];
80
+ return parsed.findings.filter((f) => f.type && f.severity && f.description && f.location && f.fix);
81
+ }
82
+ catch {
83
+ return [];
84
+ }
85
+ }
86
+ /**
87
+ * Format deep scan findings as markdown or JSON.
88
+ */
89
+ export function formatDeepScanFindings(findings, format) {
90
+ if (format === "json") {
91
+ return JSON.stringify({
92
+ summary: {
93
+ total: findings.length,
94
+ critical: findings.filter(f => f.severity === "critical").length,
95
+ high: findings.filter(f => f.severity === "high").length,
96
+ medium: findings.filter(f => f.severity === "medium").length,
97
+ low: findings.filter(f => f.severity === "low").length,
98
+ },
99
+ findings,
100
+ });
101
+ }
102
+ if (findings.length === 0) {
103
+ return "## Deep Scan Results\n\nNo additional vulnerabilities found beyond pattern-matching results.";
104
+ }
105
+ const lines = [
106
+ `## Deep Scan Results`,
107
+ ``,
108
+ `Found ${findings.length} finding(s) via LLM analysis:`,
109
+ ``,
110
+ ];
111
+ const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
112
+ findings.sort((a, b) => (severityOrder[a.severity] ?? 4) - (severityOrder[b.severity] ?? 4));
113
+ for (const f of findings) {
114
+ lines.push(`### [${f.severity.toUpperCase()}] ${f.type}`);
115
+ lines.push(`**Location:** ${f.location}`);
116
+ lines.push(`${f.description}`);
117
+ lines.push(`**Fix:** ${f.fix}`);
118
+ lines.push(``);
119
+ }
120
+ return lines.join("\n");
121
+ }
122
+ /**
123
+ * Call an LLM API for deep analysis. Uses native fetch.
124
+ * Supports Anthropic (ANTHROPIC_API_KEY) or OpenAI (OPENAI_API_KEY).
125
+ * Returns null if no API key is available.
126
+ */
127
+ export async function callLLM(prompt) {
128
+ // guardvibe-ignore — API URLs are hardcoded trusted endpoints, not user-controlled
129
+ const anthropicKey = process.env.ANTHROPIC_API_KEY;
130
+ const openaiKey = process.env.OPENAI_API_KEY;
131
+ if (anthropicKey) {
132
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
133
+ method: "POST",
134
+ headers: {
135
+ "Content-Type": "application/json",
136
+ "x-api-key": anthropicKey,
137
+ "anthropic-version": "2023-06-01",
138
+ },
139
+ body: JSON.stringify({
140
+ model: "claude-sonnet-4-6",
141
+ max_tokens: 2048,
142
+ messages: [{ role: "user", content: prompt }],
143
+ }),
144
+ });
145
+ if (!res.ok)
146
+ return null;
147
+ const data = await res.json();
148
+ return data.content?.[0]?.text ?? null;
149
+ }
150
+ if (openaiKey) {
151
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
152
+ method: "POST",
153
+ headers: {
154
+ "Content-Type": "application/json",
155
+ "Authorization": `Bearer ${openaiKey}`,
156
+ },
157
+ body: JSON.stringify({
158
+ model: "gpt-4o",
159
+ max_tokens: 2048,
160
+ messages: [{ role: "user", content: prompt }],
161
+ }),
162
+ });
163
+ if (!res.ok)
164
+ return null;
165
+ const data = await res.json();
166
+ return data.choices?.[0]?.message?.content ?? null;
167
+ }
168
+ return null;
169
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Full Audit — single source of truth for AI assistants.
3
+ * Orchestrates all security tools in one call, produces:
4
+ * - PASS/FAIL/WARN verdict
5
+ * - Unified report across code, secrets, deps, config, taint, auth
6
+ * - Deterministic result hash (same code = same hash)
7
+ * - Coverage metrics (files scanned, rules applied, %)
8
+ */
9
+ export type AuditVerdict = "PASS" | "WARN" | "FAIL";
10
+ export interface AuditCoverage {
11
+ filesScanned: number;
12
+ filesSkipped: number;
13
+ totalFiles: number;
14
+ coveragePercent: number;
15
+ rulesApplied: number;
16
+ }
17
+ export interface FindingRef {
18
+ ruleId: string;
19
+ severity: string;
20
+ file: string;
21
+ line: number;
22
+ [key: string]: unknown;
23
+ }
24
+ export interface AuditSection {
25
+ name: string;
26
+ status: "ok" | "error" | "skipped";
27
+ findings: number;
28
+ critical: number;
29
+ high: number;
30
+ medium: number;
31
+ details: string;
32
+ }
33
+ export interface AuditResult {
34
+ verdict: AuditVerdict;
35
+ score: number;
36
+ grade: string;
37
+ coverage: AuditCoverage;
38
+ resultHash: string;
39
+ timestamp: string;
40
+ sections: AuditSection[];
41
+ truncation: {
42
+ truncated: boolean;
43
+ maxFindings: number;
44
+ totalFindings: number;
45
+ taintFileCap: number;
46
+ taintFilesProcessed: number;
47
+ };
48
+ summary: {
49
+ totalFindings: number;
50
+ critical: number;
51
+ high: number;
52
+ medium: number;
53
+ };
54
+ actionItems: string[];
55
+ }
56
+ /**
57
+ * Compute verdict: PASS (0 critical + 0 high), WARN (high > 0), FAIL (critical > 0)
58
+ */
59
+ export declare function computeVerdict(critical: number, high: number, _medium: number): AuditVerdict;
60
+ /**
61
+ * Compute coverage metrics from scan results.
62
+ */
63
+ export declare function computeCoverage(filesScanned: number, filesSkipped: number, rulesApplied: number): AuditCoverage;
64
+ /**
65
+ * Compute deterministic SHA256 hash of findings.
66
+ * Same findings (in any order) = same hash.
67
+ */
68
+ export declare function computeResultHash(findings: FindingRef[]): string;
69
+ /**
70
+ * Run a full security audit — single source of truth.
71
+ * Orchestrates code scan, secret scan, dependency scan, config audit,
72
+ * taint analysis, and auth coverage in one call.
73
+ */
74
+ export declare function runFullAudit(path: string, options?: {
75
+ skipDeps?: boolean;
76
+ skipSecrets?: boolean;
77
+ }): Promise<AuditResult>;
78
+ /**
79
+ * Format audit result as markdown or JSON.
80
+ */
81
+ export declare function formatAuditResult(result: AuditResult, format: "markdown" | "json"): string;