ship-safe 6.1.1 → 6.2.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.
Files changed (47) hide show
  1. package/README.md +735 -641
  2. package/cli/agents/api-fuzzer.js +345 -345
  3. package/cli/agents/auth-bypass-agent.js +348 -348
  4. package/cli/agents/base-agent.js +272 -272
  5. package/cli/agents/cicd-scanner.js +236 -201
  6. package/cli/agents/config-auditor.js +521 -521
  7. package/cli/agents/deep-analyzer.js +6 -2
  8. package/cli/agents/git-history-scanner.js +170 -170
  9. package/cli/agents/html-reporter.js +568 -568
  10. package/cli/agents/index.js +84 -84
  11. package/cli/agents/injection-tester.js +500 -500
  12. package/cli/agents/llm-redteam.js +251 -251
  13. package/cli/agents/mobile-scanner.js +231 -231
  14. package/cli/agents/orchestrator.js +322 -322
  15. package/cli/agents/pii-compliance-agent.js +301 -301
  16. package/cli/agents/scoring-engine.js +248 -248
  17. package/cli/agents/supabase-rls-agent.js +154 -154
  18. package/cli/agents/supply-chain-agent.js +650 -507
  19. package/cli/bin/ship-safe.js +452 -426
  20. package/cli/commands/agent.js +608 -608
  21. package/cli/commands/audit.js +986 -980
  22. package/cli/commands/baseline.js +193 -193
  23. package/cli/commands/ci.js +342 -342
  24. package/cli/commands/deps.js +516 -516
  25. package/cli/commands/doctor.js +159 -159
  26. package/cli/commands/fix.js +218 -218
  27. package/cli/commands/hooks.js +268 -0
  28. package/cli/commands/init.js +407 -407
  29. package/cli/commands/mcp.js +304 -304
  30. package/cli/commands/red-team.js +7 -1
  31. package/cli/commands/remediate.js +798 -798
  32. package/cli/commands/rotate.js +571 -571
  33. package/cli/commands/scan.js +569 -569
  34. package/cli/commands/score.js +449 -449
  35. package/cli/commands/watch.js +281 -281
  36. package/cli/hooks/patterns.js +313 -0
  37. package/cli/hooks/post-tool-use.js +140 -0
  38. package/cli/hooks/pre-tool-use.js +186 -0
  39. package/cli/index.js +73 -69
  40. package/cli/providers/llm-provider.js +397 -287
  41. package/cli/utils/autofix-rules.js +74 -74
  42. package/cli/utils/cache-manager.js +311 -311
  43. package/cli/utils/output.js +230 -230
  44. package/cli/utils/patterns.js +1121 -1121
  45. package/cli/utils/pdf-generator.js +94 -94
  46. package/package.json +69 -69
  47. package/configs/supabase/rls-templates.sql +0 -242
@@ -100,8 +100,12 @@ export class DeepAnalyzer {
100
100
  return new DeepAnalyzer({ provider, ...options });
101
101
  }
102
102
 
103
- // Auto-detect from env
104
- const provider = autoDetectProvider(rootPath);
103
+ // Auto-detect from env, honouring explicit --provider / --base-url / --model
104
+ const provider = autoDetectProvider(rootPath, {
105
+ provider: options.provider,
106
+ baseUrl: options.baseUrl,
107
+ model: options.model,
108
+ });
105
109
  if (!provider) return null;
106
110
 
107
111
  return new DeepAnalyzer({ provider, ...options });
