ship-safe 5.0.0 → 6.0.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/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/base-agent.js +2 -1
- package/cli/agents/config-auditor.js +3 -11
- package/cli/agents/exception-handler-agent.js +187 -0
- package/cli/agents/html-reporter.js +511 -370
- package/cli/agents/index.js +6 -0
- package/cli/agents/mcp-security-agent.js +182 -0
- package/cli/agents/pii-compliance-agent.js +301 -301
- package/cli/agents/scoring-engine.js +14 -6
- package/cli/agents/vibe-coding-agent.js +250 -0
- package/cli/bin/ship-safe.js +43 -6
- package/cli/commands/agent.js +6 -4
- package/cli/commands/audit.js +85 -14
- package/cli/commands/baseline.js +3 -2
- package/cli/commands/benchmark.js +327 -0
- package/cli/commands/ci.js +342 -260
- 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 +218 -216
- package/cli/commands/init.js +349 -349
- package/cli/commands/mcp.js +304 -303
- package/cli/commands/red-team.js +2 -2
- package/cli/commands/remediate.js +155 -7
- package/cli/commands/scan.js +567 -565
- package/cli/commands/score.js +2 -0
- package/cli/commands/vibe-check.js +276 -0
- package/cli/commands/watch.js +161 -160
- package/cli/index.js +8 -1
- package/cli/utils/cache-manager.js +1 -1
- package/cli/utils/output.js +5 -2
- package/cli/utils/patterns.js +1121 -1104
- package/cli/utils/pdf-generator.js +1 -1
- package/package.json +2 -2
|
@@ -16,22 +16,28 @@ import path from 'path';
|
|
|
16
16
|
// SCORING CONFIGURATION
|
|
17
17
|
// =============================================================================
|
|
18
18
|
|
|
19
|
+
// Weights aligned with OWASP Top 10 2025:
|
|
20
|
+
// A01 Broken Access Control (auth: 15), A02 Security Misconfiguration (config: 8),
|
|
21
|
+
// A03 Software Supply Chain Failures (supply-chain: 12, deps: 13),
|
|
22
|
+
// A05 Injection (injection: 15), A07 Auth Failures (secrets: 15),
|
|
23
|
+
// A10 Mishandling of Exceptional Conditions (→ injection category)
|
|
24
|
+
// + API Security (10), AI/LLM Security (12) — weights sum to 100
|
|
19
25
|
const CATEGORIES = {
|
|
20
26
|
secrets: { weight: 15, label: 'Secrets', deductions: { critical: 25, high: 15, medium: 5 } },
|
|
21
27
|
injection: { weight: 15, label: 'Code Vulnerabilities', deductions: { critical: 20, high: 10, medium: 3 } },
|
|
22
|
-
deps: { weight:
|
|
28
|
+
deps: { weight: 13, label: 'Dependencies', deductions: { critical: 20, high: 10, medium: 5, moderate: 5 } },
|
|
23
29
|
auth: { weight: 15, label: 'Auth & Access Control', deductions: { critical: 20, high: 10, medium: 3 } },
|
|
24
|
-
config: { weight:
|
|
25
|
-
'supply-chain':{ weight:
|
|
30
|
+
config: { weight: 8, label: 'Configuration', deductions: { critical: 15, high: 8, medium: 3 } },
|
|
31
|
+
'supply-chain':{ weight: 12, label: 'Supply Chain', deductions: { critical: 15, high: 8, medium: 3 } },
|
|
26
32
|
api: { weight: 10, label: 'API Security', deductions: { critical: 15, high: 8, medium: 3 } },
|
|
27
|
-
llm: { weight:
|
|
33
|
+
llm: { weight: 12, label: 'AI/LLM Security', deductions: { critical: 15, high: 8, medium: 3 } },
|
|
28
34
|
};
|
|
29
35
|
|
|
30
36
|
// Fallback categories for findings that don't match a known category
|
|
31
37
|
const FALLBACK_CATEGORY_MAP = {
|
|
32
38
|
'secret': 'secrets',
|
|
33
39
|
'vulnerability': 'injection',
|
|
34
|
-
'ssrf': 'injection',
|
|
40
|
+
'ssrf': 'injection', // OWASP 2025: SSRF merged into A01 Broken Access Control
|
|
35
41
|
'history': 'secrets',
|
|
36
42
|
'cicd': 'config',
|
|
37
43
|
'mobile': 'injection',
|
|
@@ -39,7 +45,9 @@ const FALLBACK_CATEGORY_MAP = {
|
|
|
39
45
|
'mcp': 'llm',
|
|
40
46
|
'agentic': 'llm',
|
|
41
47
|
'rag': 'llm',
|
|
42
|
-
'
|
|
48
|
+
'vibe': 'injection', // Vibe coding findings → Code Vulnerabilities
|
|
49
|
+
'exception': 'injection', // OWASP A10:2025 — Mishandling of Exceptional Conditions
|
|
50
|
+
'recon': null, // skip recon findings
|
|
43
51
|
};
|
|
44
52
|
|
|
45
53
|
const GRADES = [
|
|
@@ -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
|
+
}
|
package/cli/bin/ship-safe.js
CHANGED
|
@@ -37,6 +37,9 @@ import { auditCommand } from '../commands/audit.js';
|
|
|
37
37
|
import { doctorCommand } from '../commands/doctor.js';
|
|
38
38
|
import { baselineCommand } from '../commands/baseline.js';
|
|
39
39
|
import { ciCommand } from '../commands/ci.js';
|
|
40
|
+
import { diffCommand } from '../commands/diff.js';
|
|
41
|
+
import { vibeCheckCommand } from '../commands/vibe-check.js';
|
|
42
|
+
import { benchmarkCommand } from '../commands/benchmark.js';
|
|
40
43
|
import { PolicyEngine } from '../agents/policy-engine.js';
|
|
41
44
|
import { SBOMGenerator } from '../agents/sbom-generator.js';
|
|
42
45
|
|
|
@@ -47,7 +50,7 @@ import { SBOMGenerator } from '../agents/sbom-generator.js';
|
|
|
47
50
|
const DEFAULT_MODEL = 'claude-haiku-4-5-20251001';
|
|
48
51
|
|
|
49
52
|
// Read version from package.json
|
|
50
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
53
|
+
const __filename = fileURLToPath(import.meta.url); // ship-safe-ignore — module's own path via import.meta.url, not user input
|
|
51
54
|
const __dirname = dirname(__filename);
|
|
52
55
|
const packageJson = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'));
|
|
53
56
|
const VERSION = packageJson.version;
|
|
@@ -189,7 +192,7 @@ program
|
|
|
189
192
|
// -----------------------------------------------------------------------------
|
|
190
193
|
program
|
|
191
194
|
.command('audit [path]')
|
|
192
|
-
.description('Full security audit: secrets +
|
|
195
|
+
.description('Full security audit: secrets + 17 agents + deps + score + deep analysis + remediation plan')
|
|
193
196
|
.option('--json', 'Output results as JSON')
|
|
194
197
|
.option('--sarif', 'Output results in SARIF format')
|
|
195
198
|
.option('--csv', 'Output results as CSV')
|
|
@@ -210,12 +213,24 @@ program
|
|
|
210
213
|
.option('-v, --verbose', 'Verbose output')
|
|
211
214
|
.action(auditCommand);
|
|
212
215
|
|
|
216
|
+
// -----------------------------------------------------------------------------
|
|
217
|
+
// DIFF COMMAND (v6.0 — Scan only changed files)
|
|
218
|
+
// -----------------------------------------------------------------------------
|
|
219
|
+
program
|
|
220
|
+
.command('diff [ref]')
|
|
221
|
+
.description('Scan only changed files (git diff) — fast pre-commit & PR scanning')
|
|
222
|
+
.option('--staged', 'Scan only staged changes')
|
|
223
|
+
.option('--json', 'Output results as JSON')
|
|
224
|
+
.option('-p, --path <path>', 'Project path (default: cwd)')
|
|
225
|
+
.option('--timeout <ms>', 'Per-agent timeout in milliseconds (default: 30000)', parseInt)
|
|
226
|
+
.action(diffCommand);
|
|
227
|
+
|
|
213
228
|
// -----------------------------------------------------------------------------
|
|
214
229
|
// RED TEAM COMMAND (v4.0 — Multi-Agent Security Audit)
|
|
215
230
|
// -----------------------------------------------------------------------------
|
|
216
231
|
program
|
|
217
232
|
.command('red-team [path]')
|
|
218
|
-
.description('Multi-agent security audit:
|
|
233
|
+
.description('Multi-agent security audit: 17 agents scan for 80+ attack classes')
|
|
219
234
|
.option('--agents <list>', 'Comma-separated list of agents to run')
|
|
220
235
|
.option('--json', 'Output results as JSON')
|
|
221
236
|
.option('--sarif', 'Output results in SARIF format')
|
|
@@ -291,8 +306,27 @@ program
|
|
|
291
306
|
.option('--json', 'JSON output')
|
|
292
307
|
.option('--no-deps', 'Skip dependency audit')
|
|
293
308
|
.option('--baseline', 'Only check new findings (not in baseline)')
|
|
309
|
+
.option('--github-pr', 'Post findings as a GitHub PR comment (requires gh CLI)')
|
|
294
310
|
.action(ciCommand);
|
|
295
311
|
|
|
312
|
+
// -----------------------------------------------------------------------------
|
|
313
|
+
// VIBE CHECK COMMAND
|
|
314
|
+
// -----------------------------------------------------------------------------
|
|
315
|
+
program
|
|
316
|
+
.command('vibe-check [path]')
|
|
317
|
+
.description('Fun security check with emoji output, shareable score, and badge generator')
|
|
318
|
+
.option('--badge', 'Generate a shields.io markdown badge for your README')
|
|
319
|
+
.action(vibeCheckCommand);
|
|
320
|
+
|
|
321
|
+
// -----------------------------------------------------------------------------
|
|
322
|
+
// BENCHMARK COMMAND
|
|
323
|
+
// -----------------------------------------------------------------------------
|
|
324
|
+
program
|
|
325
|
+
.command('benchmark [path]')
|
|
326
|
+
.description('Compare your security score against industry averages')
|
|
327
|
+
.option('--json', 'Output results as JSON')
|
|
328
|
+
.action(benchmarkCommand);
|
|
329
|
+
|
|
296
330
|
// -----------------------------------------------------------------------------
|
|
297
331
|
// DOCTOR COMMAND
|
|
298
332
|
// -----------------------------------------------------------------------------
|
|
@@ -309,11 +343,14 @@ program
|
|
|
309
343
|
if (process.argv.length === 2) {
|
|
310
344
|
console.log(banner);
|
|
311
345
|
console.log(chalk.yellow('\nQuick start:\n'));
|
|
312
|
-
console.log(chalk.cyan.bold('
|
|
313
|
-
console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets +
|
|
346
|
+
console.log(chalk.cyan.bold(' v6.0 — Full Security Audit'));
|
|
347
|
+
console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + 17 agents + deps + remediation'));
|
|
314
348
|
console.log(chalk.white(' npx ship-safe audit . --deep') + chalk.gray('# LLM-powered taint analysis (Anthropic/Ollama)'));
|
|
315
|
-
console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('#
|
|
349
|
+
console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 17-agent red team scan (80+ attack classes)'));
|
|
350
|
+
console.log(chalk.white(' npx ship-safe vibe-check . ') + chalk.gray('# Fun security check with emoji & shareable badge'));
|
|
351
|
+
console.log(chalk.white(' npx ship-safe benchmark . ') + chalk.gray('# Compare score against industry averages'));
|
|
316
352
|
console.log(chalk.white(' npx ship-safe ci . ') + chalk.gray('# CI/CD mode: scan, score, exit code'));
|
|
353
|
+
console.log(chalk.white(' npx ship-safe diff ') + chalk.gray('# Scan only changed files (fast pre-commit)'));
|
|
317
354
|
console.log(chalk.white(' npx ship-safe watch . ') + chalk.gray('# Continuous monitoring mode'));
|
|
318
355
|
console.log(chalk.white(' npx ship-safe sbom . ') + chalk.gray('# Generate CycloneDX SBOM (CRA-ready)'));
|
|
319
356
|
console.log(chalk.white(' npx ship-safe policy init ') + chalk.gray('# Create security policy template'));
|
package/cli/commands/agent.js
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
SECURITY_PATTERNS,
|
|
34
34
|
SKIP_DIRS,
|
|
35
35
|
SKIP_EXTENSIONS,
|
|
36
|
+
SKIP_FILENAMES,
|
|
36
37
|
TEST_FILE_PATTERNS,
|
|
37
38
|
MAX_FILE_SIZE
|
|
38
39
|
} from '../utils/patterns.js';
|
|
@@ -99,7 +100,7 @@ export async function agentCommand(targetPath = '.', options = {}) {
|
|
|
99
100
|
|
|
100
101
|
// ── 4. Fallback: no API key ────────────────────────────────────────────────
|
|
101
102
|
if (!apiKey) {
|
|
102
|
-
console.log(chalk.yellow(' ⚠ No ANTHROPIC_API_KEY found.'));
|
|
103
|
+
console.log(chalk.yellow(' ⚠ No ANTHROPIC_API_KEY found.')); // ship-safe-ignore — env var name in user-facing message, no key value
|
|
103
104
|
console.log(chalk.gray(' Set it in your environment or .env to enable AI classification.'));
|
|
104
105
|
if (secretCount > 0) {
|
|
105
106
|
console.log(chalk.gray(' Falling back to pattern-based remediation for secrets...\n'));
|
|
@@ -226,8 +227,8 @@ export async function agentCommand(targetPath = '.', options = {}) {
|
|
|
226
227
|
* Returns the key string or null if not found.
|
|
227
228
|
*/
|
|
228
229
|
function loadApiKey(rootPath) {
|
|
229
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
230
|
-
return process.env.ANTHROPIC_API_KEY;
|
|
230
|
+
if (process.env.ANTHROPIC_API_KEY) { // ship-safe-ignore — reading env var at runtime, no hardcoded key value
|
|
231
|
+
return process.env.ANTHROPIC_API_KEY; // ship-safe-ignore — returning env var value, not a hardcoded secret
|
|
231
232
|
}
|
|
232
233
|
|
|
233
234
|
const envPath = path.join(rootPath, '.env');
|
|
@@ -239,7 +240,7 @@ function loadApiKey(rootPath) {
|
|
|
239
240
|
if (trimmed.startsWith('#') || !trimmed.includes('=')) continue;
|
|
240
241
|
const eqIdx = trimmed.indexOf('=');
|
|
241
242
|
const key = trimmed.slice(0, eqIdx).trim();
|
|
242
|
-
if (key === 'ANTHROPIC_API_KEY') {
|
|
243
|
+
if (key === 'ANTHROPIC_API_KEY') { // ship-safe-ignore — parsing .env file to read user's own API key from their project
|
|
243
244
|
const val = trimmed.slice(eqIdx + 1).trim().replace(/^["']|["']$/g, '');
|
|
244
245
|
if (val) return val;
|
|
245
246
|
}
|
|
@@ -270,6 +271,7 @@ async function scanProject(rootPath) {
|
|
|
270
271
|
const files = allFiles.filter(file => {
|
|
271
272
|
const ext = path.extname(file).toLowerCase();
|
|
272
273
|
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
274
|
+
if (SKIP_FILENAMES.has(path.basename(file))) return false;
|
|
273
275
|
const basename = path.basename(file);
|
|
274
276
|
if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
|
|
275
277
|
if (TEST_FILE_PATTERNS.some(p => p.test(file))) return false;
|
package/cli/commands/audit.js
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
SECURITY_PATTERNS,
|
|
30
30
|
SKIP_DIRS,
|
|
31
31
|
SKIP_EXTENSIONS,
|
|
32
|
+
SKIP_FILENAMES,
|
|
32
33
|
MAX_FILE_SIZE,
|
|
33
34
|
loadGitignorePatterns
|
|
34
35
|
} from '../utils/patterns.js';
|
|
@@ -132,11 +133,39 @@ export async function auditCommand(targetPath = '.', options = {}) {
|
|
|
132
133
|
description: f.description,
|
|
133
134
|
matched: f.matched,
|
|
134
135
|
confidence: f.confidence,
|
|
135
|
-
fix:
|
|
136
|
+
fix: file.match(/\.env(\..*)?$/)
|
|
137
|
+
? `Ensure .env is in .gitignore and use a secrets manager for production`
|
|
138
|
+
: `Move to environment variable or secrets manager`,
|
|
136
139
|
});
|
|
137
140
|
}
|
|
138
141
|
}
|
|
139
142
|
|
|
143
|
+
// Downgrade .env findings if the file is gitignored (properly managed)
|
|
144
|
+
const gitignoreContent = (() => {
|
|
145
|
+
try { return fs.readFileSync(path.join(absolutePath, '.gitignore'), 'utf-8'); } catch { return ''; }
|
|
146
|
+
})();
|
|
147
|
+
const envIsGitignored = gitignoreContent.split('\n')
|
|
148
|
+
.map(l => l.trim())
|
|
149
|
+
.some(l => /^\.env(\s|$)/.test(l) || l === '*.env' || l === '.env*' || l === '.env.local' || l === '.env.production');
|
|
150
|
+
|
|
151
|
+
if (envIsGitignored) {
|
|
152
|
+
for (const f of secretFindings) {
|
|
153
|
+
if (f.file.match(/\.env(\..*)?$/) && !f.file.includes('node_modules')) {
|
|
154
|
+
f.severity = 'low';
|
|
155
|
+
f.confidence = 'low';
|
|
156
|
+
f.fix = 'Already gitignored — ensure secrets manager is used for production deploys';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Downgrade secrets in test files (intentional test fixtures)
|
|
162
|
+
const TEST_PATH = /(?:__tests__|\.test\.|\.spec\.|\/test\/|\/tests\/|\/fixtures?\/)/i;
|
|
163
|
+
for (const f of secretFindings) {
|
|
164
|
+
if (TEST_PATH.test(f.file)) {
|
|
165
|
+
f.confidence = 'low';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
140
169
|
// Merge with cached findings for unchanged files
|
|
141
170
|
secretFindings = [...secretFindings, ...cachedSecretFindings];
|
|
142
171
|
|
|
@@ -154,13 +183,14 @@ export async function auditCommand(targetPath = '.', options = {}) {
|
|
|
154
183
|
}
|
|
155
184
|
|
|
156
185
|
// ── Phase 2: Agent Scan ───────────────────────────────────────────────────
|
|
157
|
-
const
|
|
186
|
+
const orchestrator = buildOrchestrator();
|
|
187
|
+
const registeredAgentCount = orchestrator.agents?.length || 15;
|
|
188
|
+
const agentSpinner = machineOutput ? null : ora({ text: chalk.white(`[Phase 2/4] Running ${registeredAgentCount} security agents...`), color: 'cyan' }).start();
|
|
158
189
|
let agentFindings = [];
|
|
159
190
|
let recon = null;
|
|
160
191
|
let agentResults = [];
|
|
161
192
|
|
|
162
193
|
try {
|
|
163
|
-
const orchestrator = buildOrchestrator();
|
|
164
194
|
// Suppress individual agent spinners by using quiet mode
|
|
165
195
|
// Pass changedFiles for incremental scanning if cache is valid
|
|
166
196
|
const orchestratorOpts = { quiet: true };
|
|
@@ -172,7 +202,7 @@ export async function auditCommand(targetPath = '.', options = {}) {
|
|
|
172
202
|
if (cacheDiff && cacheDiff.changedFiles.length < allFiles.length) {
|
|
173
203
|
orchestratorOpts.changedFiles = cacheDiff.changedFiles;
|
|
174
204
|
}
|
|
175
|
-
const results = await orchestrator.runAll(absolutePath, orchestratorOpts);
|
|
205
|
+
const results = await orchestrator.runAll(absolutePath, orchestratorOpts); // ship-safe-ignore — orchestrator result, not LLM output triggering actions
|
|
176
206
|
recon = results.recon;
|
|
177
207
|
agentFindings = results.findings;
|
|
178
208
|
agentResults = results.agentResults;
|
|
@@ -232,6 +262,8 @@ export async function auditCommand(targetPath = '.', options = {}) {
|
|
|
232
262
|
// Score
|
|
233
263
|
const scoringEngine = new ScoringEngine();
|
|
234
264
|
const scoreResult = scoringEngine.compute(filteredFindings, depVulns);
|
|
265
|
+
// Round score to 1 decimal place to avoid floating-point noise (e.g., 63.300000000000004)
|
|
266
|
+
scoreResult.score = Math.round(scoreResult.score * 10) / 10;
|
|
235
267
|
scoringEngine.saveToHistory(absolutePath, scoreResult, suppressions);
|
|
236
268
|
|
|
237
269
|
const gradeColor = scoreResult.score >= 75 ? chalk.green.bold : scoreResult.score >= 60 ? chalk.yellow.bold : chalk.red.bold;
|
|
@@ -391,7 +423,9 @@ export async function auditCommand(targetPath = '.', options = {}) {
|
|
|
391
423
|
const trend = scoringEngine.getTrend(absolutePath, scoreResult.score);
|
|
392
424
|
if (trend) {
|
|
393
425
|
const arrow = trend.diff > 0 ? chalk.green('↑') : trend.diff < 0 ? chalk.red('↓') : chalk.gray('→');
|
|
394
|
-
|
|
426
|
+
const roundedDiff = Math.round(trend.diff * 10) / 10;
|
|
427
|
+
const diffLabel = roundedDiff === 0 ? chalk.gray('no change') : chalk.white(`${roundedDiff > 0 ? '+' : ''}${roundedDiff}`);
|
|
428
|
+
console.log(chalk.gray(` Trend: ${trend.previousScore} → ${trend.currentScore} ${arrow} (`) + diffLabel + chalk.gray(')'));
|
|
395
429
|
}
|
|
396
430
|
|
|
397
431
|
// ── Detailed Comparison ────────────────────────────────────────────────
|
|
@@ -415,14 +449,47 @@ function buildRemediationPlan(findings, depVulns, rootPath) {
|
|
|
415
449
|
const plan = [];
|
|
416
450
|
let priority = 1;
|
|
417
451
|
|
|
452
|
+
// Exclude low-confidence findings (test files, docs, comments) from remediation plan
|
|
453
|
+
const actionable = findings.filter(f => f.confidence !== 'low');
|
|
454
|
+
|
|
418
455
|
// Priority order: secrets first, then by severity
|
|
419
|
-
const secretFindings =
|
|
420
|
-
const otherFindings =
|
|
456
|
+
const secretFindings = actionable.filter(f => f.category === 'secrets' || f.category === 'secret');
|
|
457
|
+
const otherFindings = actionable.filter(f => f.category !== 'secrets' && f.category !== 'secret');
|
|
421
458
|
|
|
422
459
|
// Group and sort
|
|
423
460
|
for (const sev of SEV_ORDER) {
|
|
424
|
-
// Secrets at this severity
|
|
425
|
-
|
|
461
|
+
// Secrets at this severity — group .env findings by file
|
|
462
|
+
const sevSecrets = secretFindings.filter(s => s.severity === sev);
|
|
463
|
+
const envGroups = new Map();
|
|
464
|
+
const nonEnvSecrets = [];
|
|
465
|
+
|
|
466
|
+
for (const f of sevSecrets) {
|
|
467
|
+
const relFile = path.relative(rootPath, f.file).replace(/\\/g, '/');
|
|
468
|
+
if (f.file.match(/\.env(\..*)?$/)) {
|
|
469
|
+
if (!envGroups.has(relFile)) envGroups.set(relFile, []);
|
|
470
|
+
envGroups.get(relFile).push(f);
|
|
471
|
+
} else {
|
|
472
|
+
nonEnvSecrets.push(f);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// One plan item per .env file
|
|
477
|
+
for (const [relFile, envFindings] of envGroups) {
|
|
478
|
+
const names = envFindings.map(f => f.title || f.rule).join(', ');
|
|
479
|
+
plan.push({
|
|
480
|
+
priority: priority++,
|
|
481
|
+
severity: sev,
|
|
482
|
+
category: 'secrets',
|
|
483
|
+
categoryLabel: 'SECRETS',
|
|
484
|
+
title: `${envFindings.length} secret${envFindings.length > 1 ? 's' : ''} in ${relFile} (${names})`,
|
|
485
|
+
file: relFile,
|
|
486
|
+
action: envFindings[0].fix || 'Ensure .env is in .gitignore and use a secrets manager for production',
|
|
487
|
+
effort: 'low',
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Individual items for non-.env secrets
|
|
492
|
+
for (const f of nonEnvSecrets) {
|
|
426
493
|
plan.push({
|
|
427
494
|
priority: priority++,
|
|
428
495
|
severity: sev,
|
|
@@ -498,14 +565,17 @@ function printReport(scoreResult, findings, depVulns, recon, plan, rootPath, fil
|
|
|
498
565
|
const count = Object.values(cat.counts).reduce((a, b) => a + b, 0);
|
|
499
566
|
const icon = count === 0 ? chalk.green('✔') : chalk.red('✘');
|
|
500
567
|
const status = count === 0 ? chalk.green('clean') : chalk.red(`${count} issue(s)`);
|
|
501
|
-
const deduction = cat.deduction > 0 ? chalk.red(`-${cat.deduction} pts`) : chalk.gray('+0');
|
|
568
|
+
const deduction = cat.deduction > 0 ? chalk.red(`-${Math.round(cat.deduction * 10) / 10} pts`) : chalk.gray('+0');
|
|
502
569
|
console.log(` ${icon} ${chalk.white(cat.label.padEnd(22))} ${status.padEnd(25)} ${deduction}`);
|
|
503
570
|
}
|
|
504
571
|
|
|
505
|
-
// Deps row
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
572
|
+
// Deps row — only print if not already included in scoreResult.categories
|
|
573
|
+
const hasDepsCategory = Object.values(scoreResult.categories).some(c => c.label?.toLowerCase().includes('depend'));
|
|
574
|
+
if (!hasDepsCategory) {
|
|
575
|
+
const depIcon = depVulns.length === 0 ? chalk.green('✔') : chalk.red('✘');
|
|
576
|
+
const depStatus = depVulns.length === 0 ? chalk.green('clean') : chalk.red(`${depVulns.length} CVE(s)`);
|
|
577
|
+
console.log(` ${depIcon} ${chalk.white('Dependencies'.padEnd(22))} ${depStatus}`);
|
|
578
|
+
}
|
|
509
579
|
|
|
510
580
|
console.log(chalk.gray(`\n Files scanned: ${filesScanned} | Findings: ${findings.length} | CVEs: ${depVulns.length}`));
|
|
511
581
|
|
|
@@ -694,6 +764,7 @@ async function findFiles(rootPath) {
|
|
|
694
764
|
return files.filter(file => {
|
|
695
765
|
const ext = path.extname(file).toLowerCase();
|
|
696
766
|
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
767
|
+
if (SKIP_FILENAMES.has(path.basename(file))) return false;
|
|
697
768
|
if (path.basename(file).endsWith('.min.js') || path.basename(file).endsWith('.min.css')) return false;
|
|
698
769
|
try { if (fs.statSync(file).size > MAX_FILE_SIZE) return false; } catch { return false; }
|
|
699
770
|
return true;
|
package/cli/commands/baseline.js
CHANGED
|
@@ -17,7 +17,7 @@ import path from 'path';
|
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
import ora from 'ora';
|
|
19
19
|
import { buildOrchestrator } from '../agents/index.js';
|
|
20
|
-
import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, MAX_FILE_SIZE } from '../utils/patterns.js';
|
|
20
|
+
import { SECRET_PATTERNS, SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, MAX_FILE_SIZE } from '../utils/patterns.js';
|
|
21
21
|
import { isHighEntropyMatch } from '../utils/entropy.js';
|
|
22
22
|
import fg from 'fast-glob';
|
|
23
23
|
|
|
@@ -45,6 +45,7 @@ async function quickScan(rootPath) {
|
|
|
45
45
|
const filtered = files.filter(f => {
|
|
46
46
|
const ext = path.extname(f).toLowerCase();
|
|
47
47
|
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
48
|
+
if (SKIP_FILENAMES.has(path.basename(f))) return false;
|
|
48
49
|
try { return fs.statSync(f).size <= MAX_FILE_SIZE; } catch { return false; }
|
|
49
50
|
});
|
|
50
51
|
|
|
@@ -77,7 +78,7 @@ async function fullScan(rootPath) {
|
|
|
77
78
|
const { findings: secretFindings, files } = await quickScan(rootPath);
|
|
78
79
|
|
|
79
80
|
const orchestrator = buildOrchestrator();
|
|
80
|
-
const { findings: agentFindings } = await orchestrator.runAll(rootPath, { quiet: true });
|
|
81
|
+
const { findings: agentFindings } = await orchestrator.runAll(rootPath, { quiet: true }); // ship-safe-ignore — orchestrator result, not LLM output triggering actions
|
|
81
82
|
|
|
82
83
|
return [...secretFindings, ...agentFindings];
|
|
83
84
|
}
|