ship-safe 4.2.0 → 4.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.
- package/README.md +65 -16
- package/cli/__tests__/agents.test.js +226 -0
- package/cli/agents/api-fuzzer.js +111 -0
- package/cli/agents/base-agent.js +262 -253
- package/cli/agents/config-auditor.js +71 -0
- package/cli/agents/html-reporter.js +370 -363
- package/cli/agents/index.js +59 -56
- package/cli/agents/orchestrator.js +44 -1
- package/cli/agents/supabase-rls-agent.js +148 -0
- package/cli/agents/supply-chain-agent.js +356 -274
- package/cli/bin/ship-safe.js +15 -1
- package/cli/commands/audit.js +27 -1
- package/cli/commands/baseline.js +192 -0
- package/cli/index.js +4 -0
- package/cli/utils/autofix-rules.js +74 -0
- package/cli/utils/pdf-generator.js +94 -0
- package/package.json +1 -1
package/cli/agents/base-agent.js
CHANGED
|
@@ -1,253 +1,262 @@
|
|
|
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, 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
|
-
// ── Helpers available to all agents ─────────────────────────────────────────
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Discover all scannable files in a directory.
|
|
93
|
-
* Respects SKIP_DIRS, SKIP_EXTENSIONS, and MAX_FILE_SIZE.
|
|
94
|
-
*/
|
|
95
|
-
async discoverFiles(rootPath, extraGlobs = ['**/*']) {
|
|
96
|
-
const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
|
|
97
|
-
|
|
98
|
-
// Respect .gitignore patterns
|
|
99
|
-
const gitignoreGlobs = loadGitignorePatterns(rootPath);
|
|
100
|
-
globIgnore.push(...gitignoreGlobs);
|
|
101
|
-
|
|
102
|
-
// Load .ship-safeignore patterns
|
|
103
|
-
const ignorePatterns = this._loadIgnorePatterns(rootPath);
|
|
104
|
-
for (const p of ignorePatterns) {
|
|
105
|
-
if (p.endsWith('/')) {
|
|
106
|
-
globIgnore.push(`**/${p}**`);
|
|
107
|
-
} else {
|
|
108
|
-
globIgnore.push(`**/${p}`);
|
|
109
|
-
globIgnore.push(p);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const allFiles = await fg(extraGlobs, {
|
|
114
|
-
cwd: rootPath,
|
|
115
|
-
absolute: true,
|
|
116
|
-
onlyFiles: true,
|
|
117
|
-
ignore: globIgnore,
|
|
118
|
-
dot: true,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
return allFiles.filter(file => {
|
|
122
|
-
const ext = path.extname(file).toLowerCase();
|
|
123
|
-
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
124
|
-
const basename = path.basename(file);
|
|
125
|
-
if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
|
|
126
|
-
try {
|
|
127
|
-
const stats = fs.statSync(file);
|
|
128
|
-
if (stats.size > MAX_FILE_SIZE) return false;
|
|
129
|
-
} catch {
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
return true;
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Load .ship-safeignore patterns from the project root.
|
|
138
|
-
*/
|
|
139
|
-
_loadIgnorePatterns(rootPath) {
|
|
140
|
-
const ignorePath = path.join(rootPath, '.ship-safeignore');
|
|
141
|
-
try {
|
|
142
|
-
if (!fs.existsSync(ignorePath)) return [];
|
|
143
|
-
return fs.readFileSync(ignorePath, 'utf-8')
|
|
144
|
-
.split('\n')
|
|
145
|
-
.map(l => l.trim())
|
|
146
|
-
.filter(l => l && !l.startsWith('#'));
|
|
147
|
-
} catch {
|
|
148
|
-
return [];
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Get the files this agent should scan.
|
|
154
|
-
* If incremental scanning is active (changedFiles in context), returns only changed files.
|
|
155
|
-
* Otherwise returns all files. Agents that need the full file list can use context.files directly.
|
|
156
|
-
*/
|
|
157
|
-
getFilesToScan(context) {
|
|
158
|
-
return context.changedFiles || context.files;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Read a file safely, returning null on failure.
|
|
163
|
-
*/
|
|
164
|
-
readFile(filePath) {
|
|
165
|
-
try {
|
|
166
|
-
return fs.readFileSync(filePath, 'utf-8');
|
|
167
|
-
} catch {
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Read a file and return its lines with line numbers.
|
|
174
|
-
*/
|
|
175
|
-
readLines(filePath) {
|
|
176
|
-
const content = this.readFile(filePath);
|
|
177
|
-
if (!content) return [];
|
|
178
|
-
return content.split('\n');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Get surrounding code context for a finding.
|
|
183
|
-
*/
|
|
184
|
-
getContext(filePath, lineNum, radius = 3) {
|
|
185
|
-
const lines = this.readLines(filePath);
|
|
186
|
-
if (lines.length === 0) return '';
|
|
187
|
-
const start = Math.max(0, lineNum - 1 - radius);
|
|
188
|
-
const end = Math.min(lines.length, lineNum + radius);
|
|
189
|
-
return lines.slice(start, end).join('\n');
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Check if a line has the ship-safe-ignore suppression comment.
|
|
194
|
-
*/
|
|
195
|
-
isSuppressed(line) {
|
|
196
|
-
return /ship-safe-ignore/i.test(line);
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Scan file lines against an array of regex patterns.
|
|
201
|
-
* Returns findings for every match.
|
|
202
|
-
*/
|
|
203
|
-
scanFileWithPatterns(filePath, patterns) {
|
|
204
|
-
const content = this.readFile(filePath);
|
|
205
|
-
if (!content) return [];
|
|
206
|
-
|
|
207
|
-
const lines = content.split('\n');
|
|
208
|
-
const findings = [];
|
|
209
|
-
|
|
210
|
-
for (let i = 0; i < lines.length; i++) {
|
|
211
|
-
const line = lines[i];
|
|
212
|
-
if (this.isSuppressed(line)) continue;
|
|
213
|
-
|
|
214
|
-
for (const p of patterns) {
|
|
215
|
-
p.regex.lastIndex = 0;
|
|
216
|
-
let match;
|
|
217
|
-
while ((match = p.regex.exec(line)) !== null) {
|
|
218
|
-
|
|
219
|
-
file: filePath,
|
|
220
|
-
line: i + 1,
|
|
221
|
-
column: match.index + 1,
|
|
222
|
-
severity: p.severity || 'medium',
|
|
223
|
-
category: this.category,
|
|
224
|
-
rule: p.rule,
|
|
225
|
-
title: p.title,
|
|
226
|
-
description: p.description,
|
|
227
|
-
matched: match[0],
|
|
228
|
-
confidence: p.confidence || 'high',
|
|
229
|
-
cwe: p.cwe || null,
|
|
230
|
-
owasp: p.owasp || null,
|
|
231
|
-
fix: p.fix || null,
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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, 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
|
+
// ── Helpers available to all agents ─────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Discover all scannable files in a directory.
|
|
93
|
+
* Respects SKIP_DIRS, SKIP_EXTENSIONS, and MAX_FILE_SIZE.
|
|
94
|
+
*/
|
|
95
|
+
async discoverFiles(rootPath, extraGlobs = ['**/*']) {
|
|
96
|
+
const globIgnore = Array.from(SKIP_DIRS).map(dir => `**/${dir}/**`);
|
|
97
|
+
|
|
98
|
+
// Respect .gitignore patterns
|
|
99
|
+
const gitignoreGlobs = loadGitignorePatterns(rootPath);
|
|
100
|
+
globIgnore.push(...gitignoreGlobs);
|
|
101
|
+
|
|
102
|
+
// Load .ship-safeignore patterns
|
|
103
|
+
const ignorePatterns = this._loadIgnorePatterns(rootPath);
|
|
104
|
+
for (const p of ignorePatterns) {
|
|
105
|
+
if (p.endsWith('/')) {
|
|
106
|
+
globIgnore.push(`**/${p}**`);
|
|
107
|
+
} else {
|
|
108
|
+
globIgnore.push(`**/${p}`);
|
|
109
|
+
globIgnore.push(p);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const allFiles = await fg(extraGlobs, {
|
|
114
|
+
cwd: rootPath,
|
|
115
|
+
absolute: true,
|
|
116
|
+
onlyFiles: true,
|
|
117
|
+
ignore: globIgnore,
|
|
118
|
+
dot: true,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return allFiles.filter(file => {
|
|
122
|
+
const ext = path.extname(file).toLowerCase();
|
|
123
|
+
if (SKIP_EXTENSIONS.has(ext)) return false;
|
|
124
|
+
const basename = path.basename(file);
|
|
125
|
+
if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
|
|
126
|
+
try {
|
|
127
|
+
const stats = fs.statSync(file);
|
|
128
|
+
if (stats.size > MAX_FILE_SIZE) return false;
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Load .ship-safeignore patterns from the project root.
|
|
138
|
+
*/
|
|
139
|
+
_loadIgnorePatterns(rootPath) {
|
|
140
|
+
const ignorePath = path.join(rootPath, '.ship-safeignore');
|
|
141
|
+
try {
|
|
142
|
+
if (!fs.existsSync(ignorePath)) return [];
|
|
143
|
+
return fs.readFileSync(ignorePath, 'utf-8')
|
|
144
|
+
.split('\n')
|
|
145
|
+
.map(l => l.trim())
|
|
146
|
+
.filter(l => l && !l.startsWith('#'));
|
|
147
|
+
} catch {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get the files this agent should scan.
|
|
154
|
+
* If incremental scanning is active (changedFiles in context), returns only changed files.
|
|
155
|
+
* Otherwise returns all files. Agents that need the full file list can use context.files directly.
|
|
156
|
+
*/
|
|
157
|
+
getFilesToScan(context) {
|
|
158
|
+
return context.changedFiles || context.files;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Read a file safely, returning null on failure.
|
|
163
|
+
*/
|
|
164
|
+
readFile(filePath) {
|
|
165
|
+
try {
|
|
166
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Read a file and return its lines with line numbers.
|
|
174
|
+
*/
|
|
175
|
+
readLines(filePath) {
|
|
176
|
+
const content = this.readFile(filePath);
|
|
177
|
+
if (!content) return [];
|
|
178
|
+
return content.split('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get surrounding code context for a finding.
|
|
183
|
+
*/
|
|
184
|
+
getContext(filePath, lineNum, radius = 3) {
|
|
185
|
+
const lines = this.readLines(filePath);
|
|
186
|
+
if (lines.length === 0) return '';
|
|
187
|
+
const start = Math.max(0, lineNum - 1 - radius);
|
|
188
|
+
const end = Math.min(lines.length, lineNum + radius);
|
|
189
|
+
return lines.slice(start, end).join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Check if a line has the ship-safe-ignore suppression comment.
|
|
194
|
+
*/
|
|
195
|
+
isSuppressed(line) {
|
|
196
|
+
return /ship-safe-ignore/i.test(line);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Scan file lines against an array of regex patterns.
|
|
201
|
+
* Returns findings for every match.
|
|
202
|
+
*/
|
|
203
|
+
scanFileWithPatterns(filePath, patterns) {
|
|
204
|
+
const content = this.readFile(filePath);
|
|
205
|
+
if (!content) return [];
|
|
206
|
+
|
|
207
|
+
const lines = content.split('\n');
|
|
208
|
+
const findings = [];
|
|
209
|
+
|
|
210
|
+
for (let i = 0; i < lines.length; i++) {
|
|
211
|
+
const line = lines[i];
|
|
212
|
+
if (this.isSuppressed(line)) continue;
|
|
213
|
+
|
|
214
|
+
for (const p of patterns) {
|
|
215
|
+
p.regex.lastIndex = 0;
|
|
216
|
+
let match;
|
|
217
|
+
while ((match = p.regex.exec(line)) !== null) {
|
|
218
|
+
const finding = createFinding({
|
|
219
|
+
file: filePath,
|
|
220
|
+
line: i + 1,
|
|
221
|
+
column: match.index + 1,
|
|
222
|
+
severity: p.severity || 'medium',
|
|
223
|
+
category: this.category,
|
|
224
|
+
rule: p.rule,
|
|
225
|
+
title: p.title,
|
|
226
|
+
description: p.description,
|
|
227
|
+
matched: match[0],
|
|
228
|
+
confidence: p.confidence || 'high',
|
|
229
|
+
cwe: p.cwe || null,
|
|
230
|
+
owasp: p.owasp || null,
|
|
231
|
+
fix: p.fix || null,
|
|
232
|
+
});
|
|
233
|
+
// Attach surrounding code context (3 lines before/after)
|
|
234
|
+
const start = Math.max(0, i - 3);
|
|
235
|
+
const end = Math.min(lines.length, i + 4);
|
|
236
|
+
finding.codeContext = lines.slice(start, end).map((l, idx) => ({
|
|
237
|
+
line: start + idx + 1,
|
|
238
|
+
text: l,
|
|
239
|
+
highlight: (start + idx) === i,
|
|
240
|
+
}));
|
|
241
|
+
findings.push(finding);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return findings;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Check if content imports or requires a specific module.
|
|
251
|
+
*/
|
|
252
|
+
hasImport(content, moduleName) {
|
|
253
|
+
const importRe = new RegExp(
|
|
254
|
+
`(?:import\\s+.*from\\s+['"]${moduleName}['"])|` +
|
|
255
|
+
`(?:require\\s*\\(\\s*['"]${moduleName}['"]\\s*\\))`,
|
|
256
|
+
'g'
|
|
257
|
+
);
|
|
258
|
+
return importRe.test(content);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export default BaseAgent;
|
|
@@ -199,6 +199,45 @@ const CONFIG_PATTERNS = [
|
|
|
199
199
|
fix: 'Set enabled = true and configure log destination',
|
|
200
200
|
},
|
|
201
201
|
|
|
202
|
+
// ── Terraform (expanded) ───────────────────────────────────────────────────
|
|
203
|
+
{
|
|
204
|
+
rule: 'TERRAFORM_RDS_PUBLIC',
|
|
205
|
+
title: 'Terraform: Publicly Accessible RDS',
|
|
206
|
+
regex: /publicly_accessible\s*=\s*true/g,
|
|
207
|
+
severity: 'critical',
|
|
208
|
+
cwe: 'CWE-284',
|
|
209
|
+
description: 'RDS instance is publicly accessible. Databases should not be exposed to the internet.',
|
|
210
|
+
fix: 'Set publicly_accessible = false and use VPC private subnets',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
rule: 'TERRAFORM_CLOUDFRONT_HTTP',
|
|
214
|
+
title: 'Terraform: CloudFront Allows HTTP',
|
|
215
|
+
regex: /viewer_protocol_policy\s*=\s*["']allow-all["']/g,
|
|
216
|
+
severity: 'high',
|
|
217
|
+
cwe: 'CWE-319',
|
|
218
|
+
description: 'CloudFront distribution allows HTTP traffic. Enforce HTTPS-only.',
|
|
219
|
+
fix: 'Set viewer_protocol_policy = "redirect-to-https"',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
rule: 'TERRAFORM_LAMBDA_ADMIN',
|
|
223
|
+
title: 'Terraform: Lambda with Admin Role',
|
|
224
|
+
regex: /(?:policy_arn|role)\s*=\s*.*AdministratorAccess/g,
|
|
225
|
+
severity: 'critical',
|
|
226
|
+
cwe: 'CWE-250',
|
|
227
|
+
description: 'Lambda function attached to AdministratorAccess policy. Apply least privilege.',
|
|
228
|
+
fix: 'Create a custom IAM policy with only the permissions the function needs',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
rule: 'TERRAFORM_S3_NO_VERSIONING',
|
|
232
|
+
title: 'Terraform: S3 Bucket Without Versioning',
|
|
233
|
+
regex: /versioning\s*\{[\s\S]*?enabled\s*=\s*false/g,
|
|
234
|
+
severity: 'medium',
|
|
235
|
+
cwe: 'CWE-693',
|
|
236
|
+
confidence: 'medium',
|
|
237
|
+
description: 'S3 bucket versioning disabled. Versioning protects against accidental deletion and ransomware.',
|
|
238
|
+
fix: 'Set enabled = true in the versioning block',
|
|
239
|
+
},
|
|
240
|
+
|
|
202
241
|
// ── Kubernetes ─────────────────────────────────────────────────────────────
|
|
203
242
|
{
|
|
204
243
|
rule: 'K8S_PRIVILEGED_CONTAINER',
|
|
@@ -247,6 +286,16 @@ const CONFIG_PATTERNS = [
|
|
|
247
286
|
fix: 'Create a dedicated ServiceAccount with only needed RBAC bindings',
|
|
248
287
|
},
|
|
249
288
|
|
|
289
|
+
{
|
|
290
|
+
rule: 'K8S_LATEST_IMAGE',
|
|
291
|
+
title: 'Kubernetes: Container Using :latest Tag',
|
|
292
|
+
regex: /image\s*:\s*["']?\S+:latest["']?/g,
|
|
293
|
+
severity: 'medium',
|
|
294
|
+
cwe: 'CWE-1104',
|
|
295
|
+
description: 'Container image uses :latest tag, which is mutable and can change unexpectedly.',
|
|
296
|
+
fix: 'Pin to a specific image digest or version tag',
|
|
297
|
+
},
|
|
298
|
+
|
|
250
299
|
// ── Docker Compose ─────────────────────────────────────────────────────────
|
|
251
300
|
{
|
|
252
301
|
rule: 'COMPOSE_HOST_MOUNT',
|
|
@@ -398,6 +447,28 @@ export class ConfigAuditor extends BaseAgent {
|
|
|
398
447
|
findings = findings.concat(this.scanFileWithPatterns(file, CONFIG_PATTERNS));
|
|
399
448
|
}
|
|
400
449
|
|
|
450
|
+
// ── Project-level: K8s NetworkPolicy check ─────────────────────────────────
|
|
451
|
+
if (k8sFiles.length > 0) {
|
|
452
|
+
const hasNetworkPolicy = k8sFiles.some(f => {
|
|
453
|
+
const content = this.readFile(f);
|
|
454
|
+
return content && /kind\s*:\s*NetworkPolicy/i.test(content);
|
|
455
|
+
});
|
|
456
|
+
if (!hasNetworkPolicy) {
|
|
457
|
+
findings.push(createFinding({
|
|
458
|
+
file: k8sFiles[0],
|
|
459
|
+
line: 0,
|
|
460
|
+
severity: 'medium',
|
|
461
|
+
category: 'config',
|
|
462
|
+
rule: 'K8S_NO_NETWORK_POLICY',
|
|
463
|
+
title: 'Kubernetes: No NetworkPolicy Defined',
|
|
464
|
+
description: 'Kubernetes manifests found but no NetworkPolicy defined. Without network policies, all pod-to-pod traffic is allowed.',
|
|
465
|
+
matched: 'Missing NetworkPolicy',
|
|
466
|
+
confidence: 'medium',
|
|
467
|
+
fix: 'Add a NetworkPolicy resource to restrict pod-to-pod traffic',
|
|
468
|
+
}));
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
401
472
|
// ── Scan config files ─────────────────────────────────────────────────────
|
|
402
473
|
const configFiles = files.filter(f => {
|
|
403
474
|
const basename = path.basename(f);
|