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.
- package/README.md +735 -641
- package/cli/agents/api-fuzzer.js +345 -345
- package/cli/agents/auth-bypass-agent.js +348 -348
- package/cli/agents/base-agent.js +272 -272
- package/cli/agents/cicd-scanner.js +236 -201
- package/cli/agents/config-auditor.js +521 -521
- package/cli/agents/deep-analyzer.js +6 -2
- package/cli/agents/git-history-scanner.js +170 -170
- package/cli/agents/html-reporter.js +568 -568
- package/cli/agents/index.js +84 -84
- package/cli/agents/injection-tester.js +500 -500
- package/cli/agents/llm-redteam.js +251 -251
- package/cli/agents/mobile-scanner.js +231 -231
- package/cli/agents/orchestrator.js +322 -322
- package/cli/agents/pii-compliance-agent.js +301 -301
- package/cli/agents/scoring-engine.js +248 -248
- package/cli/agents/supabase-rls-agent.js +154 -154
- package/cli/agents/supply-chain-agent.js +650 -507
- package/cli/bin/ship-safe.js +452 -426
- package/cli/commands/agent.js +608 -608
- package/cli/commands/audit.js +986 -980
- package/cli/commands/baseline.js +193 -193
- package/cli/commands/ci.js +342 -342
- package/cli/commands/deps.js +516 -516
- package/cli/commands/doctor.js +159 -159
- package/cli/commands/fix.js +218 -218
- package/cli/commands/hooks.js +268 -0
- package/cli/commands/init.js +407 -407
- package/cli/commands/mcp.js +304 -304
- package/cli/commands/red-team.js +7 -1
- package/cli/commands/remediate.js +798 -798
- package/cli/commands/rotate.js +571 -571
- package/cli/commands/scan.js +569 -569
- package/cli/commands/score.js +449 -449
- package/cli/commands/watch.js +281 -281
- package/cli/hooks/patterns.js +313 -0
- package/cli/hooks/post-tool-use.js +140 -0
- package/cli/hooks/pre-tool-use.js +186 -0
- package/cli/index.js +73 -69
- package/cli/providers/llm-provider.js +397 -287
- package/cli/utils/autofix-rules.js +74 -74
- package/cli/utils/cache-manager.js +311 -311
- package/cli/utils/output.js +230 -230
- package/cli/utils/patterns.js +1121 -1121
- package/cli/utils/pdf-generator.js +94 -94
- package/package.json +69 -69
- package/configs/supabase/rls-templates.sql +0 -242
package/cli/agents/base-agent.js
CHANGED
|
@@ -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;
|