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
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
SECRET_PATTERNS,
|
|
45
45
|
SKIP_DIRS,
|
|
46
46
|
SKIP_EXTENSIONS,
|
|
47
|
+
SKIP_FILENAMES,
|
|
47
48
|
TEST_FILE_PATTERNS,
|
|
48
49
|
MAX_FILE_SIZE
|
|
49
50
|
} from '../utils/patterns.js';
|
|
@@ -80,7 +81,7 @@ function envVarRef(varName, framework, filePath = '') {
|
|
|
80
81
|
|
|
81
82
|
/**
|
|
82
83
|
* Convert pattern name to SCREAMING_SNAKE_CASE env var name.
|
|
83
|
-
* e.g. "OpenAI API Key" → "OPENAI_API_KEY"
|
|
84
|
+
* e.g. "OpenAI API Key" → "OPENAI_API_KEY" // ship-safe-ignore — example name in doc comment, not a secret value
|
|
84
85
|
* "[custom] My Token" → "MY_TOKEN"
|
|
85
86
|
*/
|
|
86
87
|
function patternToEnvVar(patternName) {
|
|
@@ -94,7 +95,7 @@ function patternToEnvVar(patternName) {
|
|
|
94
95
|
|
|
95
96
|
/**
|
|
96
97
|
* Ensure env var name is unique within the current session.
|
|
97
|
-
* If "OPENAI_API_KEY" is already taken, returns "OPENAI_API_KEY_2".
|
|
98
|
+
* If "OPENAI_API_KEY" is already taken, returns "OPENAI_API_KEY_2". // ship-safe-ignore — example in doc comment
|
|
98
99
|
*/
|
|
99
100
|
function uniqueVarName(baseName, seen) {
|
|
100
101
|
if (!seen.has(baseName)) return baseName;
|
|
@@ -110,9 +111,9 @@ function uniqueVarName(baseName, seen) {
|
|
|
110
111
|
/**
|
|
111
112
|
* Compute what to replace in a line and extract the raw secret value.
|
|
112
113
|
*
|
|
113
|
-
* Given: matched = 'apiKey = "sk-abc123xyz"', envRef = 'process.env.OPENAI_API_KEY'
|
|
114
|
+
* Given: matched = 'apiKey = "sk-abc123xyz"', envRef = 'process.env.OPENAI_API_KEY' // ship-safe-ignore — example in doc comment, no real secret
|
|
114
115
|
* Returns:
|
|
115
|
-
* replacement = 'apiKey = process.env.OPENAI_API_KEY'
|
|
116
|
+
* replacement = 'apiKey = process.env.OPENAI_API_KEY' // ship-safe-ignore — example replacement in doc comment
|
|
116
117
|
* secretValue = 'sk-abc123xyz'
|
|
117
118
|
*/
|
|
118
119
|
function computeReplacement(matched, envRef) {
|
|
@@ -401,7 +402,7 @@ function updateEnvExample(rootPath, envVars) {
|
|
|
401
402
|
|
|
402
403
|
function checkPublicRepo(rootPath) {
|
|
403
404
|
try {
|
|
404
|
-
const remotes = execSync('git remote -v', { cwd: rootPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
405
|
+
const remotes = execSync('git remote -v', { cwd: rootPath, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }); // ship-safe-ignore
|
|
405
406
|
if (remotes.includes('github.com') || remotes.includes('gitlab.com')) {
|
|
406
407
|
// We can't easily check visibility without an API call, so warn if it looks like a hosted repo
|
|
407
408
|
console.log();
|
|
@@ -419,7 +420,7 @@ function checkPublicRepo(rootPath) {
|
|
|
419
420
|
function stageFiles(files, rootPath) {
|
|
420
421
|
if (files.length === 0) return;
|
|
421
422
|
try {
|
|
422
|
-
execFileSync('git', ['add', ...files], { cwd: rootPath, stdio: 'inherit' });
|
|
423
|
+
execFileSync('git', ['add', ...files], { cwd: rootPath, stdio: 'inherit' }); // ship-safe-ignore
|
|
423
424
|
output.success(`Staged ${files.length} file(s) with git add`);
|
|
424
425
|
} catch {
|
|
425
426
|
output.warning('Could not stage files — run git add manually.');
|
|
@@ -440,6 +441,7 @@ async function findFiles(rootPath) {
|
|
|
440
441
|
for (const file of files) {
|
|
441
442
|
const ext = path.extname(file).toLowerCase();
|
|
442
443
|
if (SKIP_EXTENSIONS.has(ext)) continue;
|
|
444
|
+
if (SKIP_FILENAMES.has(path.basename(file))) continue;
|
|
443
445
|
const basename = path.basename(file);
|
|
444
446
|
if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) continue;
|
|
445
447
|
if (TEST_FILE_PATTERNS.some(p => p.test(file))) continue;
|
|
@@ -484,6 +486,137 @@ async function scanFile(filePath) {
|
|
|
484
486
|
return findings;
|
|
485
487
|
}
|
|
486
488
|
|
|
489
|
+
// =============================================================================
|
|
490
|
+
// AUTO-FIX AGENT FINDINGS (--all flag)
|
|
491
|
+
// =============================================================================
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Apply automatic fixes for common agent findings:
|
|
495
|
+
* 1. Pin GitHub Actions to SHA (uses@tag → uses@sha)
|
|
496
|
+
* 2. Add httpOnly/secure/sameSite to cookie-setting code
|
|
497
|
+
* 3. Add USER directive to Dockerfiles without one
|
|
498
|
+
* 4. Disable debug mode (hardcoded debug → env var) ship-safe-ignore
|
|
499
|
+
*
|
|
500
|
+
* Returns array of human-readable fix descriptions.
|
|
501
|
+
*/
|
|
502
|
+
async function autoFixAgentFindings(rootPath, options) { // ship-safe-ignore — function name, not an agent with elevated permissions
|
|
503
|
+
const fixes = [];
|
|
504
|
+
|
|
505
|
+
// ── 1. Pin GitHub Actions to commit SHA ─────────────────────────────
|
|
506
|
+
const workflowDir = path.join(rootPath, '.github', 'workflows');
|
|
507
|
+
if (fs.existsSync(workflowDir)) {
|
|
508
|
+
const yamlFiles = fs.readdirSync(workflowDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
509
|
+
for (const file of yamlFiles) {
|
|
510
|
+
const filePath = path.join(workflowDir, file);
|
|
511
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
512
|
+
let modified = false;
|
|
513
|
+
|
|
514
|
+
// Match uses: owner/repo@v1.2.3 or uses: owner/repo@v1 (not already a SHA)
|
|
515
|
+
const usesRegex = /^(\s+uses:\s+)([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)@(v?\d+[^\s#]*)/gm;
|
|
516
|
+
content = content.replace(usesRegex, (match, prefix, repo, tag) => {
|
|
517
|
+
// Skip if already pinned to SHA (40+ hex chars)
|
|
518
|
+
if (/^[0-9a-f]{40,}$/i.test(tag)) return match;
|
|
519
|
+
// Add a comment noting the original tag
|
|
520
|
+
modified = true;
|
|
521
|
+
return `${prefix}${repo}@${tag} # TODO: pin to SHA for supply chain safety`;
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
if (modified) {
|
|
525
|
+
fs.writeFileSync(filePath, content);
|
|
526
|
+
fixes.push(`.github/workflows/${file} — marked unpinned Actions for SHA pinning`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ── 2. Add httpOnly/secure/sameSite to cookie settings ──────────────
|
|
532
|
+
const cookieFiles = await fg('**/*.{js,ts,jsx,tsx,mjs}', {
|
|
533
|
+
cwd: rootPath, absolute: true, ignore: ['**/node_modules/**', '**/dist/**', '**/build/**'],
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
for (const filePath of cookieFiles.slice(0, 200)) {
|
|
537
|
+
try {
|
|
538
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
539
|
+
let modified = false;
|
|
540
|
+
|
|
541
|
+
// Pattern: res.cookie('name', value, { ... }) missing httpOnly
|
|
542
|
+
// Only fix if we see res.cookie with an options object that lacks httpOnly
|
|
543
|
+
const cookiePattern = /(res\.cookie\s*\([^)]*,\s*\{)([^}]*)(})/g;
|
|
544
|
+
content = content.replace(cookiePattern, (match, prefix, opts, suffix) => {
|
|
545
|
+
if (/httpOnly/i.test(opts)) return match; // already has it
|
|
546
|
+
modified = true;
|
|
547
|
+
const additions = [];
|
|
548
|
+
if (!/httpOnly/i.test(opts)) additions.push(' httpOnly: true');
|
|
549
|
+
if (!/secure/i.test(opts)) additions.push(' secure: true');
|
|
550
|
+
if (!/sameSite/i.test(opts)) additions.push(" sameSite: 'strict'");
|
|
551
|
+
const addStr = additions.length > 0 ? ',' + additions.join(',') : '';
|
|
552
|
+
return prefix + opts.trimEnd() + addStr + ' ' + suffix;
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
if (modified) {
|
|
556
|
+
fs.writeFileSync(filePath, content);
|
|
557
|
+
const rel = path.relative(rootPath, filePath);
|
|
558
|
+
fixes.push(`${rel} — added httpOnly/secure/sameSite to cookie options`);
|
|
559
|
+
}
|
|
560
|
+
} catch { /* skip */ }
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ── 3. Add USER directive to Dockerfiles ────────────────────────────
|
|
564
|
+
const dockerfiles = await fg('**/Dockerfile*', {
|
|
565
|
+
cwd: rootPath, absolute: true, ignore: ['**/node_modules/**'],
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
for (const filePath of dockerfiles) {
|
|
569
|
+
try {
|
|
570
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
571
|
+
if (/^\s*USER\s+/m.test(content)) continue; // already has USER
|
|
572
|
+
|
|
573
|
+
// Add USER before CMD/ENTRYPOINT
|
|
574
|
+
const cmdMatch = content.match(/^(CMD|ENTRYPOINT)\s/m);
|
|
575
|
+
if (cmdMatch) {
|
|
576
|
+
const idx = content.indexOf(cmdMatch[0]);
|
|
577
|
+
content = content.slice(0, idx) + 'USER 1001\n' + content.slice(idx);
|
|
578
|
+
fs.writeFileSync(filePath, content);
|
|
579
|
+
const rel = path.relative(rootPath, filePath);
|
|
580
|
+
fixes.push(`${rel} — added USER 1001 before CMD/ENTRYPOINT`);
|
|
581
|
+
}
|
|
582
|
+
} catch { /* skip */ }
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ── 4. Replace hardcoded debug settings with env var reference ──── ship-safe-ignore
|
|
586
|
+
const configFiles = await fg('**/*.{py,js,ts,env.example}', {
|
|
587
|
+
cwd: rootPath, absolute: true, ignore: ['**/node_modules/**', '**/dist/**', '**/.env'],
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
for (const filePath of configFiles.slice(0, 100)) {
|
|
591
|
+
try {
|
|
592
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
593
|
+
let modified = false;
|
|
594
|
+
|
|
595
|
+
if (filePath.endsWith('.py')) {
|
|
596
|
+
// Django/Flask: DEBUG=True → env var reference (ship-safe-ignore — regex pattern, not actual debug setting)
|
|
597
|
+
content = content.replace(/^(\s*DEBUG\s*=\s*)True\s*$/gm, (match, prefix) => {
|
|
598
|
+
modified = true;
|
|
599
|
+
return `${prefix}os.environ.get('DEBUG', 'False') == 'True'`;
|
|
600
|
+
});
|
|
601
|
+
} else { // ship-safe-ignore — regex pattern matching debug settings, not actual debug config
|
|
602
|
+
// JS/TS: debug:true → process.env.DEBUG reference
|
|
603
|
+
content = content.replace(/^(\s*(?:DEBUG|debug)\s*[:=]\s*)true\s*([,;]?\s*)$/gm, (match, prefix, suffix) => {
|
|
604
|
+
modified = true;
|
|
605
|
+
return `${prefix}process.env.DEBUG === 'true'${suffix}`;
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (modified) {
|
|
610
|
+
fs.writeFileSync(filePath, content);
|
|
611
|
+
const rel = path.relative(rootPath, filePath);
|
|
612
|
+
fixes.push(`${rel} — replaced hardcoded debug setting with env var`); // ship-safe-ignore
|
|
613
|
+
}
|
|
614
|
+
} catch { /* skip */ }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return fixes;
|
|
618
|
+
}
|
|
619
|
+
|
|
487
620
|
// =============================================================================
|
|
488
621
|
// MAIN COMMAND
|
|
489
622
|
// =============================================================================
|
|
@@ -632,7 +765,22 @@ export async function remediateCommand(targetPath = '.', options = {}) {
|
|
|
632
765
|
stageFiles(modifiedFiles, absolutePath);
|
|
633
766
|
}
|
|
634
767
|
|
|
635
|
-
// ── 12.
|
|
768
|
+
// ── 12. Auto-fix agent findings if --all ─────────────────────────────
|
|
769
|
+
if (options.all) {
|
|
770
|
+
const autoFixResults = await autoFixAgentFindings(absolutePath, options);
|
|
771
|
+
if (autoFixResults.length > 0) {
|
|
772
|
+
console.log();
|
|
773
|
+
output.success(`Auto-fixed ${autoFixResults.length} additional issue(s):`);
|
|
774
|
+
for (const r of autoFixResults) {
|
|
775
|
+
console.log(chalk.gray(` ✓ ${r}`));
|
|
776
|
+
}
|
|
777
|
+
if (options.stage) {
|
|
778
|
+
stageFiles(autoFixResults.map(r => r.split(' — ')[0]).filter(f => fs.existsSync(path.resolve(absolutePath, f))), absolutePath);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// ── 13. Summary ───────────────────────────────────────────────────────────
|
|
636
784
|
console.log();
|
|
637
785
|
console.log(chalk.cyan.bold(' Remediation complete'));
|
|
638
786
|
console.log(chalk.gray(` Files fixed: ${modifiedFiles.length}`));
|