ship-safe 5.0.0 → 5.0.1
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/base-agent.js +2 -1
- package/cli/agents/pii-compliance-agent.js +301 -301
- package/cli/commands/agent.js +2 -0
- package/cli/commands/audit.js +71 -7
- package/cli/commands/baseline.js +2 -1
- package/cli/commands/ci.js +262 -260
- package/cli/commands/fix.js +218 -216
- package/cli/commands/mcp.js +304 -303
- package/cli/commands/remediate.js +2 -0
- package/cli/commands/scan.js +567 -565
- package/cli/commands/score.js +2 -0
- package/cli/commands/watch.js +161 -160
- package/cli/index.js +1 -1
- package/cli/utils/patterns.js +1118 -1104
- package/package.json +1 -1
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 };
|
|
@@ -415,14 +445,47 @@ function buildRemediationPlan(findings, depVulns, rootPath) {
|
|
|
415
445
|
const plan = [];
|
|
416
446
|
let priority = 1;
|
|
417
447
|
|
|
448
|
+
// Exclude low-confidence findings (test files, docs, comments) from remediation plan
|
|
449
|
+
const actionable = findings.filter(f => f.confidence !== 'low');
|
|
450
|
+
|
|
418
451
|
// Priority order: secrets first, then by severity
|
|
419
|
-
const secretFindings =
|
|
420
|
-
const otherFindings =
|
|
452
|
+
const secretFindings = actionable.filter(f => f.category === 'secrets' || f.category === 'secret');
|
|
453
|
+
const otherFindings = actionable.filter(f => f.category !== 'secrets' && f.category !== 'secret');
|
|
421
454
|
|
|
422
455
|
// Group and sort
|
|
423
456
|
for (const sev of SEV_ORDER) {
|
|
424
|
-
// Secrets at this severity
|
|
425
|
-
|
|
457
|
+
// Secrets at this severity — group .env findings by file
|
|
458
|
+
const sevSecrets = secretFindings.filter(s => s.severity === sev);
|
|
459
|
+
const envGroups = new Map();
|
|
460
|
+
const nonEnvSecrets = [];
|
|
461
|
+
|
|
462
|
+
for (const f of sevSecrets) {
|
|
463
|
+
const relFile = path.relative(rootPath, f.file).replace(/\\/g, '/');
|
|
464
|
+
if (f.file.match(/\.env(\..*)?$/)) {
|
|
465
|
+
if (!envGroups.has(relFile)) envGroups.set(relFile, []);
|
|
466
|
+
envGroups.get(relFile).push(f);
|
|
467
|
+
} else {
|
|
468
|
+
nonEnvSecrets.push(f);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// One plan item per .env file
|
|
473
|
+
for (const [relFile, envFindings] of envGroups) {
|
|
474
|
+
const names = envFindings.map(f => f.title || f.rule).join(', ');
|
|
475
|
+
plan.push({
|
|
476
|
+
priority: priority++,
|
|
477
|
+
severity: sev,
|
|
478
|
+
category: 'secrets',
|
|
479
|
+
categoryLabel: 'SECRETS',
|
|
480
|
+
title: `${envFindings.length} secret${envFindings.length > 1 ? 's' : ''} in ${relFile} (${names})`,
|
|
481
|
+
file: relFile,
|
|
482
|
+
action: envFindings[0].fix || 'Ensure .env is in .gitignore and use a secrets manager for production',
|
|
483
|
+
effort: 'low',
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Individual items for non-.env secrets
|
|
488
|
+
for (const f of nonEnvSecrets) {
|
|
426
489
|
plan.push({
|
|
427
490
|
priority: priority++,
|
|
428
491
|
severity: sev,
|
|
@@ -694,6 +757,7 @@ async function findFiles(rootPath) {
|
|
|
694
757
|
return files.filter(file => {
|
|
695
758
|
const ext = path.extname(file).toLowerCase();
|
|
696
759
|
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
760
|
+
if (SKIP_FILENAMES.has(path.basename(file))) return false;
|
|
697
761
|
if (path.basename(file).endsWith('.min.js') || path.basename(file).endsWith('.min.css')) return false;
|
|
698
762
|
try { if (fs.statSync(file).size > MAX_FILE_SIZE) return false; } catch { return false; }
|
|
699
763
|
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
|
|