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
@@ -1,272 +1,272 @@
1
- /**
2
- * BaseAgent — Foundation for all security scanning agents
3
- * ========================================================
4
- *
5
- * Every agent in ship-safe extends BaseAgent. It provides:
6
- * - Standard finding format
7
- * - File discovery with skip-list support
8
- * - Severity classification
9
- * - Consistent output interface
10
- *
11
- * USAGE:
12
- * class MyAgent extends BaseAgent {
13
- * constructor() { super('MyAgent', 'Description', 'category'); }
14
- * async analyze(context) { return [findings]; }
15
- * }
16
- */
17
-
18
- import fs from 'fs';
19
- import path from 'path';
20
- import fg from 'fast-glob';
21
- import { SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, MAX_FILE_SIZE, loadGitignorePatterns } from '../utils/patterns.js';
22
-
23
- // =============================================================================
24
- // FINDING FACTORY
25
- // =============================================================================
26
-
27
- /**
28
- * Create a standardized finding object.
29
- */
30
- export function createFinding({
31
- file,
32
- line = 0,
33
- column = 0,
34
- severity = 'medium',
35
- category = 'vulnerability',
36
- rule,
37
- title,
38
- description,
39
- matched = '',
40
- confidence = 'high',
41
- cwe = null,
42
- owasp = null,
43
- fix = null,
44
- }) {
45
- return {
46
- file,
47
- line,
48
- column,
49
- severity,
50
- category,
51
- rule,
52
- title,
53
- description,
54
- matched,
55
- confidence,
56
- cwe,
57
- owasp,
58
- fix,
59
- };
60
- }
61
-
62
- // =============================================================================
63
- // BASE AGENT CLASS
64
- // =============================================================================
65
-
66
- export class BaseAgent {
67
- /**
68
- * @param {string} name — Agent name (e.g. 'InjectionTester')
69
- * @param {string} description — What this agent does
70
- * @param {string} category — Finding category for scoring
71
- */
72
- constructor(name, description, category) {
73
- this.name = name;
74
- this.description = description;
75
- this.category = category;
76
- }
77
-
78
- /**
79
- * Run the agent's analysis on a codebase.
80
- * Subclasses MUST override this method.
81
- *
82
- * @param {object} context — { rootPath, files, recon, options }
83
- * @returns {Promise<object[]>} — Array of finding objects
84
- */
85
- async analyze(context) {
86
- throw new Error(`${this.name}.analyze() not implemented`);
87
- }
88
-
89
- /**
90
- * Whether this agent should run given the recon results.
91
- * Override in subclasses to skip irrelevant scans.
92
- * Default: always run.
93
- */
94
- shouldRun(recon) {
95
- return true;
96
- }
97
-
98
- // ── Helpers available to all agents ─────────────────────────────────────────
99
-
100
- /**
101
- * Discover all scannable files in a directory.
102
- * Respects SKIP_DIRS, SKIP_EXTENSIONS, and MAX_FILE_SIZE.
103
- */
104
- async discoverFiles(rootPath, extraGlobs = ['**/*']) {
105
- const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
106
-
107
- // Respect .gitignore patterns
108
- const gitignoreGlobs = loadGitignorePatterns(rootPath);
109
- globIgnore.push(...gitignoreGlobs);
110
-
111
- // Load .ship-safeignore patterns
112
- const ignorePatterns = this._loadIgnorePatterns(rootPath);
113
- for (const p of ignorePatterns) {
114
- if (p.endsWith('/')) {
115
- globIgnore.push(`**/${p}**`);
116
- } else {
117
- globIgnore.push(`**/${p}`);
118
- globIgnore.push(p);
119
- }
120
- }
121
-
122
- const allFiles = await fg(extraGlobs, {
123
- cwd: rootPath,
124
- absolute: true,
125
- onlyFiles: true,
126
- ignore: globIgnore,
127
- dot: true,
128
- });
129
-
130
- return allFiles.filter(file => {
131
- const ext = path.extname(file).toLowerCase();
132
- if (SKIP_EXTENSIONS.has(ext)) return false;
133
- const basename = path.basename(file);
134
- if (SKIP_FILENAMES.has(basename)) return false;
135
- if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
136
- try {
137
- const stats = fs.statSync(file);
138
- if (stats.size > MAX_FILE_SIZE) return false;
139
- } catch {
140
- return false;
141
- }
142
- return true;
143
- });
144
- }
145
-
146
- /**
147
- * Load .ship-safeignore patterns from the project root.
148
- */
149
- _loadIgnorePatterns(rootPath) {
150
- const ignorePath = path.join(rootPath, '.ship-safeignore');
151
- try {
152
- if (!fs.existsSync(ignorePath)) return [];
153
- return fs.readFileSync(ignorePath, 'utf-8')
154
- .split('\n')
155
- .map(l => l.trim())
156
- .filter(l => l && !l.startsWith('#'));
157
- } catch {
158
- return [];
159
- }
160
- }
161
-
162
- /**
163
- * Get the files this agent should scan.
164
- * If incremental scanning is active (changedFiles in context), returns only changed files.
165
- * Otherwise returns all files. Agents that need the full file list can use context.files directly.
166
- */
167
- getFilesToScan(context) {
168
- return context.changedFiles || context.files;
169
- }
170
-
171
- /**
172
- * Read a file safely, returning null on failure.
173
- */
174
- readFile(filePath) {
175
- try {
176
- return fs.readFileSync(filePath, 'utf-8');
177
- } catch {
178
- return null;
179
- }
180
- }
181
-
182
- /**
183
- * Read a file and return its lines with line numbers.
184
- */
185
- readLines(filePath) {
186
- const content = this.readFile(filePath);
187
- if (!content) return [];
188
- return content.split('\n');
189
- }
190
-
191
- /**
192
- * Get surrounding code context for a finding.
193
- */
194
- getContext(filePath, lineNum, radius = 3) {
195
- const lines = this.readLines(filePath);
196
- if (lines.length === 0) return '';
197
- const start = Math.max(0, lineNum - 1 - radius);
198
- const end = Math.min(lines.length, lineNum + radius);
199
- return lines.slice(start, end).join('\n');
200
- }
201
-
202
- /**
203
- * Check if a line has the ship-safe-ignore suppression comment.
204
- */
205
- isSuppressed(line) {
206
- return /ship-safe-ignore/i.test(line);
207
- }
208
-
209
- /**
210
- * Scan file lines against an array of regex patterns.
211
- * Returns findings for every match.
212
- */
213
- scanFileWithPatterns(filePath, patterns) {
214
- const content = this.readFile(filePath);
215
- if (!content) return [];
216
-
217
- const lines = content.split('\n');
218
- const findings = [];
219
-
220
- for (let i = 0; i < lines.length; i++) {
221
- const line = lines[i];
222
- if (this.isSuppressed(line)) continue;
223
-
224
- for (const p of patterns) {
225
- p.regex.lastIndex = 0;
226
- let match;
227
- while ((match = p.regex.exec(line)) !== null) {
228
- const finding = createFinding({
229
- file: filePath,
230
- line: i + 1,
231
- column: match.index + 1,
232
- severity: p.severity || 'medium',
233
- category: this.category,
234
- rule: p.rule,
235
- title: p.title,
236
- description: p.description,
237
- matched: match[0],
238
- confidence: p.confidence || 'high',
239
- cwe: p.cwe || null,
240
- owasp: p.owasp || null,
241
- fix: p.fix || null,
242
- });
243
- // Attach surrounding code context (3 lines before/after)
244
- const start = Math.max(0, i - 3);
245
- const end = Math.min(lines.length, i + 4);
246
- finding.codeContext = lines.slice(start, end).map((l, idx) => ({
247
- line: start + idx + 1,
248
- text: l,
249
- highlight: (start + idx) === i,
250
- }));
251
- findings.push(finding);
252
- }
253
- }
254
- }
255
-
256
- return findings;
257
- }
258
-
259
- /**
260
- * Check if content imports or requires a specific module.
261
- */
262
- hasImport(content, moduleName) {
263
- const importRe = new RegExp(
264
- `(?:import\\s+.*from\\s+['"]${moduleName}['"])|` +
265
- `(?:require\\s*\\(\\s*['"]${moduleName}['"]\\s*\\))`,
266
- 'g'
267
- );
268
- return importRe.test(content);
269
- }
270
- }
271
-
272
- export default BaseAgent;
1
+ /**
2
+ * BaseAgent — Foundation for all security scanning agents
3
+ * ========================================================
4
+ *
5
+ * Every agent in ship-safe extends BaseAgent. It provides:
6
+ * - Standard finding format
7
+ * - File discovery with skip-list support
8
+ * - Severity classification
9
+ * - Consistent output interface
10
+ *
11
+ * USAGE:
12
+ * class MyAgent extends BaseAgent {
13
+ * constructor() { super('MyAgent', 'Description', 'category'); }
14
+ * async analyze(context) { return [findings]; }
15
+ * }
16
+ */
17
+
18
+ import fs from 'fs';
19
+ import path from 'path';
20
+ import fg from 'fast-glob';
21
+ import { SKIP_DIRS, SKIP_EXTENSIONS, SKIP_FILENAMES, MAX_FILE_SIZE, loadGitignorePatterns } from '../utils/patterns.js';
22
+
23
+ // =============================================================================
24
+ // FINDING FACTORY
25
+ // =============================================================================
26
+
27
+ /**
28
+ * Create a standardized finding object.
29
+ */
30
+ export function createFinding({
31
+ file,
32
+ line = 0,
33
+ column = 0,
34
+ severity = 'medium',
35
+ category = 'vulnerability',
36
+ rule,
37
+ title,
38
+ description,
39
+ matched = '',
40
+ confidence = 'high',
41
+ cwe = null,
42
+ owasp = null,
43
+ fix = null,
44
+ }) {
45
+ return {
46
+ file,
47
+ line,
48
+ column,
49
+ severity,
50
+ category,
51
+ rule,
52
+ title,
53
+ description,
54
+ matched,
55
+ confidence,
56
+ cwe,
57
+ owasp,
58
+ fix,
59
+ };
60
+ }
61
+
62
+ // =============================================================================
63
+ // BASE AGENT CLASS
64
+ // =============================================================================
65
+
66
+ export class BaseAgent {
67
+ /**
68
+ * @param {string} name — Agent name (e.g. 'InjectionTester')
69
+ * @param {string} description — What this agent does
70
+ * @param {string} category — Finding category for scoring
71
+ */
72
+ constructor(name, description, category) {
73
+ this.name = name;
74
+ this.description = description;
75
+ this.category = category;
76
+ }
77
+
78
+ /**
79
+ * Run the agent's analysis on a codebase.
80
+ * Subclasses MUST override this method.
81
+ *
82
+ * @param {object} context — { rootPath, files, recon, options }
83
+ * @returns {Promise<object[]>} — Array of finding objects
84
+ */
85
+ async analyze(context) {
86
+ throw new Error(`${this.name}.analyze() not implemented`);
87
+ }
88
+
89
+ /**
90
+ * Whether this agent should run given the recon results.
91
+ * Override in subclasses to skip irrelevant scans.
92
+ * Default: always run.
93
+ */
94
+ shouldRun(recon) {
95
+ return true;
96
+ }
97
+
98
+ // ── Helpers available to all agents ─────────────────────────────────────────
99
+
100
+ /**
101
+ * Discover all scannable files in a directory.
102
+ * Respects SKIP_DIRS, SKIP_EXTENSIONS, and MAX_FILE_SIZE.
103
+ */
104
+ async discoverFiles(rootPath, extraGlobs = ['**/*']) {
105
+ const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
106
+
107
+ // Respect .gitignore patterns
108
+ const gitignoreGlobs = loadGitignorePatterns(rootPath);
109
+ globIgnore.push(...gitignoreGlobs);
110
+
111
+ // Load .ship-safeignore patterns
112
+ const ignorePatterns = this._loadIgnorePatterns(rootPath);
113
+ for (const p of ignorePatterns) {
114
+ if (p.endsWith('/')) {
115
+ globIgnore.push(`**/${p}**`);
116
+ } else {
117
+ globIgnore.push(`**/${p}`);
118
+ globIgnore.push(p);
119
+ }
120
+ }
121
+
122
+ const allFiles = await fg(extraGlobs, {
123
+ cwd: rootPath,
124
+ absolute: true,
125
+ onlyFiles: true,
126
+ ignore: globIgnore,
127
+ dot: true,
128
+ });
129
+
130
+ return allFiles.filter(file => {
131
+ const ext = path.extname(file).toLowerCase();
132
+ if (SKIP_EXTENSIONS.has(ext)) return false;
133
+ const basename = path.basename(file);
134
+ if (SKIP_FILENAMES.has(basename)) return false;
135
+ if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
136
+ try {
137
+ const stats = fs.statSync(file);
138
+ if (stats.size > MAX_FILE_SIZE) return false;
139
+ } catch {
140
+ return false;
141
+ }
142
+ return true;
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Load .ship-safeignore patterns from the project root.
148
+ */
149
+ _loadIgnorePatterns(rootPath) {
150
+ const ignorePath = path.join(rootPath, '.ship-safeignore');
151
+ try {
152
+ if (!fs.existsSync(ignorePath)) return [];
153
+ return fs.readFileSync(ignorePath, 'utf-8')
154
+ .split('\n')
155
+ .map(l => l.trim())
156
+ .filter(l => l && !l.startsWith('#'));
157
+ } catch {
158
+ return [];
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Get the files this agent should scan.
164
+ * If incremental scanning is active (changedFiles in context), returns only changed files.
165
+ * Otherwise returns all files. Agents that need the full file list can use context.files directly.
166
+ */
167
+ getFilesToScan(context) {
168
+ return context.changedFiles || context.files;
169
+ }
170
+
171
+ /**
172
+ * Read a file safely, returning null on failure.
173
+ */
174
+ readFile(filePath) {
175
+ try {
176
+ return fs.readFileSync(filePath, 'utf-8');
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Read a file and return its lines with line numbers.
184
+ */
185
+ readLines(filePath) {
186
+ const content = this.readFile(filePath);
187
+ if (!content) return [];
188
+ return content.split('\n');
189
+ }
190
+
191
+ /**
192
+ * Get surrounding code context for a finding.
193
+ */
194
+ getContext(filePath, lineNum, radius = 3) {
195
+ const lines = this.readLines(filePath);
196
+ if (lines.length === 0) return '';
197
+ const start = Math.max(0, lineNum - 1 - radius);
198
+ const end = Math.min(lines.length, lineNum + radius);
199
+ return lines.slice(start, end).join('\n');
200
+ }
201
+
202
+ /**
203
+ * Check if a line has the ship-safe-ignore suppression comment.
204
+ */
205
+ isSuppressed(line) {
206
+ return /ship-safe-ignore/i.test(line);
207
+ }
208
+
209
+ /**
210
+ * Scan file lines against an array of regex patterns.
211
+ * Returns findings for every match.
212
+ */
213
+ scanFileWithPatterns(filePath, patterns) {
214
+ const content = this.readFile(filePath);
215
+ if (!content) return [];
216
+
217
+ const lines = content.split('\n');
218
+ const findings = [];
219
+
220
+ for (let i = 0; i < lines.length; i++) {
221
+ const line = lines[i];
222
+ if (this.isSuppressed(line)) continue;
223
+
224
+ for (const p of patterns) {
225
+ p.regex.lastIndex = 0;
226
+ let match;
227
+ while ((match = p.regex.exec(line)) !== null) {
228
+ const finding = createFinding({
229
+ file: filePath,
230
+ line: i + 1,
231
+ column: match.index + 1,
232
+ severity: p.severity || 'medium',
233
+ category: this.category,
234
+ rule: p.rule,
235
+ title: p.title,
236
+ description: p.description,
237
+ matched: match[0],
238
+ confidence: p.confidence || 'high',
239
+ cwe: p.cwe || null,
240
+ owasp: p.owasp || null,
241
+ fix: p.fix || null,
242
+ });
243
+ // Attach surrounding code context (3 lines before/after)
244
+ const start = Math.max(0, i - 3);
245
+ const end = Math.min(lines.length, i + 4);
246
+ finding.codeContext = lines.slice(start, end).map((l, idx) => ({
247
+ line: start + idx + 1,
248
+ text: l,
249
+ highlight: (start + idx) === i,
250
+ }));
251
+ findings.push(finding);
252
+ }
253
+ }
254
+ }
255
+
256
+ return findings;
257
+ }
258
+
259
+ /**
260
+ * Check if content imports or requires a specific module.
261
+ */
262
+ hasImport(content, moduleName) {
263
+ const importRe = new RegExp(
264
+ `(?:import\\s+.*from\\s+['"]${moduleName}['"])|` +
265
+ `(?:require\\s*\\(\\s*['"]${moduleName}['"]\\s*\\))`,
266
+ 'g'
267
+ );
268
+ return importRe.test(content);
269
+ }
270
+ }
271
+
272
+ export default BaseAgent;