ship-safe 5.0.1 → 6.1.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/README.md +110 -23
- package/cli/agents/abom-generator.js +225 -0
- package/cli/agents/agent-config-scanner.js +547 -0
- package/cli/agents/agentic-security-agent.js +1 -1
- package/cli/agents/api-fuzzer.js +1 -1
- package/cli/agents/auth-bypass-agent.js +2 -2
- package/cli/agents/config-auditor.js +3 -11
- package/cli/agents/exception-handler-agent.js +187 -0
- package/cli/agents/html-reporter.js +532 -370
- package/cli/agents/index.js +11 -1
- package/cli/agents/mcp-security-agent.js +182 -0
- package/cli/agents/pii-compliance-agent.js +4 -4
- package/cli/agents/scoring-engine.js +25 -6
- package/cli/agents/vibe-coding-agent.js +250 -0
- package/cli/bin/ship-safe.js +96 -6
- package/cli/commands/abom.js +73 -0
- package/cli/commands/agent.js +4 -4
- package/cli/commands/audit.js +15 -7
- package/cli/commands/baseline.js +1 -1
- package/cli/commands/benchmark.js +327 -0
- package/cli/commands/ci.js +81 -1
- package/cli/commands/deps.js +73 -4
- package/cli/commands/diff.js +200 -0
- package/cli/commands/doctor.js +14 -4
- package/cli/commands/fix.js +1 -1
- package/cli/commands/guard.js +99 -0
- package/cli/commands/init.js +407 -349
- package/cli/commands/openclaw.js +378 -0
- package/cli/commands/red-team.js +2 -2
- package/cli/commands/remediate.js +153 -7
- package/cli/commands/scan-skill.js +329 -0
- package/cli/commands/update-intel.js +55 -0
- package/cli/commands/vibe-check.js +276 -0
- package/cli/commands/watch.js +124 -4
- package/cli/data/threat-intel.json +85 -0
- package/cli/index.js +9 -0
- package/cli/utils/cache-manager.js +1 -1
- package/cli/utils/compliance-map.js +125 -0
- package/cli/utils/output.js +5 -2
- package/cli/utils/patterns.js +3 -0
- package/cli/utils/pdf-generator.js +1 -1
- package/cli/utils/threat-intel.js +167 -0
- package/package.json +2 -2
package/cli/agents/index.js
CHANGED
|
@@ -23,6 +23,10 @@ export { MCPSecurityAgent } from './mcp-security-agent.js';
|
|
|
23
23
|
export { AgenticSecurityAgent } from './agentic-security-agent.js';
|
|
24
24
|
export { RAGSecurityAgent } from './rag-security-agent.js';
|
|
25
25
|
export { PIIComplianceAgent } from './pii-compliance-agent.js';
|
|
26
|
+
export { VibeCodingAgent } from './vibe-coding-agent.js';
|
|
27
|
+
export { ExceptionHandlerAgent } from './exception-handler-agent.js';
|
|
28
|
+
export { AgentConfigScanner } from './agent-config-scanner.js';
|
|
29
|
+
export { ABOMGenerator } from './abom-generator.js';
|
|
26
30
|
export { VerifierAgent } from './verifier-agent.js';
|
|
27
31
|
export { DeepAnalyzer } from './deep-analyzer.js';
|
|
28
32
|
export { ScoringEngine, GRADES, CATEGORIES } from './scoring-engine.js';
|
|
@@ -31,7 +35,7 @@ export { PolicyEngine } from './policy-engine.js';
|
|
|
31
35
|
export { HTMLReporter } from './html-reporter.js';
|
|
32
36
|
|
|
33
37
|
/**
|
|
34
|
-
* Create a fully configured orchestrator with all
|
|
38
|
+
* Create a fully configured orchestrator with all 16 scanning agents.
|
|
35
39
|
* (VerifierAgent and DeepAnalyzer run as post-processors, not in the agent pool.)
|
|
36
40
|
*/
|
|
37
41
|
import { Orchestrator as OrchestratorClass } from './orchestrator.js';
|
|
@@ -50,6 +54,9 @@ import { MCPSecurityAgent as MCPSecurityAgentClass } from './mcp-security-agent.
|
|
|
50
54
|
import { AgenticSecurityAgent as AgenticSecurityAgentClass } from './agentic-security-agent.js';
|
|
51
55
|
import { RAGSecurityAgent as RAGSecurityAgentClass } from './rag-security-agent.js';
|
|
52
56
|
import { PIIComplianceAgent as PIIComplianceAgentClass } from './pii-compliance-agent.js';
|
|
57
|
+
import { VibeCodingAgent as VibeCodingAgentClass } from './vibe-coding-agent.js';
|
|
58
|
+
import { ExceptionHandlerAgent as ExceptionHandlerAgentClass } from './exception-handler-agent.js';
|
|
59
|
+
import { AgentConfigScanner as AgentConfigScannerClass } from './agent-config-scanner.js';
|
|
53
60
|
|
|
54
61
|
export function buildOrchestrator() {
|
|
55
62
|
const orchestrator = new OrchestratorClass();
|
|
@@ -69,6 +76,9 @@ export function buildOrchestrator() {
|
|
|
69
76
|
new AgenticSecurityAgentClass(),
|
|
70
77
|
new RAGSecurityAgentClass(),
|
|
71
78
|
new PIIComplianceAgentClass(),
|
|
79
|
+
new VibeCodingAgentClass(),
|
|
80
|
+
new ExceptionHandlerAgentClass(),
|
|
81
|
+
new AgentConfigScannerClass(),
|
|
72
82
|
]);
|
|
73
83
|
return orchestrator;
|
|
74
84
|
}
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* Maps to: OWASP Agentic AI ASI02 (Tool Misuse), ASI03 (Privilege Abuse)
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import fs from 'fs';
|
|
19
20
|
import path from 'path';
|
|
21
|
+
import os from 'os';
|
|
20
22
|
import { BaseAgent } from './base-agent.js';
|
|
21
23
|
|
|
22
24
|
// =============================================================================
|
|
@@ -230,8 +232,26 @@ const MCP_CONFIG_FILES = [
|
|
|
230
232
|
'mcp-config.json',
|
|
231
233
|
'claude_desktop_config.json',
|
|
232
234
|
'.cursor/mcp.json',
|
|
235
|
+
'.vscode/mcp.json',
|
|
233
236
|
];
|
|
234
237
|
|
|
238
|
+
// Well-known official MCP server packages
|
|
239
|
+
const OFFICIAL_MCP_SERVERS = new Set([
|
|
240
|
+
'@modelcontextprotocol/server-filesystem',
|
|
241
|
+
'@modelcontextprotocol/server-github',
|
|
242
|
+
'@modelcontextprotocol/server-gitlab',
|
|
243
|
+
'@modelcontextprotocol/server-google-maps',
|
|
244
|
+
'@modelcontextprotocol/server-memory',
|
|
245
|
+
'@modelcontextprotocol/server-postgres',
|
|
246
|
+
'@modelcontextprotocol/server-puppeteer',
|
|
247
|
+
'@modelcontextprotocol/server-slack',
|
|
248
|
+
'@modelcontextprotocol/server-sqlite',
|
|
249
|
+
'@modelcontextprotocol/server-brave-search',
|
|
250
|
+
'@modelcontextprotocol/server-fetch',
|
|
251
|
+
'@modelcontextprotocol/server-everything',
|
|
252
|
+
'@modelcontextprotocol/server-sequential-thinking',
|
|
253
|
+
]);
|
|
254
|
+
|
|
235
255
|
// =============================================================================
|
|
236
256
|
// MCP SECURITY AGENT
|
|
237
257
|
// =============================================================================
|
|
@@ -281,6 +301,15 @@ export class MCPSecurityAgent extends BaseAgent {
|
|
|
281
301
|
findings = findings.concat(this._checkServerAuth(file));
|
|
282
302
|
}
|
|
283
303
|
|
|
304
|
+
// ── 4. Check MCP configs for typosquatting & over-permissioned servers ─
|
|
305
|
+
for (const file of configFiles) {
|
|
306
|
+
findings = findings.concat(this._checkMcpTyposquatting(file));
|
|
307
|
+
findings = findings.concat(this._checkOverPermissioned(file));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── 5. Detect shadow MCP configs (not in version control) ───────────
|
|
311
|
+
findings = findings.concat(this._detectShadowMcpConfigs(rootPath));
|
|
312
|
+
|
|
284
313
|
return findings;
|
|
285
314
|
}
|
|
286
315
|
|
|
@@ -353,6 +382,159 @@ export class MCPSecurityAgent extends BaseAgent {
|
|
|
353
382
|
|
|
354
383
|
return findings;
|
|
355
384
|
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Detect possible typosquatted MCP server names in config.
|
|
388
|
+
*/
|
|
389
|
+
_checkMcpTyposquatting(filePath) {
|
|
390
|
+
const content = this.readFile(filePath);
|
|
391
|
+
if (!content) return [];
|
|
392
|
+
const findings = [];
|
|
393
|
+
|
|
394
|
+
try {
|
|
395
|
+
const config = JSON.parse(content);
|
|
396
|
+
const servers = config.mcpServers || config.servers || {};
|
|
397
|
+
|
|
398
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
399
|
+
const cmd = server.command || '';
|
|
400
|
+
const args = (server.args || []).join(' ');
|
|
401
|
+
const fullCmd = `${cmd} ${args}`;
|
|
402
|
+
|
|
403
|
+
// Check if server uses an npx package that looks like a typosquat of official ones
|
|
404
|
+
const npxMatch = fullCmd.match(/npx\s+(?:-[^\s]+\s+)*([^\s]+)/);
|
|
405
|
+
if (npxMatch) {
|
|
406
|
+
const pkg = npxMatch[1];
|
|
407
|
+
for (const official of OFFICIAL_MCP_SERVERS) {
|
|
408
|
+
const distance = this._levenshtein(pkg, official);
|
|
409
|
+
if (distance > 0 && distance <= 3 && pkg !== official) {
|
|
410
|
+
findings.push({
|
|
411
|
+
file: filePath, line: 1, column: 0,
|
|
412
|
+
severity: 'critical',
|
|
413
|
+
category: this.category,
|
|
414
|
+
rule: 'MCP_TYPOSQUAT_SERVER',
|
|
415
|
+
title: `MCP: Possible Typosquatted Server "${pkg}"`,
|
|
416
|
+
description: `MCP server package "${pkg}" is ${distance} char(s) from official "${official}". Could be a supply chain attack.`,
|
|
417
|
+
matched: pkg,
|
|
418
|
+
confidence: 'medium',
|
|
419
|
+
cwe: 'CWE-494',
|
|
420
|
+
owasp: 'A08:2021',
|
|
421
|
+
fix: `Verify this is the correct package. Did you mean "${official}"?`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} catch { /* not valid JSON */ }
|
|
428
|
+
|
|
429
|
+
return findings;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Check for over-permissioned MCP servers (filesystem access to / or ~).
|
|
434
|
+
*/
|
|
435
|
+
_checkOverPermissioned(filePath) {
|
|
436
|
+
const content = this.readFile(filePath);
|
|
437
|
+
if (!content) return [];
|
|
438
|
+
const findings = [];
|
|
439
|
+
|
|
440
|
+
try {
|
|
441
|
+
const config = JSON.parse(content);
|
|
442
|
+
const servers = config.mcpServers || config.servers || {};
|
|
443
|
+
|
|
444
|
+
for (const [name, server] of Object.entries(servers)) {
|
|
445
|
+
const args = server.args || [];
|
|
446
|
+
const argsStr = args.join(' ');
|
|
447
|
+
|
|
448
|
+
// Check for root/home filesystem access
|
|
449
|
+
if (/(?:^\/\s|['" ]\/['"]?\s|\/Users\/|\/home\/|\\Users\\|C:\\)/.test(argsStr)) {
|
|
450
|
+
const hasWideAccess = args.some(a =>
|
|
451
|
+
a === '/' || a === '~' || a === '%USERPROFILE%' ||
|
|
452
|
+
/^\/(?:Users|home)\/[^/]+$/.test(a) ||
|
|
453
|
+
/^[A-Z]:\\(?:Users\\)?[^\\]*$/.test(a)
|
|
454
|
+
);
|
|
455
|
+
if (hasWideAccess) {
|
|
456
|
+
findings.push({
|
|
457
|
+
file: filePath, line: 1, column: 0,
|
|
458
|
+
severity: 'high',
|
|
459
|
+
category: this.category,
|
|
460
|
+
rule: 'MCP_OVER_PERMISSIONED',
|
|
461
|
+
title: `MCP: Server "${name}" Has Broad Filesystem Access`,
|
|
462
|
+
description: `MCP server "${name}" has access to a wide directory scope. A prompt injection attack could read or modify sensitive files.`,
|
|
463
|
+
matched: argsStr.slice(0, 200),
|
|
464
|
+
confidence: 'high',
|
|
465
|
+
cwe: 'CWE-269',
|
|
466
|
+
owasp: 'A01:2021',
|
|
467
|
+
fix: 'Restrict filesystem access to the minimum required directory: e.g., the project folder only.',
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} catch { /* not valid JSON */ }
|
|
473
|
+
|
|
474
|
+
return findings;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Detect shadow MCP configs that exist but aren't in .gitignore or git.
|
|
479
|
+
*/
|
|
480
|
+
_detectShadowMcpConfigs(rootPath) {
|
|
481
|
+
const findings = [];
|
|
482
|
+
const home = os.homedir();
|
|
483
|
+
|
|
484
|
+
// Check common locations for MCP configs outside version control
|
|
485
|
+
const homeConfigs = [
|
|
486
|
+
path.join(home, '.cursor', 'mcp.json'),
|
|
487
|
+
path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
|
|
488
|
+
path.join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json'),
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
for (const configPath of homeConfigs) {
|
|
492
|
+
try {
|
|
493
|
+
if (fs.existsSync(configPath)) {
|
|
494
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
495
|
+
const config = JSON.parse(content);
|
|
496
|
+
const servers = config.mcpServers || config.servers || {};
|
|
497
|
+
const serverCount = Object.keys(servers).length;
|
|
498
|
+
|
|
499
|
+
if (serverCount > 0) {
|
|
500
|
+
findings.push({
|
|
501
|
+
file: configPath, line: 1, column: 0,
|
|
502
|
+
severity: 'medium',
|
|
503
|
+
category: this.category,
|
|
504
|
+
rule: 'MCP_SHADOW_CONFIG',
|
|
505
|
+
title: `MCP: ${serverCount} Shadow Server(s) in User Config`,
|
|
506
|
+
description: `Found ${serverCount} MCP server(s) configured outside the project in ${configPath}. These operate outside your project's security controls.`,
|
|
507
|
+
matched: Object.keys(servers).join(', '),
|
|
508
|
+
confidence: 'medium',
|
|
509
|
+
cwe: 'CWE-269',
|
|
510
|
+
owasp: 'A05:2021',
|
|
511
|
+
fix: 'Review shadow MCP servers. Move project-specific servers to the project mcp.json and track in version control.',
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
} catch { /* skip */ }
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return findings;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Simple Levenshtein distance for typosquatting detection.
|
|
523
|
+
*/
|
|
524
|
+
_levenshtein(a, b) {
|
|
525
|
+
const m = a.length, n = b.length;
|
|
526
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
527
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
528
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
529
|
+
for (let i = 1; i <= m; i++) {
|
|
530
|
+
for (let j = 1; j <= n; j++) {
|
|
531
|
+
dp[i][j] = a[i-1] === b[j-1]
|
|
532
|
+
? dp[i-1][j-1]
|
|
533
|
+
: 1 + Math.min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return dp[m][n];
|
|
537
|
+
}
|
|
356
538
|
}
|
|
357
539
|
|
|
358
540
|
export default MCPSecurityAgent;
|
|
@@ -39,7 +39,7 @@ const PATTERNS = [
|
|
|
39
39
|
{
|
|
40
40
|
rule: 'PII_IN_LOGGER',
|
|
41
41
|
title: 'Privacy: PII in Structured Logger',
|
|
42
|
-
regex: /(?:logger|
|
|
42
|
+
regex: /(?:logger|winston|pino|bunyan|morgan)\.(?:info|warn|error|debug|log)\s*\([\s\S]{0,200}(?:[\.\[\(]email\b|password|ssn|creditCard|credit_card|phoneNumber|phone_number|dateOfBirth|date_of_birth)/g,
|
|
43
43
|
severity: 'high',
|
|
44
44
|
cwe: 'CWE-532',
|
|
45
45
|
owasp: 'A09:2021',
|
|
@@ -62,7 +62,7 @@ const PATTERNS = [
|
|
|
62
62
|
{
|
|
63
63
|
rule: 'PII_IN_ERROR_RESPONSE',
|
|
64
64
|
title: 'Privacy: PII in Error Response to Client',
|
|
65
|
-
regex: /(?:res\.(?:json|send|status)|response\.(?:json|send)|jsonify)\s*\(\s*(?:\{[\s\S]{0,200}(?:email|password|ssn|creditCard|
|
|
65
|
+
regex: /(?:res\.(?:json|send|status)|response\.(?:json|send)|jsonify)\s*\(\s*(?:\{[\s\S]{0,200}(?:email|password|ssn|creditCard|credit_card|phoneNumber|phone_number|patient|dateOfBirth|date_of_birth)[\s\S]{0,100}\}|err(?:or)?\.(?:stack|message))/g,
|
|
66
66
|
severity: 'high',
|
|
67
67
|
cwe: 'CWE-209',
|
|
68
68
|
owasp: 'A01:2021',
|
|
@@ -151,7 +151,7 @@ const PATTERNS = [
|
|
|
151
151
|
{
|
|
152
152
|
rule: 'PII_EMAIL_HARDCODED',
|
|
153
153
|
title: 'Privacy: Real Email Address Hardcoded',
|
|
154
|
-
regex: /['"][a-zA-Z0-9._%+-]+@(?!example\.com|test\.com|placeholder|fake|dummy)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}['"]/g,
|
|
154
|
+
regex: /['"][a-zA-Z0-9._%+-]+@(?!example\.com|example\.org|test\.com|test\.org|testing\.com|localhost|placeholder|fake|dummy|mailinator\.com|noreply|no-reply|sample\.com)[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}['"]/g,
|
|
155
155
|
severity: 'medium',
|
|
156
156
|
cwe: 'CWE-312',
|
|
157
157
|
owasp: 'A02:2021',
|
|
@@ -199,7 +199,7 @@ const PATTERNS = [
|
|
|
199
199
|
{
|
|
200
200
|
rule: 'PII_GEOLOCATION_STORAGE',
|
|
201
201
|
title: 'Privacy: Precise Geolocation Stored',
|
|
202
|
-
regex: /(?:latitude|longitude|
|
|
202
|
+
regex: /(?:latitude|longitude|\blat\b|\blng\b|geolocation|geoip|geo_location)[\s\S]{0,100}(?:save|store|insert|create|update|write|database|db|mongo|prisma|sequelize)/gi,
|
|
203
203
|
severity: 'medium',
|
|
204
204
|
cwe: 'CWE-359',
|
|
205
205
|
owasp: 'A01:2021',
|
|
@@ -11,27 +11,34 @@
|
|
|
11
11
|
|
|
12
12
|
import fs from 'fs';
|
|
13
13
|
import path from 'path';
|
|
14
|
+
import { getComplianceSummary } from '../utils/compliance-map.js';
|
|
14
15
|
|
|
15
16
|
// =============================================================================
|
|
16
17
|
// SCORING CONFIGURATION
|
|
17
18
|
// =============================================================================
|
|
18
19
|
|
|
20
|
+
// Weights aligned with OWASP Top 10 2025:
|
|
21
|
+
// A01 Broken Access Control (auth: 15), A02 Security Misconfiguration (config: 8),
|
|
22
|
+
// A03 Software Supply Chain Failures (supply-chain: 12, deps: 13),
|
|
23
|
+
// A05 Injection (injection: 15), A07 Auth Failures (secrets: 15),
|
|
24
|
+
// A10 Mishandling of Exceptional Conditions (→ injection category)
|
|
25
|
+
// + API Security (10), AI/LLM Security (12) — weights sum to 100
|
|
19
26
|
const CATEGORIES = {
|
|
20
27
|
secrets: { weight: 15, label: 'Secrets', deductions: { critical: 25, high: 15, medium: 5 } },
|
|
21
28
|
injection: { weight: 15, label: 'Code Vulnerabilities', deductions: { critical: 20, high: 10, medium: 3 } },
|
|
22
|
-
deps: { weight:
|
|
29
|
+
deps: { weight: 13, label: 'Dependencies', deductions: { critical: 20, high: 10, medium: 5, moderate: 5 } },
|
|
23
30
|
auth: { weight: 15, label: 'Auth & Access Control', deductions: { critical: 20, high: 10, medium: 3 } },
|
|
24
|
-
config: { weight:
|
|
25
|
-
'supply-chain':{ weight:
|
|
31
|
+
config: { weight: 8, label: 'Configuration', deductions: { critical: 15, high: 8, medium: 3 } },
|
|
32
|
+
'supply-chain':{ weight: 12, label: 'Supply Chain', deductions: { critical: 15, high: 8, medium: 3 } },
|
|
26
33
|
api: { weight: 10, label: 'API Security', deductions: { critical: 15, high: 8, medium: 3 } },
|
|
27
|
-
llm: { weight:
|
|
34
|
+
llm: { weight: 12, label: 'AI/LLM Security', deductions: { critical: 15, high: 8, medium: 3 } },
|
|
28
35
|
};
|
|
29
36
|
|
|
30
37
|
// Fallback categories for findings that don't match a known category
|
|
31
38
|
const FALLBACK_CATEGORY_MAP = {
|
|
32
39
|
'secret': 'secrets',
|
|
33
40
|
'vulnerability': 'injection',
|
|
34
|
-
'ssrf': 'injection',
|
|
41
|
+
'ssrf': 'injection', // OWASP 2025: SSRF merged into A01 Broken Access Control
|
|
35
42
|
'history': 'secrets',
|
|
36
43
|
'cicd': 'config',
|
|
37
44
|
'mobile': 'injection',
|
|
@@ -39,7 +46,10 @@ const FALLBACK_CATEGORY_MAP = {
|
|
|
39
46
|
'mcp': 'llm',
|
|
40
47
|
'agentic': 'llm',
|
|
41
48
|
'rag': 'llm',
|
|
42
|
-
'
|
|
49
|
+
'vibe': 'injection', // Vibe coding findings → Code Vulnerabilities
|
|
50
|
+
'exception': 'injection', // OWASP A10:2025 — Mishandling of Exceptional Conditions
|
|
51
|
+
'agent-config': 'llm', // Agent config security → AI/LLM category
|
|
52
|
+
'recon': null, // skip recon findings
|
|
43
53
|
};
|
|
44
54
|
|
|
45
55
|
const GRADES = [
|
|
@@ -128,12 +138,21 @@ export class ScoringEngine {
|
|
|
128
138
|
const score = Math.max(0, 100 - totalDeduction);
|
|
129
139
|
const grade = GRADES.find(g => score >= g.min);
|
|
130
140
|
|
|
141
|
+
// ── Compliance mapping ─────────────────────────────────────────────────
|
|
142
|
+
let compliance;
|
|
143
|
+
try {
|
|
144
|
+
compliance = getComplianceSummary(findings);
|
|
145
|
+
} catch {
|
|
146
|
+
compliance = null;
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
return {
|
|
132
150
|
score,
|
|
133
151
|
grade,
|
|
134
152
|
categories: categoryResults,
|
|
135
153
|
totalFindings: findings.length,
|
|
136
154
|
totalDepVulns: depVulns.length,
|
|
155
|
+
compliance,
|
|
137
156
|
};
|
|
138
157
|
}
|
|
139
158
|
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vibe Coding Security Agent
|
|
3
|
+
* ============================
|
|
4
|
+
*
|
|
5
|
+
* Detects security vulnerabilities commonly introduced by
|
|
6
|
+
* AI-generated code (Cursor, Copilot, Claude Code, etc.).
|
|
7
|
+
*
|
|
8
|
+
* 45% of AI-generated code contains security vulnerabilities.
|
|
9
|
+
* This agent targets the most frequent patterns:
|
|
10
|
+
*
|
|
11
|
+
* - Placeholder credentials left in code
|
|
12
|
+
* - Missing input validation on AI-generated routes
|
|
13
|
+
* - Overly permissive defaults (CORS *, DEBUG=True)
|
|
14
|
+
* - Unprotected API endpoints (no auth middleware)
|
|
15
|
+
* - TODO/FIXME security markers left behind
|
|
16
|
+
* - Hardcoded localhost/development URLs in production code
|
|
17
|
+
* - Incomplete error handling (empty catch blocks)
|
|
18
|
+
* - AI-generated boilerplate with known insecure patterns
|
|
19
|
+
*
|
|
20
|
+
* Maps to: OWASP A01 (Broken Access Control), A05 (Security Misconfiguration)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import path from 'path';
|
|
24
|
+
import { BaseAgent, createFinding } from './base-agent.js';
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// VIBE CODING PATTERNS
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
const PATTERNS = [
|
|
31
|
+
// ── Placeholder Credentials ───────────────────────────────────────────────
|
|
32
|
+
{
|
|
33
|
+
rule: 'VIBE_PLACEHOLDER_KEY',
|
|
34
|
+
title: 'Vibe Code: Placeholder API Key Left in Code',
|
|
35
|
+
regex: /(?:api[_-]?key|apikey|secret|token|password)\s*[:=]\s*['"](?:your[_-]?(?:api[_-]?)?key[_-]?here|sk[_-](?:test|live)[_-]x{5,}|xxx+|insert[_-]?(?:your[_-]?)?(?:key|token|secret)[_-]?here|change[_-]?me|replace[_-]?(?:this|me)|put[_-]?your[_-]?(?:key|token))['"]/gi,
|
|
36
|
+
severity: 'critical',
|
|
37
|
+
cwe: 'CWE-798',
|
|
38
|
+
owasp: 'A07:2021',
|
|
39
|
+
description: 'AI-generated placeholder credential left in code. These are commonly generated by Copilot/Cursor and forgotten before deploy.',
|
|
40
|
+
fix: 'Replace with environment variable: process.env.API_KEY or os.environ["API_KEY"]',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
rule: 'VIBE_PLACEHOLDER_URL',
|
|
44
|
+
title: 'Vibe Code: Placeholder URL Left in Code',
|
|
45
|
+
regex: /['"]https?:\/\/(?:your[_-]?(?:domain|api|server|backend)|example\.com\/api|api\.example|placeholder)[^'"]*['"]/gi,
|
|
46
|
+
severity: 'medium',
|
|
47
|
+
cwe: 'CWE-1188',
|
|
48
|
+
owasp: 'A05:2021',
|
|
49
|
+
confidence: 'medium',
|
|
50
|
+
description: 'AI-generated placeholder URL left in code. May cause requests to fail or leak data to unintended endpoints.',
|
|
51
|
+
fix: 'Replace with environment variable for the API URL',
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
// ── Security TODO/FIXME Markers ───────────────────────────────────────────
|
|
55
|
+
{
|
|
56
|
+
rule: 'VIBE_TODO_AUTH',
|
|
57
|
+
title: 'Vibe Code: TODO — Add Authentication',
|
|
58
|
+
regex: /\/\/\s*(?:TODO|FIXME|HACK|XXX)\s*:?\s*(?:add|implement|fix|enable|require)\s+(?:auth(?:entication|orization)?|login|session|jwt|token|middleware|permission|rbac|access.?control)/gi,
|
|
59
|
+
severity: 'high',
|
|
60
|
+
cwe: 'CWE-306',
|
|
61
|
+
owasp: 'A01:2021',
|
|
62
|
+
description: 'Code has a TODO marker to add authentication/authorization. AI-generated code often ships without auth.',
|
|
63
|
+
fix: 'Implement authentication before deploying. This TODO indicates a known security gap.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
rule: 'VIBE_TODO_VALIDATION',
|
|
67
|
+
title: 'Vibe Code: TODO — Add Input Validation',
|
|
68
|
+
regex: /\/\/\s*(?:TODO|FIXME|HACK|XXX)\s*:?\s*(?:add|implement|fix|enable)\s+(?:valid(?:ation|ate)|sanitiz(?:e|ation)|escap(?:e|ing)|input.?check|type.?check)/gi,
|
|
69
|
+
severity: 'high',
|
|
70
|
+
cwe: 'CWE-20',
|
|
71
|
+
owasp: 'A03:2021',
|
|
72
|
+
description: 'Code has a TODO marker to add input validation. AI-generated endpoints frequently skip validation.',
|
|
73
|
+
fix: 'Implement input validation before deploying. Unvalidated input leads to injection attacks.',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
rule: 'VIBE_TODO_SECURITY',
|
|
77
|
+
title: 'Vibe Code: TODO — Security Fix Needed',
|
|
78
|
+
regex: /\/\/\s*(?:TODO|FIXME|HACK|XXX)\s*:?\s*(?:add|implement|fix|enable|remove)\s+(?:security|encrypt(?:ion)?|hash(?:ing)?|rate.?limit|csrf|xss|cors|https|ssl|tls|firewall)/gi,
|
|
79
|
+
severity: 'high',
|
|
80
|
+
cwe: 'CWE-693',
|
|
81
|
+
owasp: 'A05:2021',
|
|
82
|
+
description: 'Security-related TODO left in code. This marks a known vulnerability the developer intended to fix.',
|
|
83
|
+
fix: 'Address this security TODO before deploying to production.',
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// ── Missing Auth on Routes ────────────────────────────────────────────────
|
|
87
|
+
{
|
|
88
|
+
rule: 'VIBE_UNPROTECTED_DELETE',
|
|
89
|
+
title: 'Vibe Code: DELETE Endpoint Without Auth Check',
|
|
90
|
+
regex: /(?:app|router)\.(?:delete)\s*\(\s*['"][^'"]+['"]\s*,\s*(?:async\s+)?\(?(?:req|request|ctx)\b/g,
|
|
91
|
+
severity: 'high',
|
|
92
|
+
cwe: 'CWE-306',
|
|
93
|
+
owasp: 'A01:2021',
|
|
94
|
+
confidence: 'medium',
|
|
95
|
+
description: 'DELETE endpoint defined without visible auth middleware. AI often generates CRUD routes without access control.',
|
|
96
|
+
fix: 'Add authentication middleware: router.delete("/resource/:id", authMiddleware, handler)',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
rule: 'VIBE_UNPROTECTED_ADMIN',
|
|
100
|
+
title: 'Vibe Code: Admin Route Without Auth Middleware',
|
|
101
|
+
regex: /(?:app|router)\.(?:get|post|put|patch|delete)\s*\(\s*['"]\/admin[^'"]*['"]\s*,\s*(?:async\s+)?\(?(?:req|request|ctx)\b/g,
|
|
102
|
+
severity: 'critical',
|
|
103
|
+
cwe: 'CWE-306',
|
|
104
|
+
owasp: 'A01:2021',
|
|
105
|
+
confidence: 'medium',
|
|
106
|
+
description: 'Admin route defined without visible auth middleware. AI-generated admin panels often lack access control.',
|
|
107
|
+
fix: 'Add auth + admin role check: router.get("/admin", authMiddleware, requireAdmin, handler)',
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// ── Insecure Defaults ─────────────────────────────────────────────────────
|
|
111
|
+
{
|
|
112
|
+
rule: 'VIBE_WILDCARD_CORS',
|
|
113
|
+
title: 'Vibe Code: CORS Allow All Origins',
|
|
114
|
+
regex: /cors\s*\(\s*\{?\s*(?:origin\s*:\s*(?:true|['"]?\*['"]?)|(?:origin\s*:\s*\[?\s*['"]?\*['"]?))/g,
|
|
115
|
+
severity: 'high',
|
|
116
|
+
cwe: 'CWE-942',
|
|
117
|
+
owasp: 'A05:2021',
|
|
118
|
+
description: 'CORS configured to allow all origins. AI tools frequently generate cors({ origin: "*" }) or cors({ origin: true }) as defaults.',
|
|
119
|
+
fix: 'Restrict to specific origins: cors({ origin: ["https://yourdomain.com"] })',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
rule: 'VIBE_DEBUG_PRODUCTION',
|
|
123
|
+
title: 'Vibe Code: Debug Mode Enabled',
|
|
124
|
+
regex: /(?:DEBUG|debug)\s*[:=]\s*(?:true|True|1|['"]true['"])\s*(?:,|\n|$|;)/g,
|
|
125
|
+
severity: 'high',
|
|
126
|
+
cwe: 'CWE-489',
|
|
127
|
+
owasp: 'A05:2021',
|
|
128
|
+
confidence: 'medium',
|
|
129
|
+
description: 'Debug mode is enabled. AI-generated configs often leave DEBUG=True from development.',
|
|
130
|
+
fix: 'Set DEBUG=False for production. Use environment variable: DEBUG=os.environ.get("DEBUG", "False")',
|
|
131
|
+
},
|
|
132
|
+
// TLS disabled detection is handled by AuthBypassAgent (NODE_TLS_REJECT_UNAUTHORIZED)
|
|
133
|
+
// and ConfigAuditor — not duplicated here to avoid false positives on regex definitions.
|
|
134
|
+
|
|
135
|
+
// ── Hardcoded Development URLs ────────────────────────────────────────────
|
|
136
|
+
{
|
|
137
|
+
rule: 'VIBE_HARDCODED_LOCALHOST',
|
|
138
|
+
title: 'Vibe Code: Hardcoded localhost URL in Production Code',
|
|
139
|
+
regex: /(?:fetch|axios|http|request|api|endpoint|baseURL|base_url|API_URL|BACKEND_URL)\s*[:=(]\s*['"]https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0):\d+[^'"]*['"]/gi,
|
|
140
|
+
severity: 'medium',
|
|
141
|
+
cwe: 'CWE-1188',
|
|
142
|
+
owasp: 'A05:2021',
|
|
143
|
+
confidence: 'medium',
|
|
144
|
+
description: 'Hardcoded localhost URL found. AI-generated code often hardcodes development URLs that break in production.',
|
|
145
|
+
fix: 'Use environment variable: process.env.API_URL || "http://localhost:3000"',
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
// Empty catch blocks and except: pass are handled by ExceptionHandlerAgent (OWASP A10:2025)
|
|
149
|
+
|
|
150
|
+
// ── AI-Generated Insecure Patterns ────────────────────────────────────────
|
|
151
|
+
{
|
|
152
|
+
rule: 'VIBE_EVAL_INPUT',
|
|
153
|
+
title: 'Vibe Code: eval() with Variable Input',
|
|
154
|
+
regex: /\beval\s*\(\s*(?:req\.|request\.|body\.|params\.|query\.|input|data|payload|args|user)/g,
|
|
155
|
+
severity: 'critical',
|
|
156
|
+
cwe: 'CWE-95',
|
|
157
|
+
owasp: 'A03:2021',
|
|
158
|
+
description: 'eval() called with user-controlled input. AI sometimes generates eval() for "dynamic" functionality.',
|
|
159
|
+
fix: 'Never use eval() with user input. Use a safe parser or allowlist of operations.',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
rule: 'VIBE_INNER_HTML',
|
|
163
|
+
title: 'Vibe Code: innerHTML with Variable',
|
|
164
|
+
regex: /\.innerHTML\s*=\s*(?!['"]<)[^;]+(?:req|request|input|data|user|response|result|body|params|query|payload)/g,
|
|
165
|
+
severity: 'high',
|
|
166
|
+
cwe: 'CWE-79',
|
|
167
|
+
owasp: 'A03:2021',
|
|
168
|
+
description: 'innerHTML set with dynamic content. AI-generated frontend code often skips XSS sanitization.',
|
|
169
|
+
fix: 'Use textContent for plain text, or sanitize with DOMPurify: el.innerHTML = DOMPurify.sanitize(html)',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
rule: 'VIBE_NO_PARAMETERIZED_QUERY',
|
|
173
|
+
title: 'Vibe Code: String Concatenation in SQL',
|
|
174
|
+
regex: /(?:query|execute|raw|exec)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+\s*(?:req|request|body|params|query|input|user|data))/g,
|
|
175
|
+
severity: 'critical',
|
|
176
|
+
cwe: 'CWE-89',
|
|
177
|
+
owasp: 'A03:2021',
|
|
178
|
+
description: 'SQL query built with string concatenation/interpolation. AI-generated database code frequently lacks parameterization.',
|
|
179
|
+
fix: 'Use parameterized queries: db.query("SELECT * FROM users WHERE id = $1", [userId])',
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// ── Missing Rate Limiting ─────────────────────────────────────────────────
|
|
183
|
+
{
|
|
184
|
+
rule: 'VIBE_NO_RATE_LIMIT',
|
|
185
|
+
title: 'Vibe Code: Auth Endpoint Without Rate Limiting',
|
|
186
|
+
regex: /(?:app|router)\.post\s*\(\s*['"]\/(?:login|signin|sign-in|register|signup|sign-up|forgot-password|reset-password|auth\/login)['"]\s*,\s*(?:async\s+)?\(?(?:req|request|ctx)\b/g,
|
|
187
|
+
severity: 'high',
|
|
188
|
+
cwe: 'CWE-307',
|
|
189
|
+
owasp: 'A07:2021',
|
|
190
|
+
confidence: 'medium',
|
|
191
|
+
description: 'Authentication endpoint without visible rate limiting. AI-generated auth routes rarely include brute-force protection.',
|
|
192
|
+
fix: 'Add rate limiting: app.post("/login", rateLimit({ windowMs: 15*60*1000, max: 5 }), handler)',
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
// ── Insecure File Operations ──────────────────────────────────────────────
|
|
196
|
+
{
|
|
197
|
+
rule: 'VIBE_PATH_TRAVERSAL',
|
|
198
|
+
title: 'Vibe Code: User Input in File Path',
|
|
199
|
+
regex: /(?:readFile|writeFile|createReadStream|createWriteStream|open|access)\s*\(\s*(?:req\.|request\.|body\.|params\.|query\.|`[^`]*\$\{(?:req|request|params|query|body))/g,
|
|
200
|
+
severity: 'critical',
|
|
201
|
+
cwe: 'CWE-22',
|
|
202
|
+
owasp: 'A01:2021',
|
|
203
|
+
description: 'File operation uses user-controlled input without path validation. AI-generated file serving code often allows path traversal.',
|
|
204
|
+
fix: 'Validate and sanitize the path: path.resolve(baseDir, path.basename(userInput))',
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
// ── Exposed Secrets in Client Code ────────────────────────────────────────
|
|
208
|
+
{
|
|
209
|
+
rule: 'VIBE_SECRET_IN_FRONTEND',
|
|
210
|
+
title: 'Vibe Code: Secret Key in Frontend/Client Code',
|
|
211
|
+
regex: /(?:NEXT_PUBLIC_|REACT_APP_|VITE_|NUXT_PUBLIC_|EXPO_PUBLIC_)(?:SECRET|PRIVATE|API_SECRET|DATABASE_URL|DB_PASSWORD|JWT_SECRET|STRIPE_SECRET)/gi,
|
|
212
|
+
severity: 'critical',
|
|
213
|
+
cwe: 'CWE-200',
|
|
214
|
+
owasp: 'A01:2021',
|
|
215
|
+
description: 'Secret exposed via public environment variable prefix. AI tools often use NEXT_PUBLIC_/REACT_APP_ for all env vars.',
|
|
216
|
+
fix: 'Remove the public prefix. Server-side secrets must NOT use NEXT_PUBLIC_, REACT_APP_, or VITE_ prefixes.',
|
|
217
|
+
},
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
// =============================================================================
|
|
221
|
+
// VIBE CODING AGENT CLASS
|
|
222
|
+
// =============================================================================
|
|
223
|
+
|
|
224
|
+
export class VibeCodingAgent extends BaseAgent {
|
|
225
|
+
constructor() {
|
|
226
|
+
super(
|
|
227
|
+
'VibeCodingAgent',
|
|
228
|
+
'Detects security vulnerabilities commonly introduced by AI-generated code',
|
|
229
|
+
'injection', // maps to Code Vulnerabilities in scoring
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async analyze(context) {
|
|
234
|
+
const files = this.getFilesToScan(context);
|
|
235
|
+
const findings = [];
|
|
236
|
+
|
|
237
|
+
for (const file of files) {
|
|
238
|
+
const ext = path.extname(file).toLowerCase();
|
|
239
|
+
// Only scan source code files
|
|
240
|
+
if (!['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
241
|
+
'.py', '.rb', '.go', '.java', '.rs', '.php',
|
|
242
|
+
'.vue', '.svelte', '.astro'].includes(ext)) continue;
|
|
243
|
+
|
|
244
|
+
const results = this.scanFileWithPatterns(file, PATTERNS);
|
|
245
|
+
findings.push(...results);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return findings;
|
|
249
|
+
}
|
|
250
|
+
}
|