ship-safe 6.1.1 → 6.3.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 (49) hide show
  1. package/README.md +748 -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 +85 -84
  11. package/cli/agents/injection-tester.js +500 -500
  12. package/cli/agents/legal-risk-agent.js +302 -0
  13. package/cli/agents/llm-redteam.js +251 -251
  14. package/cli/agents/mobile-scanner.js +231 -231
  15. package/cli/agents/orchestrator.js +322 -322
  16. package/cli/agents/pii-compliance-agent.js +301 -301
  17. package/cli/agents/scoring-engine.js +248 -248
  18. package/cli/agents/supabase-rls-agent.js +154 -154
  19. package/cli/agents/supply-chain-agent.js +650 -507
  20. package/cli/bin/ship-safe.js +464 -426
  21. package/cli/commands/agent.js +608 -608
  22. package/cli/commands/audit.js +1006 -980
  23. package/cli/commands/baseline.js +193 -193
  24. package/cli/commands/ci.js +342 -342
  25. package/cli/commands/deps.js +516 -516
  26. package/cli/commands/doctor.js +159 -159
  27. package/cli/commands/fix.js +218 -218
  28. package/cli/commands/hooks.js +268 -0
  29. package/cli/commands/init.js +407 -407
  30. package/cli/commands/legal.js +158 -0
  31. package/cli/commands/mcp.js +304 -304
  32. package/cli/commands/red-team.js +7 -1
  33. package/cli/commands/remediate.js +798 -798
  34. package/cli/commands/rotate.js +571 -571
  35. package/cli/commands/scan.js +569 -569
  36. package/cli/commands/score.js +449 -449
  37. package/cli/commands/watch.js +281 -281
  38. package/cli/hooks/patterns.js +313 -0
  39. package/cli/hooks/post-tool-use.js +140 -0
  40. package/cli/hooks/pre-tool-use.js +186 -0
  41. package/cli/index.js +73 -69
  42. package/cli/providers/llm-provider.js +397 -287
  43. package/cli/utils/autofix-rules.js +74 -74
  44. package/cli/utils/cache-manager.js +311 -311
  45. package/cli/utils/output.js +230 -230
  46. package/cli/utils/patterns.js +1121 -1121
  47. package/cli/utils/pdf-generator.js +94 -94
  48. package/package.json +69 -69
  49. 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;