ship-safe 6.4.0 → 8.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/README.md +80 -23
- package/cli/agents/agent-attestation-agent.js +318 -0
- package/cli/agents/agent-config-scanner.js +15 -0
- package/cli/agents/agentic-security-agent.js +35 -0
- package/cli/agents/cicd-scanner.js +22 -0
- package/cli/agents/config-auditor.js +235 -0
- package/cli/agents/deep-analyzer.js +39 -19
- package/cli/agents/hermes-security-agent.js +536 -0
- package/cli/agents/index.js +65 -21
- package/cli/agents/managed-agent-scanner.js +333 -0
- package/cli/agents/memory-poisoning-agent.js +304 -0
- package/cli/agents/scoring-engine.js +16 -1
- package/cli/agents/supply-chain-agent.js +129 -3
- package/cli/bin/ship-safe.js +178 -5
- package/cli/commands/audit.js +116 -2
- package/cli/commands/autofix.js +383 -0
- package/cli/commands/env-audit.js +349 -0
- package/cli/commands/live-advisories.js +241 -0
- package/cli/commands/red-team.js +2 -2
- package/cli/commands/scan-mcp.js +78 -0
- package/cli/commands/scan-skill.js +248 -5
- package/cli/commands/watch.js +205 -0
- package/cli/index.js +5 -0
- package/cli/providers/llm-provider.js +89 -1
- package/cli/utils/compliance-map.js +66 -0
- package/cli/utils/hermes-tool-registry.js +252 -0
- package/cli/utils/patterns.js +1 -0
- package/cli/utils/plugin-loader.js +276 -0
- package/cli/utils/scan-playbook.js +312 -0
- package/cli/utils/security-memory.js +296 -0
- package/package.json +2 -2
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import fs from 'fs';
|
|
13
13
|
import path from 'path';
|
|
14
|
-
import { getComplianceSummary } from '../utils/compliance-map.js';
|
|
14
|
+
import { getComplianceSummary, getAgenticSummary, enrichAgenticRisk } from '../utils/compliance-map.js';
|
|
15
15
|
|
|
16
16
|
// =============================================================================
|
|
17
17
|
// SCORING CONFIGURATION
|
|
@@ -49,6 +49,7 @@ const FALLBACK_CATEGORY_MAP = {
|
|
|
49
49
|
'vibe': 'injection', // Vibe coding findings → Code Vulnerabilities
|
|
50
50
|
'exception': 'injection', // OWASP A10:2025 — Mishandling of Exceptional Conditions
|
|
51
51
|
'agent-config': 'llm', // Agent config security → AI/LLM category
|
|
52
|
+
'memory-poisoning': 'llm', // Memory poisoning → AI/LLM category
|
|
52
53
|
'recon': null, // skip recon findings
|
|
53
54
|
};
|
|
54
55
|
|
|
@@ -87,6 +88,11 @@ export class ScoringEngine {
|
|
|
87
88
|
};
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
// ── Enrich findings with OWASP Agentic AI Top 10 metadata ──────────────
|
|
92
|
+
for (const finding of findings) {
|
|
93
|
+
enrichAgenticRisk(finding);
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
// ── Classify findings into categories ─────────────────────────────────────
|
|
91
97
|
for (const finding of findings) {
|
|
92
98
|
const cat = this.resolveCategory(finding.category);
|
|
@@ -146,6 +152,14 @@ export class ScoringEngine {
|
|
|
146
152
|
compliance = null;
|
|
147
153
|
}
|
|
148
154
|
|
|
155
|
+
// ── OWASP Agentic AI Top 10 summary ──────────────────────────────────
|
|
156
|
+
let agenticSummary;
|
|
157
|
+
try {
|
|
158
|
+
agenticSummary = getAgenticSummary(findings);
|
|
159
|
+
} catch {
|
|
160
|
+
agenticSummary = null;
|
|
161
|
+
}
|
|
162
|
+
|
|
149
163
|
return {
|
|
150
164
|
score,
|
|
151
165
|
grade,
|
|
@@ -153,6 +167,7 @@ export class ScoringEngine {
|
|
|
153
167
|
totalFindings: findings.length,
|
|
154
168
|
totalDepVulns: depVulns.length,
|
|
155
169
|
compliance,
|
|
170
|
+
agenticSummary,
|
|
156
171
|
};
|
|
157
172
|
}
|
|
158
173
|
|
|
@@ -26,7 +26,7 @@ const COMPROMISED_PACKAGES = [
|
|
|
26
26
|
{
|
|
27
27
|
name: 'axios',
|
|
28
28
|
badVersions: ['1.8.2'],
|
|
29
|
-
note: 'TeamPCP/CanisterWorm campaign (Mar 31 2026). Malicious publish
|
|
29
|
+
note: 'TeamPCP/CanisterWorm campaign (Mar 31 2026). Attributed to Sapphire Sleet (North Korean state actor) by Microsoft Threat Intelligence. Malicious publish connected to C2 domain delivering a second-stage RAT with persistence.',
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
name: 'telnyx',
|
|
@@ -582,7 +582,8 @@ export class SupplyChainAudit extends BaseAgent {
|
|
|
582
582
|
}));
|
|
583
583
|
}
|
|
584
584
|
|
|
585
|
-
// ── 9.
|
|
585
|
+
// ── 9. Trojanized package behavioral signatures ───────────────────────────
|
|
586
|
+
// Patterns from Axios 1.8.2, LiteLLM 1.82.7, TeamPCP campaign (Mar 2026)
|
|
586
587
|
if (fs.existsSync(path.join(rootPath, 'node_modules'))) {
|
|
587
588
|
try {
|
|
588
589
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
@@ -590,7 +591,132 @@ export class SupplyChainAudit extends BaseAgent {
|
|
|
590
591
|
...(pkg.dependencies || {}),
|
|
591
592
|
...(pkg.devDependencies || {}),
|
|
592
593
|
};
|
|
593
|
-
|
|
594
|
+
|
|
595
|
+
for (const depName of Object.keys(allDeps).slice(0, 80)) {
|
|
596
|
+
const depDir = path.join(rootPath, 'node_modules', depName);
|
|
597
|
+
const depPkgPath = path.join(depDir, 'package.json');
|
|
598
|
+
if (!fs.existsSync(depPkgPath)) continue;
|
|
599
|
+
|
|
600
|
+
try {
|
|
601
|
+
const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
|
|
602
|
+
|
|
603
|
+
// 9a. Hidden dependencies added by attacker
|
|
604
|
+
// Axios 1.8.2 injected a hidden malicious dep that wasn't in the legitimate version.
|
|
605
|
+
// Check for deps that only exist in certain version ranges.
|
|
606
|
+
const depDeps = depPkg.dependencies || {};
|
|
607
|
+
for (const [subDep, subVer] of Object.entries(depDeps)) {
|
|
608
|
+
// Packages with very high download counts that suddenly gain unknown subdependencies
|
|
609
|
+
if (/^[a-z]+-[a-z0-9]+-[a-z0-9]+$/.test(subDep) && typeof subVer === 'string' && subVer.startsWith('git+')) {
|
|
610
|
+
findings.push(createFinding({
|
|
611
|
+
file: depPkgPath,
|
|
612
|
+
line: 0,
|
|
613
|
+
severity: 'critical',
|
|
614
|
+
category: 'supply-chain',
|
|
615
|
+
rule: 'TROJAN_HIDDEN_DEP',
|
|
616
|
+
title: `Suspicious Hidden Dependency in ${depName}: ${subDep}`,
|
|
617
|
+
description: `"${depName}" depends on "${subDep}" via a git URL. This matches the Axios/TeamPCP trojanization pattern where attackers inject a malicious dependency into a popular package.`,
|
|
618
|
+
matched: `${subDep}: ${subVer}`,
|
|
619
|
+
fix: `Compare this version's dependencies against the official release. If "${subDep}" was not in the previous version, this package may be trojanized.`,
|
|
620
|
+
}));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// 9b. Install scripts that read and exfiltrate env vars
|
|
625
|
+
// LiteLLM 1.82.7 harvested AWS/GCP/Azure tokens + SSH keys
|
|
626
|
+
const scripts = depPkg.scripts || {};
|
|
627
|
+
for (const hook of ['preinstall', 'install', 'postinstall']) {
|
|
628
|
+
const cmd = scripts[hook];
|
|
629
|
+
if (!cmd) continue;
|
|
630
|
+
|
|
631
|
+
// Environment variable harvesting
|
|
632
|
+
if (/process\.env|os\.environ|ENV\[|getenv/i.test(cmd) &&
|
|
633
|
+
/https?:|fetch|request|axios|curl|wget|net\./i.test(cmd)) {
|
|
634
|
+
findings.push(createFinding({
|
|
635
|
+
file: depPkgPath,
|
|
636
|
+
line: 0,
|
|
637
|
+
severity: 'critical',
|
|
638
|
+
category: 'supply-chain',
|
|
639
|
+
rule: 'TROJAN_ENV_EXFIL',
|
|
640
|
+
title: `Credential Harvesting in ${hook}: ${depName}`,
|
|
641
|
+
description: `"${depName}" reads environment variables and makes network requests during ${hook}. This is the exact pattern used in the LiteLLM/TeamPCP attack to steal cloud credentials.`,
|
|
642
|
+
matched: cmd.slice(0, 200),
|
|
643
|
+
fix: 'Remove this package immediately. Rotate any credentials (AWS, GCP, Azure tokens, SSH keys) that may have been exfiltrated.',
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// SSH key / credential file access in install scripts
|
|
648
|
+
if (/\.ssh|\.aws|\.azure|\.gcp|\.kube|\.docker|credentials|\.npmrc|\.pypirc/i.test(cmd)) {
|
|
649
|
+
findings.push(createFinding({
|
|
650
|
+
file: depPkgPath,
|
|
651
|
+
line: 0,
|
|
652
|
+
severity: 'critical',
|
|
653
|
+
category: 'supply-chain',
|
|
654
|
+
rule: 'TROJAN_CREDENTIAL_ACCESS',
|
|
655
|
+
title: `Credential File Access in ${hook}: ${depName}`,
|
|
656
|
+
description: `"${depName}" accesses credential files (.ssh, .aws, .kube, etc.) during ${hook}. This matches the TeamPCP credential theft pattern.`,
|
|
657
|
+
matched: cmd.slice(0, 200),
|
|
658
|
+
fix: 'Remove this package immediately and rotate all credentials in the accessed directories.',
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// 9c. Scan actual JS entry files for runtime exfiltration patterns
|
|
664
|
+
const main = depPkg.main || 'index.js';
|
|
665
|
+
const entryPath = path.join(depDir, main);
|
|
666
|
+
if (fs.existsSync(entryPath)) {
|
|
667
|
+
try {
|
|
668
|
+
const entryContent = fs.readFileSync(entryPath, 'utf-8');
|
|
669
|
+
if (entryContent.length < 500_000) {
|
|
670
|
+
// DNS-based exfiltration (encode data in subdomain)
|
|
671
|
+
if (/dns\.resolve|dns\.lookup/i.test(entryContent) &&
|
|
672
|
+
/process\.env|os\.hostname/i.test(entryContent)) {
|
|
673
|
+
findings.push(createFinding({
|
|
674
|
+
file: entryPath,
|
|
675
|
+
line: 1,
|
|
676
|
+
severity: 'high',
|
|
677
|
+
category: 'supply-chain',
|
|
678
|
+
rule: 'TROJAN_DNS_EXFIL',
|
|
679
|
+
title: `DNS Exfiltration Pattern: ${depName}`,
|
|
680
|
+
description: `"${depName}" combines DNS lookups with system/env data reads — a known technique for exfiltrating data via DNS subdomains to bypass firewalls.`,
|
|
681
|
+
matched: 'dns.resolve + process.env',
|
|
682
|
+
confidence: 'medium',
|
|
683
|
+
fix: 'Inspect the DNS usage. Legitimate packages rarely combine DNS with environment variable reading.',
|
|
684
|
+
}));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// WebSocket-based C2
|
|
688
|
+
if (/new\s+WebSocket/i.test(entryContent) &&
|
|
689
|
+
/process\.env|child_process|exec/i.test(entryContent)) {
|
|
690
|
+
findings.push(createFinding({
|
|
691
|
+
file: entryPath,
|
|
692
|
+
line: 1,
|
|
693
|
+
severity: 'critical',
|
|
694
|
+
category: 'supply-chain',
|
|
695
|
+
rule: 'TROJAN_WEBSOCKET_C2',
|
|
696
|
+
title: `WebSocket C2 Pattern: ${depName}`,
|
|
697
|
+
description: `"${depName}" opens WebSocket connections combined with system command execution — consistent with a Remote Access Trojan.`,
|
|
698
|
+
matched: 'WebSocket + child_process',
|
|
699
|
+
fix: 'Remove this package immediately. Scan for persistence mechanisms (cron jobs, startup scripts).',
|
|
700
|
+
}));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
} catch { /* skip */ }
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
} catch { /* skip */ }
|
|
707
|
+
}
|
|
708
|
+
} catch { /* skip */ }
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// ── 10. Blockchain C2 indicators (CanisterWorm / ICP) ────────────────────
|
|
712
|
+
if (fs.existsSync(path.join(rootPath, 'node_modules'))) {
|
|
713
|
+
try {
|
|
714
|
+
const pkg2 = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
715
|
+
const allDeps2 = {
|
|
716
|
+
...(pkg2.dependencies || {}),
|
|
717
|
+
...(pkg2.devDependencies || {}),
|
|
718
|
+
};
|
|
719
|
+
for (const depName of Object.keys(allDeps2)) {
|
|
594
720
|
const depPkgPath = path.join(rootPath, 'node_modules', depName, 'package.json');
|
|
595
721
|
if (!fs.existsSync(depPkgPath)) continue;
|
|
596
722
|
try {
|
package/cli/bin/ship-safe.js
CHANGED
|
@@ -47,6 +47,12 @@ import { abomCommand } from '../commands/abom.js';
|
|
|
47
47
|
import { updateIntelCommand } from '../commands/update-intel.js';
|
|
48
48
|
import { hooksCommand } from '../commands/hooks.js';
|
|
49
49
|
import { legalCommand } from '../commands/legal.js';
|
|
50
|
+
import { runLiveAdvisories } from '../commands/live-advisories.js';
|
|
51
|
+
import { envAuditCommand } from '../commands/env-audit.js';
|
|
52
|
+
import { autofixCommand } from '../commands/autofix.js';
|
|
53
|
+
import { memoryCommand } from '../utils/security-memory.js';
|
|
54
|
+
import { playbookCommand } from '../utils/scan-playbook.js';
|
|
55
|
+
import { listPluginFiles, scaffoldPlugin } from '../utils/plugin-loader.js';
|
|
50
56
|
import { ABOMGenerator } from '../agents/abom-generator.js';
|
|
51
57
|
import { PolicyEngine } from '../agents/policy-engine.js';
|
|
52
58
|
import { SBOMGenerator } from '../agents/sbom-generator.js';
|
|
@@ -202,7 +208,7 @@ program
|
|
|
202
208
|
// -----------------------------------------------------------------------------
|
|
203
209
|
program
|
|
204
210
|
.command('audit [path]')
|
|
205
|
-
.description('Full security audit: secrets +
|
|
211
|
+
.description('Full security audit: secrets + 22 agents + deps + score + deep analysis + remediation plan')
|
|
206
212
|
.option('--json', 'Output results as JSON')
|
|
207
213
|
.option('--sarif', 'Output results in SARIF format')
|
|
208
214
|
.option('--csv', 'Output results as CSV')
|
|
@@ -223,6 +229,8 @@ program
|
|
|
223
229
|
.option('--budget <cents>', 'Max spend in cents for deep analysis (default: 50)', parseInt)
|
|
224
230
|
.option('--verify', 'Check if leaked secrets are still active (probes provider APIs)')
|
|
225
231
|
.option('--include-legal', 'Also run the legal risk scan (DMCA, leaked source, IP disputes)')
|
|
232
|
+
.option('--agentic [iterations]', 'Agentic scan→fix→verify loop (default: 3 iterations, target score: 75)', (v) => v ? parseInt(v) : true)
|
|
233
|
+
.option('--agentic-target <score>', 'Target security score for agentic loop (default: 75)', parseInt)
|
|
226
234
|
.option('-v, --verbose', 'Verbose output')
|
|
227
235
|
.action(auditCommand);
|
|
228
236
|
|
|
@@ -243,7 +251,7 @@ program
|
|
|
243
251
|
// -----------------------------------------------------------------------------
|
|
244
252
|
program
|
|
245
253
|
.command('red-team [path]')
|
|
246
|
-
.description('Multi-agent security audit:
|
|
254
|
+
.description('Multi-agent security audit: 22 agents scan for 80+ attack classes')
|
|
247
255
|
.option('--agents <list>', 'Comma-separated list of agents to run')
|
|
248
256
|
.option('--json', 'Output results as JSON')
|
|
249
257
|
.option('--sarif', 'Output results in SARIF format')
|
|
@@ -268,8 +276,62 @@ program
|
|
|
268
276
|
.description('Continuous monitoring: watch files for security issues in real-time')
|
|
269
277
|
.option('--poll', 'Use polling mode (for network drives)')
|
|
270
278
|
.option('--configs', 'Watch only agent config files (openclaw.json, .cursorrules, mcp.json, etc.)')
|
|
279
|
+
.option('--deep', 'Run full agent scanning on changes (not just pattern matching)')
|
|
280
|
+
.option('--status', 'Show current watch status and exit')
|
|
281
|
+
.option('--threshold <score>', 'Alert when score drops below threshold', parseInt)
|
|
282
|
+
.option('--debounce <ms>', 'Debounce interval in ms (default: 1500)', parseInt)
|
|
283
|
+
.option('--slack [webhook]', 'Post findings to Slack webhook URL (or set SHIP_SAFE_SLACK_WEBHOOK env var)')
|
|
284
|
+
.option('--pr-comment', 'Post inline findings as GitHub PR review comments (requires gh CLI)')
|
|
271
285
|
.action(watchCommand);
|
|
272
286
|
|
|
287
|
+
// -----------------------------------------------------------------------------
|
|
288
|
+
// ADVISORIES COMMAND
|
|
289
|
+
// -----------------------------------------------------------------------------
|
|
290
|
+
program
|
|
291
|
+
.command('advisories [path]')
|
|
292
|
+
.description('Check dependencies against live advisory feeds (OSV.dev, GitHub Advisories)')
|
|
293
|
+
.option('--ecosystem <type>', 'Filter by ecosystem (npm, PyPI)')
|
|
294
|
+
.option('--json', 'Output as JSON')
|
|
295
|
+
.action(async (targetPath = '.', options) => {
|
|
296
|
+
const { resolve } = await import('path');
|
|
297
|
+
const absolutePath = resolve(targetPath);
|
|
298
|
+
try {
|
|
299
|
+
const result = await runLiveAdvisories(absolutePath, options);
|
|
300
|
+
if (options.json) {
|
|
301
|
+
console.log(JSON.stringify(result, null, 2));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
console.log();
|
|
305
|
+
console.log(chalk.cyan.bold(' Ship Safe — Live Advisories'));
|
|
306
|
+
console.log(chalk.gray(` Checked ${result.checked} dependencies against OSV.dev`));
|
|
307
|
+
console.log();
|
|
308
|
+
if (result.advisories.length === 0) {
|
|
309
|
+
console.log(chalk.green(' ✔ No known advisories for your current dependency versions.\n'));
|
|
310
|
+
} else {
|
|
311
|
+
const malware = result.advisories.filter(a => a.isMalware);
|
|
312
|
+
const vulns = result.advisories.filter(a => !a.isMalware);
|
|
313
|
+
if (malware.length > 0) {
|
|
314
|
+
console.log(chalk.red.bold(` !! ${malware.length} MALWARE ADVISORY(S) FOUND`));
|
|
315
|
+
for (const a of malware) {
|
|
316
|
+
console.log(chalk.red(` ${a.package}@${a.version} — ${a.id}: ${a.summary.slice(0, 80)}`));
|
|
317
|
+
}
|
|
318
|
+
console.log();
|
|
319
|
+
}
|
|
320
|
+
if (vulns.length > 0) {
|
|
321
|
+
console.log(chalk.yellow(` ${vulns.length} vulnerability advisory(s):`));
|
|
322
|
+
for (const a of vulns) {
|
|
323
|
+
const sev = a.severity === 'critical' ? chalk.red.bold(a.severity) : a.severity === 'high' ? chalk.yellow(a.severity) : chalk.blue(a.severity);
|
|
324
|
+
console.log(` ${sev} ${a.package}@${a.version} — ${a.id}`);
|
|
325
|
+
}
|
|
326
|
+
console.log();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.error(chalk.red(` Error: ${err.message}\n`));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
273
335
|
// -----------------------------------------------------------------------------
|
|
274
336
|
// SBOM COMMAND
|
|
275
337
|
// -----------------------------------------------------------------------------
|
|
@@ -404,6 +466,15 @@ How it works:
|
|
|
404
466
|
`)
|
|
405
467
|
.action(hooksCommand);
|
|
406
468
|
|
|
469
|
+
// -----------------------------------------------------------------------------
|
|
470
|
+
// ENV AUDIT COMMAND
|
|
471
|
+
// -----------------------------------------------------------------------------
|
|
472
|
+
program
|
|
473
|
+
.command('env-audit [path]')
|
|
474
|
+
.description('Credential health check: verify .env coverage, cross-reference source, check git history')
|
|
475
|
+
.option('--json', 'Output results as JSON')
|
|
476
|
+
.action(envAuditCommand);
|
|
477
|
+
|
|
407
478
|
// -----------------------------------------------------------------------------
|
|
408
479
|
// LEGAL COMMAND
|
|
409
480
|
// -----------------------------------------------------------------------------
|
|
@@ -430,6 +501,100 @@ program
|
|
|
430
501
|
.description('Diagnose environment: check Node.js, git, API keys, cache, and dependencies')
|
|
431
502
|
.action(doctorCommand);
|
|
432
503
|
|
|
504
|
+
// -----------------------------------------------------------------------------
|
|
505
|
+
// AUTOFIX COMMAND
|
|
506
|
+
// -----------------------------------------------------------------------------
|
|
507
|
+
program
|
|
508
|
+
.command('autofix [path]')
|
|
509
|
+
.description('Apply LLM-generated security fixes from a deep analysis report and open a GitHub PR')
|
|
510
|
+
.option('--report <file>', 'Path to ship-safe JSON report (default: ship-safe-report.json)')
|
|
511
|
+
.option('--severity <level>', 'Minimum severity to fix: critical, high, medium (default: high)')
|
|
512
|
+
.option('--dry-run', 'Preview fixes without applying them or creating a branch')
|
|
513
|
+
.option('--yes', 'Skip confirmation prompt')
|
|
514
|
+
.action((targetPath, options) => autofixCommand({ ...options, path: targetPath }));
|
|
515
|
+
|
|
516
|
+
// -----------------------------------------------------------------------------
|
|
517
|
+
// MEMORY COMMAND
|
|
518
|
+
// -----------------------------------------------------------------------------
|
|
519
|
+
program
|
|
520
|
+
.command('memory [subcommand]')
|
|
521
|
+
.description('Manage security memory: false-positive learning that auto-suppresses known safe findings')
|
|
522
|
+
.addHelpText('after', `
|
|
523
|
+
Subcommands:
|
|
524
|
+
list Show all suppressed findings in memory (default)
|
|
525
|
+
forget <key> Remove a specific entry by key
|
|
526
|
+
clear Wipe all memory entries
|
|
527
|
+
|
|
528
|
+
How it works:
|
|
529
|
+
When --deep analysis confirms a finding is a false positive, it is added to
|
|
530
|
+
.ship-safe/memory.json and suppressed automatically on all future scans.
|
|
531
|
+
`)
|
|
532
|
+
.argument('[args...]')
|
|
533
|
+
.action((subcommand, args, options) => memoryCommand(subcommand, args, options));
|
|
534
|
+
|
|
535
|
+
// -----------------------------------------------------------------------------
|
|
536
|
+
// PLAYBOOK COMMAND
|
|
537
|
+
// -----------------------------------------------------------------------------
|
|
538
|
+
program
|
|
539
|
+
.command('playbook [subcommand]')
|
|
540
|
+
.description('Manage scan playbooks: repo-specific context injected into every LLM analysis')
|
|
541
|
+
.addHelpText('after', `
|
|
542
|
+
Subcommands:
|
|
543
|
+
show Show the current playbook (default)
|
|
544
|
+
add-note "text" Add a custom note to the playbook
|
|
545
|
+
|
|
546
|
+
How it works:
|
|
547
|
+
After 2+ scans, a playbook is auto-generated in .ship-safe/playbook.md with
|
|
548
|
+
your repo's tech stack, auth patterns, and score history. This is injected
|
|
549
|
+
into the LLM system prompt so deep analysis is more accurate for your project.
|
|
550
|
+
`)
|
|
551
|
+
.argument('[args...]')
|
|
552
|
+
.action((subcommand, args, options) => playbookCommand(subcommand, args, options));
|
|
553
|
+
|
|
554
|
+
// -----------------------------------------------------------------------------
|
|
555
|
+
// PLUGINS COMMAND
|
|
556
|
+
// -----------------------------------------------------------------------------
|
|
557
|
+
program
|
|
558
|
+
.command('plugins [action]')
|
|
559
|
+
.description('Manage custom security agent plugins from .ship-safe/agents/')
|
|
560
|
+
.addHelpText('after', `
|
|
561
|
+
Actions:
|
|
562
|
+
list List loaded plugins (default)
|
|
563
|
+
new <name> Scaffold a new plugin in .ship-safe/agents/<name>.js
|
|
564
|
+
|
|
565
|
+
How it works:
|
|
566
|
+
Drop any .js file into .ship-safe/agents/ that exports a default class
|
|
567
|
+
extending BaseAgent with an analyze() method. It will be loaded automatically
|
|
568
|
+
on every audit or watch --deep run.
|
|
569
|
+
`)
|
|
570
|
+
.action((action, options) => {
|
|
571
|
+
const rootPath = path.resolve(process.cwd());
|
|
572
|
+
if (action === 'new') {
|
|
573
|
+
const pluginName = options.args?.[0] || options._name || 'my-rule';
|
|
574
|
+
try {
|
|
575
|
+
const filePath = scaffoldPlugin(rootPath, pluginName);
|
|
576
|
+
console.log(chalk.green(` ✔ Plugin scaffolded: ${filePath}`));
|
|
577
|
+
console.log(chalk.gray(' Edit the file to implement your custom rule, then run ship-safe audit to activate it.'));
|
|
578
|
+
} catch (err) {
|
|
579
|
+
console.error(chalk.red(` Error: ${err.message}`));
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
// list
|
|
584
|
+
const plugins = listPluginFiles(rootPath);
|
|
585
|
+
if (plugins.length === 0) {
|
|
586
|
+
console.log('\n No custom plugins found in .ship-safe/agents/');
|
|
587
|
+
console.log(chalk.gray(' Create one with: npx ship-safe plugins new my-rule\n'));
|
|
588
|
+
} else {
|
|
589
|
+
console.log(`\n ${chalk.cyan.bold('Custom Plugins')} — ${plugins.length} found\n`);
|
|
590
|
+
for (const p of plugins) {
|
|
591
|
+
console.log(` ${chalk.white(p.name)} ${chalk.gray(`(${(p.size / 1024).toFixed(1)} KB) ${p.path}`)}`);
|
|
592
|
+
}
|
|
593
|
+
console.log();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
433
598
|
// -----------------------------------------------------------------------------
|
|
434
599
|
// PARSE AND RUN
|
|
435
600
|
// -----------------------------------------------------------------------------
|
|
@@ -438,10 +603,10 @@ program
|
|
|
438
603
|
if (process.argv.length === 2) {
|
|
439
604
|
console.log(banner);
|
|
440
605
|
console.log(chalk.yellow('\nQuick start:\n'));
|
|
441
|
-
console.log(chalk.cyan.bold('
|
|
442
|
-
console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets +
|
|
606
|
+
console.log(chalk.cyan.bold(' v8.0 — Ship Safe × Hermes Agent'));
|
|
607
|
+
console.log(chalk.white(' npx ship-safe audit . ') + chalk.gray('# Full audit: secrets + 22 agents + deps + remediation'));
|
|
443
608
|
console.log(chalk.white(' npx ship-safe audit . --deep') + chalk.gray('# LLM-powered taint analysis (Anthropic/Ollama)'));
|
|
444
|
-
console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('#
|
|
609
|
+
console.log(chalk.white(' npx ship-safe red-team . ') + chalk.gray('# 22-agent red team scan (80+ attack classes)'));
|
|
445
610
|
console.log(chalk.white(' npx ship-safe vibe-check . ') + chalk.gray('# Fun security check with emoji & shareable badge'));
|
|
446
611
|
console.log(chalk.white(' npx ship-safe benchmark . ') + chalk.gray('# Compare score against industry averages'));
|
|
447
612
|
console.log(chalk.white(' npx ship-safe ci . ') + chalk.gray('# CI/CD mode: scan, score, exit code'));
|
|
@@ -464,9 +629,17 @@ if (process.argv.length === 2) {
|
|
|
464
629
|
console.log(chalk.white(' npx ship-safe rotate . ') + chalk.gray('# Revoke exposed keys (provider guides)'));
|
|
465
630
|
console.log(chalk.white(' npx ship-safe deps . ') + chalk.gray('# Audit dependencies for CVEs'));
|
|
466
631
|
console.log(chalk.white(' npx ship-safe score . ') + chalk.gray('# Security health score (0-100)'));
|
|
632
|
+
console.log(chalk.white(' npx ship-safe env-audit . ') + chalk.gray('# Credential health check (after stripe projects env --pull)'));
|
|
467
633
|
console.log(chalk.white(' npx ship-safe hooks install ') + chalk.gray('# Real-time security gate inside Claude Code (PreToolUse/PostToolUse)'));
|
|
468
634
|
console.log(chalk.white(' npx ship-safe guard ') + chalk.gray('# Block git push if secrets found'));
|
|
469
635
|
console.log(chalk.white(' npx ship-safe init ') + chalk.gray('# Add security configs to your project'));
|
|
636
|
+
console.log();
|
|
637
|
+
console.log(chalk.gray(' Intelligence commands:'));
|
|
638
|
+
console.log(chalk.white(' npx ship-safe autofix . ') + chalk.gray('# Apply LLM fixes from --deep report, open PR'));
|
|
639
|
+
console.log(chalk.white(' npx ship-safe memory list ') + chalk.gray('# View / manage false-positive memory'));
|
|
640
|
+
console.log(chalk.white(' npx ship-safe playbook show ') + chalk.gray('# View repo-specific LLM context playbook'));
|
|
641
|
+
console.log(chalk.white(' npx ship-safe plugins list ') + chalk.gray('# Manage custom agent plugins'));
|
|
642
|
+
console.log(chalk.white(' npx ship-safe watch . --deep --slack ') + chalk.gray('# Guardian mode with Slack alerts + PR comments'));
|
|
470
643
|
console.log(chalk.white('\n npx ship-safe --help ') + chalk.gray('# Show all options'));
|
|
471
644
|
console.log();
|
|
472
645
|
process.exit(0);
|
package/cli/commands/audit.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 fg from 'fast-glob';
|
|
20
|
-
import { buildOrchestrator } from '../agents/index.js';
|
|
20
|
+
import { buildOrchestrator, buildOrchestratorAsync } from '../agents/index.js';
|
|
21
21
|
import { LegalRiskAgent } from '../agents/legal-risk-agent.js';
|
|
22
22
|
import { ScoringEngine } from '../agents/scoring-engine.js';
|
|
23
23
|
import { PolicyEngine } from '../agents/policy-engine.js';
|
|
@@ -37,8 +37,11 @@ import {
|
|
|
37
37
|
import { isHighEntropyMatch, getConfidence } from '../utils/entropy.js';
|
|
38
38
|
import { CacheManager } from '../utils/cache-manager.js';
|
|
39
39
|
import { filterBaseline } from './baseline.js';
|
|
40
|
+
import { SecurityMemory } from '../utils/security-memory.js';
|
|
41
|
+
import { ScanPlaybook } from '../utils/scan-playbook.js';
|
|
40
42
|
import { generatePDF, generatePrintHTML, isChromeAvailable } from '../utils/pdf-generator.js';
|
|
41
43
|
import { SecretsVerifier } from '../utils/secrets-verifier.js';
|
|
44
|
+
import { applyInlineAnnotations } from './autofix.js';
|
|
42
45
|
|
|
43
46
|
// =============================================================================
|
|
44
47
|
// CONSTANTS
|
|
@@ -186,7 +189,7 @@ export async function auditCommand(targetPath = '.', options = {}) {
|
|
|
186
189
|
}
|
|
187
190
|
|
|
188
191
|
// ── Phase 2: Agent Scan ───────────────────────────────────────────────────
|
|
189
|
-
const orchestrator =
|
|
192
|
+
const orchestrator = await buildOrchestratorAsync(absolutePath, { quiet: true });
|
|
190
193
|
const registeredAgentCount = orchestrator.agents?.length || 15;
|
|
191
194
|
const agentSpinner = machineOutput ? null : ora({ text: chalk.white(`[Phase 2/4] Running ${registeredAgentCount} security agents...`), color: 'cyan' }).start();
|
|
192
195
|
let agentFindings = [];
|
|
@@ -278,6 +281,30 @@ export async function auditCommand(targetPath = '.', options = {}) {
|
|
|
278
281
|
}
|
|
279
282
|
}
|
|
280
283
|
|
|
284
|
+
// ── Scan Playbook — update with latest recon + findings ─────────────────
|
|
285
|
+
try {
|
|
286
|
+
const playbook = new ScanPlaybook(absolutePath);
|
|
287
|
+
const suppressedRules = new SecurityMemory(absolutePath).list().map(e => e.rule).filter(Boolean);
|
|
288
|
+
playbook.update(recon, { score: scoreResult.score, grade: scoreResult.grade?.letter || scoreResult.grade, totalFindings: filteredFindings.length }, filteredFindings, suppressedRules);
|
|
289
|
+
} catch { /* non-fatal */ }
|
|
290
|
+
|
|
291
|
+
// ── Security Memory Filter ──────────────────────────────────────────────
|
|
292
|
+
// Auto-learn false positives from deep analysis results, then suppress
|
|
293
|
+
// any finding that memory recognises from a previous scan.
|
|
294
|
+
const secMemory = new SecurityMemory(absolutePath);
|
|
295
|
+
if (options.deep) {
|
|
296
|
+
// After deep analysis ran, learn any new false positives
|
|
297
|
+
const newFPs = secMemory.learnFromAnalysis(filteredFindings);
|
|
298
|
+
if (newFPs > 0 && !machineOutput) {
|
|
299
|
+
console.log(chalk.gray(` Memory: ${newFPs} new false positive(s) learned and will be suppressed in future scans`));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const { kept: memFiltered, suppressedCount: memSuppressed } = secMemory.filter(filteredFindings);
|
|
303
|
+
filteredFindings = memFiltered;
|
|
304
|
+
if (memSuppressed > 0 && !machineOutput) {
|
|
305
|
+
console.log(chalk.gray(` Memory: ${memSuppressed} previously-confirmed false positive(s) suppressed`));
|
|
306
|
+
}
|
|
307
|
+
|
|
281
308
|
// Count suppressions (ship-safe-ignore comments)
|
|
282
309
|
const suppressions = countSuppressions(allFiles);
|
|
283
310
|
|
|
@@ -395,6 +422,11 @@ export async function auditCommand(targetPath = '.', options = {}) {
|
|
|
395
422
|
// ── Build Remediation Plan ────────────────────────────────────────────────
|
|
396
423
|
const remediationPlan = buildRemediationPlan(filteredFindings, depVulns, absolutePath);
|
|
397
424
|
|
|
425
|
+
// Skip all output and file generation for inner agentic re-scans
|
|
426
|
+
if (options._agenticInner) {
|
|
427
|
+
return { score: scoreResult.score, findings: filteredFindings };
|
|
428
|
+
}
|
|
429
|
+
|
|
398
430
|
// ── Output ────────────────────────────────────────────────────────────────
|
|
399
431
|
console.log();
|
|
400
432
|
|
|
@@ -465,9 +497,91 @@ export async function auditCommand(targetPath = '.', options = {}) {
|
|
|
465
497
|
console.log();
|
|
466
498
|
}
|
|
467
499
|
|
|
500
|
+
// ── Agentic Loop (--agentic) ────────────────────────────────────────────
|
|
501
|
+
// Scan → annotate fixes → re-scan cycle until score >= target or maxIter.
|
|
502
|
+
// NOTE: process.exit() is deferred until after the loop so all iterations
|
|
503
|
+
// can run. The inner re-scans use _agenticInner: true to skip process.exit.
|
|
504
|
+
if (options.agentic && !options._agenticInner) {
|
|
505
|
+
const maxIter = typeof options.agentic === 'number' ? options.agentic : 3;
|
|
506
|
+
const targetScore = options.agenticTarget ?? 75;
|
|
507
|
+
let iteration = 1;
|
|
508
|
+
let currentScore = scoreResult.score;
|
|
509
|
+
let currentFindings = filteredFindings;
|
|
510
|
+
|
|
511
|
+
if (!machineOutput) {
|
|
512
|
+
console.log();
|
|
513
|
+
console.log(chalk.cyan.bold(` Agentic mode: scan→fix→verify loop (max ${maxIter} iterations, target score: ${targetScore})`));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
while (currentScore < targetScore && iteration <= maxIter) {
|
|
517
|
+
if (!machineOutput) {
|
|
518
|
+
console.log(chalk.cyan(`\n ─── Agentic iteration ${iteration}/${maxIter} (current score: ${currentScore}) ───`));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const actionable = currentFindings.filter(f => f.fix && f.severity !== 'low');
|
|
522
|
+
if (actionable.length === 0) {
|
|
523
|
+
if (!machineOutput) console.log(chalk.gray(' No auto-fixable findings — stopping agentic loop.'));
|
|
524
|
+
break;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Delegate annotation to autofix module (handles comment style, idempotency, NEVER_EDIT list)
|
|
528
|
+
const fixCount = applyInlineAnnotations(actionable);
|
|
529
|
+
if (!machineOutput) {
|
|
530
|
+
console.log(chalk.yellow(` Annotated ${fixCount} finding(s). Re-scanning...`));
|
|
531
|
+
}
|
|
532
|
+
if (fixCount === 0) break;
|
|
533
|
+
|
|
534
|
+
// Re-scan without recursing into the agentic loop or calling process.exit
|
|
535
|
+
const innerResult = await runAuditInner(targetPath, {
|
|
536
|
+
...options,
|
|
537
|
+
agentic: false,
|
|
538
|
+
_agenticInner: true,
|
|
539
|
+
json: false,
|
|
540
|
+
sarif: false,
|
|
541
|
+
csv: false,
|
|
542
|
+
md: false,
|
|
543
|
+
html: false,
|
|
544
|
+
pdf: false,
|
|
545
|
+
quiet: true,
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const prevScore = currentScore;
|
|
549
|
+
currentScore = innerResult?.score ?? currentScore;
|
|
550
|
+
currentFindings = innerResult?.findings ?? currentFindings;
|
|
551
|
+
|
|
552
|
+
if (!machineOutput) {
|
|
553
|
+
const diff = currentScore - prevScore;
|
|
554
|
+
const arrow = diff > 0 ? chalk.green(`↑ +${diff.toFixed(1)}`) : diff < 0 ? chalk.red(`↓ ${diff.toFixed(1)}`) : chalk.gray('→ 0');
|
|
555
|
+
console.log(chalk.cyan(` Re-scan score: ${currentScore} ${arrow}`));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
iteration++;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (!machineOutput) {
|
|
562
|
+
if (currentScore >= targetScore) {
|
|
563
|
+
console.log(chalk.green.bold(`\n Agentic loop complete — target score ${targetScore} reached (${currentScore}).`));
|
|
564
|
+
} else {
|
|
565
|
+
console.log(chalk.yellow(`\n Agentic loop stopped after ${iteration - 1} iteration(s). Final score: ${currentScore}`));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
468
570
|
process.exit(scoreResult.score >= 75 ? 0 : 1);
|
|
469
571
|
}
|
|
470
572
|
|
|
573
|
+
/**
|
|
574
|
+
* Run a lightweight inner audit that returns { score, findings } without
|
|
575
|
+
* calling process.exit(). Used exclusively by the --agentic loop.
|
|
576
|
+
*/
|
|
577
|
+
async function runAuditInner(targetPath, options) {
|
|
578
|
+
try {
|
|
579
|
+
return await auditCommand(targetPath, options);
|
|
580
|
+
} catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
471
585
|
// =============================================================================
|
|
472
586
|
// REMEDIATION PLAN BUILDER
|
|
473
587
|
// =============================================================================
|