@@ -1,170 +1,170 @@
1
- /**
2
- * GitHistoryScanner Agent
3
- * ========================
4
- *
5
- * Scans git commit history for secrets that were committed
6
- * and later removed but remain in repository history.
7
- * These are the most dangerous secrets — developers think
8
- * they're deleted but they're still accessible.
9
- */
10
-
11
- import { execSync, execFileSync } from 'child_process';
12
- import path from 'path';
13
- import { BaseAgent, createFinding } from './base-agent.js';
14
- import { SECRET_PATTERNS } from '../utils/patterns.js';
15
-
16
- // Compile a fast combined regex from all secret patterns
17
- const FAST_SECRET_PATTERNS = SECRET_PATTERNS.map(p => ({
18
- name: p.name,
19
- pattern: p.pattern,
20
- severity: p.severity,
21
- }));
22
-
23
- export class GitHistoryScanner extends BaseAgent {
24
- constructor() {
25
- super('GitHistoryScanner', 'Scan git history for leaked secrets', 'history');
26
- }
27
-
28
- async analyze(context) {
29
- const { rootPath, options } = context;
30
- const findings = [];
31
-
32
- // Check if this is a git repository
33
- if (!this.isGitRepo(rootPath)) return [];
34
-
35
- try {
36
- // Get recent commits (default: last 50, configurable)
37
- const maxCommits = options?.maxCommits || 50;
38
- const since = options?.since || null;
39
-
40
- const gitArgs = ['-C', rootPath, 'log', '--all', '--diff-filter=A', '--diff-filter=M', '-p', '--no-color', `--max-count=${parseInt(maxCommits, 10)}`];
41
- if (since) {
42
- gitArgs.push(`--since=${since}`);
43
- }
44
-
45
- let diffOutput;
46
- try {
47
- diffOutput = execFileSync('git', gitArgs, {
48
- cwd: rootPath,
49
- encoding: 'utf-8',
50
- maxBuffer: 50 * 1024 * 1024, // 50MB buffer
51
- timeout: 60000, // 60s timeout
52
- });
53
- } catch {
54
- // git log failed — might be a shallow clone or no history
55
- return [];
56
- }
57
-
58
- if (!diffOutput) return [];
59
-
60
- // Parse the diff output
61
- let currentFile = '';
62
- let currentCommit = '';
63
- let currentDate = '';
64
- const lines = diffOutput.split('\n');
65
-
66
- for (let i = 0; i < lines.length; i++) {
67
- const line = lines[i];
68
-
69
- // Track current commit
70
- if (line.startsWith('commit ')) {
71
- currentCommit = line.slice(7, 17); // First 10 chars of hash
72
- }
73
- if (line.startsWith('Date:')) {
74
- currentDate = line.slice(5).trim();
75
- }
76
-
77
- // Track current file
78
- if (line.startsWith('diff --git ')) {
79
- const match = line.match(/diff --git a\/(.+) b\//);
80
- if (match) currentFile = match[1];
81
- }
82
-
83
- // Only check added lines (lines starting with +)
84
- if (!line.startsWith('+') || line.startsWith('+++')) continue;
85
-
86
- const addedLine = line.slice(1); // Remove the leading +
87
-
88
- // Check against all secret patterns
89
- for (const p of FAST_SECRET_PATTERNS) {
90
- p.pattern.lastIndex = 0;
91
- const match = p.pattern.exec(addedLine);
92
- if (match) {
93
- // Check if this secret still exists in current working tree
94
- const stillExists = this.existsInWorkingTree(rootPath, match[0]);
95
-
96
- findings.push(createFinding({
97
- file: path.join(rootPath, currentFile),
98
- line: 0, // Line number not meaningful in history
99
- severity: stillExists ? p.severity : this.elevateSeverity(p.severity),
100
- category: 'history',
101
- rule: 'GIT_HISTORY_SECRET',
102
- title: `Historical Secret: ${p.name}`,
103
- description: stillExists
104
- ? `Secret found in current code AND in git history (commit ${currentCommit}).`
105
- : `Secret was removed from code but still exists in git history (commit ${currentCommit}, ${currentDate}). Anyone with repo access can retrieve it.`,
106
- matched: this.maskSecret(match[0]),
107
- confidence: 'high',
108
- fix: stillExists
109
- ? 'Remove from code, rotate the credential, then clean git history with BFG or git filter-repo'
110
- : 'Rotate this credential immediately, then clean history: npx bfg --replace-text passwords.txt',
111
- }));
112
- }
113
- }
114
- }
115
-
116
- // Deduplicate by matched value (same secret in multiple commits)
117
- const seen = new Set();
118
- return findings.filter(f => {
119
- const key = `${f.matched}:${f.title}`;
120
- if (seen.has(key)) return false;
121
- seen.add(key);
122
- return true;
123
- });
124
-
125
- } catch (err) {
126
- // Don't fail the entire scan if git history scan fails
127
- return [];
128
- }
129
- }
130
-
131
- isGitRepo(dir) {
132
- try {
133
- execSync('git rev-parse --is-inside-work-tree', { cwd: dir, stdio: 'pipe' });
134
- return true;
135
- } catch {
136
- return false;
137
- }
138
- }
139
-
140
- existsInWorkingTree(rootPath, secret) {
141
- try {
142
- const result = execFileSync('git', [
143
- '-C', rootPath, 'grep', '-l', secret.slice(0, 12),
144
- '--', '*.js', '*.ts', '*.py', '*.env', '*.json'
145
- ], {
146
- cwd: rootPath,
147
- encoding: 'utf-8',
148
- timeout: 5000,
149
- stdio: ['pipe', 'pipe', 'pipe'],
150
- });
151
- return result.trim().length > 0;
152
- } catch {
153
- return false;
154
- }
155
- }
156
-
157
- elevateSeverity(sev) {
158
- // Secrets in history-only are MORE dangerous (developer thinks they're gone)
159
- if (sev === 'medium') return 'high';
160
- if (sev === 'high') return 'critical';
161
- return sev;
162
- }
163
-
164
- maskSecret(secret) {
165
- if (secret.length <= 10) return secret.slice(0, 4) + '***';
166
- return secret.slice(0, 8) + '***' + secret.slice(-4);
167
- }
168
- }
169
-
170
- export default GitHistoryScanner;
1
+ /**
2
+ * GitHistoryScanner Agent
3
+ * ========================
4
+ *
5
+ * Scans git commit history for secrets that were committed
6
+ * and later removed but remain in repository history.
7
+ * These are the most dangerous secrets — developers think
8
+ * they're deleted but they're still accessible.
9
+ */
10
+
11
+ import { execSync, execFileSync } from 'child_process';
12
+ import path from 'path';
13
+ import { BaseAgent, createFinding } from './base-agent.js';
14
+ import { SECRET_PATTERNS } from '../utils/patterns.js';
15
+
16
+ // Compile a fast combined regex from all secret patterns
17
+ const FAST_SECRET_PATTERNS = SECRET_PATTERNS.map(p => ({
18
+ name: p.name,
19
+ pattern: p.pattern,
20
+ severity: p.severity,
21
+ }));
22
+
23
+ export class GitHistoryScanner extends BaseAgent {
24
+ constructor() {
25
+ super('GitHistoryScanner', 'Scan git history for leaked secrets', 'history');
26
+ }
27
+
28
+ async analyze(context) {
29
+ const { rootPath, options } = context;
30
+ const findings = [];
31
+
32
+ // Check if this is a git repository
33
+ if (!this.isGitRepo(rootPath)) return [];
34
+
35
+ try {
36
+ // Get recent commits (default: last 50, configurable)
37
+ const maxCommits = options?.maxCommits || 50;
38
+ const since = options?.since || null;
39
+
40
+ const gitArgs = ['-C', rootPath, 'log', '--all', '--diff-filter=A', '--diff-filter=M', '-p', '--no-color', `--max-count=${parseInt(maxCommits, 10)}`];
41
+ if (since) {
42
+ gitArgs.push(`--since=${since}`);
43
+ }
44
+
45
+ let diffOutput;
46
+ try {
47
+ diffOutput = execFileSync('git', gitArgs, {
48
+ cwd: rootPath,
49
+ encoding: 'utf-8',
50
+ maxBuffer: 50 * 1024 * 1024, // 50MB buffer
51
+ timeout: 60000, // 60s timeout
52
+ });
53
+ } catch {
54
+ // git log failed — might be a shallow clone or no history
55
+ return [];
56
+ }
57
+
58
+ if (!diffOutput) return [];
59
+
60
+ // Parse the diff output
61
+ let currentFile = '';
62
+ let currentCommit = '';
63
+ let currentDate = '';
64
+ const lines = diffOutput.split('\n');
65
+
66
+ for (let i = 0; i < lines.length; i++) {
67
+ const line = lines[i];
68
+
69
+ // Track current commit
70
+ if (line.startsWith('commit ')) {
71
+ currentCommit = line.slice(7, 17); // First 10 chars of hash
72
+ }
73
+ if (line.startsWith('Date:')) {
74
+ currentDate = line.slice(5).trim();
75
+ }
76
+
77
+ // Track current file
78
+ if (line.startsWith('diff --git ')) {
79
+ const match = line.match(/diff --git a\/(.+) b\//);
80
+ if (match) currentFile = match[1];
81
+ }
82
+
83
+ // Only check added lines (lines starting with +)
84
+ if (!line.startsWith('+') || line.startsWith('+++')) continue;
85
+
86
+ const addedLine = line.slice(1); // Remove the leading +
87
+
88
+ // Check against all secret patterns
89
+ for (const p of FAST_SECRET_PATTERNS) {
90
+ p.pattern.lastIndex = 0;
91
+ const match = p.pattern.exec(addedLine);
92
+ if (match) {
93
+ // Check if this secret still exists in current working tree
94
+ const stillExists = this.existsInWorkingTree(rootPath, match[0]);
95
+
96
+ findings.push(createFinding({
97
+ file: path.join(rootPath, currentFile),
98
+ line: 0, // Line number not meaningful in history
99
+ severity: stillExists ? p.severity : this.elevateSeverity(p.severity),
100
+ category: 'history',
101
+ rule: 'GIT_HISTORY_SECRET',
102
+ title: `Historical Secret: ${p.name}`,
103
+ description: stillExists
104
+ ? `Secret found in current code AND in git history (commit ${currentCommit}).`
105
+ : `Secret was removed from code but still exists in git history (commit ${currentCommit}, ${currentDate}). Anyone with repo access can retrieve it.`,
106
+ matched: this.maskSecret(match[0]),
107
+ confidence: 'high',
108
+ fix: stillExists
109
+ ? 'Remove from code, rotate the credential, then clean git history with BFG or git filter-repo'
110
+ : 'Rotate this credential immediately, then clean history: npx bfg --replace-text passwords.txt',
111
+ }));
112
+ }
113
+ }
114
+ }
115
+
116
+ // Deduplicate by matched value (same secret in multiple commits)
117
+ const seen = new Set();
118
+ return findings.filter(f => {
119
+ const key = `${f.matched}:${f.title}`;
120
+ if (seen.has(key)) return false;
121
+ seen.add(key);
122
+ return true;
123
+ });
124
+
125
+ } catch (err) {
126
+ // Don't fail the entire scan if git history scan fails
127
+ return [];
128
+ }
129
+ }
130
+
131
+ isGitRepo(dir) {
132
+ try {
133
+ execSync('git rev-parse --is-inside-work-tree', { cwd: dir, stdio: 'pipe' });
134
+ return true;
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
139
+
140
+ existsInWorkingTree(rootPath, secret) {
141
+ try {
142
+ const result = execFileSync('git', [
143
+ '-C', rootPath, 'grep', '-l', secret.slice(0, 12),
144
+ '--', '*.js', '*.ts', '*.py', '*.env', '*.json'
145
+ ], {
146
+ cwd: rootPath,
147
+ encoding: 'utf-8',
148
+ timeout: 5000,
149
+ stdio: ['pipe', 'pipe', 'pipe'],
150
+ });
151
+ return result.trim().length > 0;
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ elevateSeverity(sev) {
158
+ // Secrets in history-only are MORE dangerous (developer thinks they're gone)
159
+ if (sev === 'medium') return 'high';
160
+ if (sev === 'high') return 'critical';
161
+ return sev;
162
+ }
163
+
164
+ maskSecret(secret) {
165
+ if (secret.length <= 10) return secret.slice(0, 4) + '***';
166
+ return secret.slice(0, 8) + '***' + secret.slice(-4);
167
+ }
168
+ }
169
+
170
+ export default GitHistoryScanner;