ferret-scan 1.0.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/CHANGELOG.md +51 -0
- package/LICENSE +21 -0
- package/README.md +416 -0
- package/bin/ferret.js +822 -0
- package/dist/__tests__/basic.test.d.ts +6 -0
- package/dist/__tests__/basic.test.js +80 -0
- package/dist/analyzers/AstAnalyzer.d.ts +30 -0
- package/dist/analyzers/AstAnalyzer.js +332 -0
- package/dist/analyzers/CorrelationAnalyzer.d.ts +21 -0
- package/dist/analyzers/CorrelationAnalyzer.js +288 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +22 -0
- package/dist/intelligence/IndicatorMatcher.d.ts +50 -0
- package/dist/intelligence/IndicatorMatcher.js +285 -0
- package/dist/intelligence/ThreatFeed.d.ts +99 -0
- package/dist/intelligence/ThreatFeed.js +296 -0
- package/dist/remediation/Fixer.d.ts +71 -0
- package/dist/remediation/Fixer.js +391 -0
- package/dist/remediation/Quarantine.d.ts +102 -0
- package/dist/remediation/Quarantine.js +329 -0
- package/dist/reporters/ConsoleReporter.d.ts +13 -0
- package/dist/reporters/ConsoleReporter.js +185 -0
- package/dist/reporters/HtmlReporter.d.ts +25 -0
- package/dist/reporters/HtmlReporter.js +604 -0
- package/dist/reporters/SarifReporter.d.ts +86 -0
- package/dist/reporters/SarifReporter.js +117 -0
- package/dist/rules/ai-specific.d.ts +8 -0
- package/dist/rules/ai-specific.js +221 -0
- package/dist/rules/backdoors.d.ts +8 -0
- package/dist/rules/backdoors.js +134 -0
- package/dist/rules/correlationRules.d.ts +8 -0
- package/dist/rules/correlationRules.js +227 -0
- package/dist/rules/credentials.d.ts +8 -0
- package/dist/rules/credentials.js +194 -0
- package/dist/rules/exfiltration.d.ts +8 -0
- package/dist/rules/exfiltration.js +139 -0
- package/dist/rules/index.d.ts +51 -0
- package/dist/rules/index.js +97 -0
- package/dist/rules/injection.d.ts +8 -0
- package/dist/rules/injection.js +136 -0
- package/dist/rules/obfuscation.d.ts +8 -0
- package/dist/rules/obfuscation.js +159 -0
- package/dist/rules/permissions.d.ts +8 -0
- package/dist/rules/permissions.js +129 -0
- package/dist/rules/persistence.d.ts +8 -0
- package/dist/rules/persistence.js +117 -0
- package/dist/rules/semanticRules.d.ts +10 -0
- package/dist/rules/semanticRules.js +212 -0
- package/dist/rules/supply-chain.d.ts +8 -0
- package/dist/rules/supply-chain.js +148 -0
- package/dist/scanner/FileDiscovery.d.ts +24 -0
- package/dist/scanner/FileDiscovery.js +282 -0
- package/dist/scanner/PatternMatcher.d.ts +25 -0
- package/dist/scanner/PatternMatcher.js +206 -0
- package/dist/scanner/Scanner.d.ts +14 -0
- package/dist/scanner/Scanner.js +266 -0
- package/dist/scanner/WatchMode.d.ts +29 -0
- package/dist/scanner/WatchMode.js +195 -0
- package/dist/types.d.ts +332 -0
- package/dist/types.js +53 -0
- package/dist/utils/baseline.d.ts +80 -0
- package/dist/utils/baseline.js +276 -0
- package/dist/utils/config.d.ts +21 -0
- package/dist/utils/config.js +247 -0
- package/dist/utils/ignore.d.ts +18 -0
- package/dist/utils/ignore.js +82 -0
- package/dist/utils/logger.d.ts +32 -0
- package/dist/utils/logger.js +75 -0
- package/package.json +119 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quarantine System - Safely isolate suspicious files and content
|
|
3
|
+
* Provides reversible quarantine operations with audit trails
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, copyFileSync, unlinkSync, statSync } from 'node:fs';
|
|
6
|
+
import { resolve, dirname, basename } from 'node:path';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import logger from '../utils/logger.js';
|
|
9
|
+
/**
|
|
10
|
+
* Default quarantine options
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_OPTIONS = {
|
|
13
|
+
quarantineDir: '.ferret-quarantine',
|
|
14
|
+
createBackup: true,
|
|
15
|
+
removeOriginal: false, // By default, keep originals for safety
|
|
16
|
+
compressFiles: true,
|
|
17
|
+
maxFileSizeMB: 100
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Generate quarantine ID
|
|
21
|
+
*/
|
|
22
|
+
function generateQuarantineId() {
|
|
23
|
+
const timestamp = Date.now().toString(36);
|
|
24
|
+
const random = Math.random().toString(36).substring(2, 7);
|
|
25
|
+
return `quar-${timestamp}-${random}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Calculate file hash
|
|
29
|
+
*/
|
|
30
|
+
function calculateFileHash(filePath) {
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(filePath);
|
|
33
|
+
return createHash('sha256').update(content).digest('hex');
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
logger.warn(`Could not calculate hash for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
37
|
+
return 'unknown';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Load quarantine database
|
|
42
|
+
*/
|
|
43
|
+
export function loadQuarantineDatabase(quarantineDir) {
|
|
44
|
+
const dbPath = resolve(quarantineDir, 'quarantine.json');
|
|
45
|
+
if (!existsSync(dbPath)) {
|
|
46
|
+
return createEmptyDatabase();
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const content = readFileSync(dbPath, 'utf-8');
|
|
50
|
+
const db = JSON.parse(content);
|
|
51
|
+
// Validate database structure
|
|
52
|
+
if (!db.version || !db.entries || !Array.isArray(db.entries)) {
|
|
53
|
+
logger.warn('Invalid quarantine database, creating new one');
|
|
54
|
+
return createEmptyDatabase();
|
|
55
|
+
}
|
|
56
|
+
return db;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
logger.error(`Failed to load quarantine database: ${error instanceof Error ? error.message : String(error)}`);
|
|
60
|
+
return createEmptyDatabase();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Save quarantine database
|
|
65
|
+
*/
|
|
66
|
+
export function saveQuarantineDatabase(db, quarantineDir) {
|
|
67
|
+
try {
|
|
68
|
+
// Ensure directory exists
|
|
69
|
+
mkdirSync(quarantineDir, { recursive: true });
|
|
70
|
+
// Update stats and metadata
|
|
71
|
+
db.lastUpdated = new Date().toISOString();
|
|
72
|
+
db.stats = calculateQuarantineStats(db.entries);
|
|
73
|
+
const dbPath = resolve(quarantineDir, 'quarantine.json');
|
|
74
|
+
const content = JSON.stringify(db, null, 2);
|
|
75
|
+
writeFileSync(dbPath, content, 'utf-8');
|
|
76
|
+
logger.debug(`Saved quarantine database with ${db.entries.length} entries`);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
logger.error(`Failed to save quarantine database: ${error instanceof Error ? error.message : String(error)}`);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Create empty quarantine database
|
|
85
|
+
*/
|
|
86
|
+
function createEmptyDatabase() {
|
|
87
|
+
return {
|
|
88
|
+
version: '1.0',
|
|
89
|
+
created: new Date().toISOString(),
|
|
90
|
+
lastUpdated: new Date().toISOString(),
|
|
91
|
+
entries: [],
|
|
92
|
+
stats: {
|
|
93
|
+
totalQuarantined: 0,
|
|
94
|
+
totalRestored: 0,
|
|
95
|
+
byCategory: {},
|
|
96
|
+
bySeverity: {}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Calculate quarantine statistics
|
|
102
|
+
*/
|
|
103
|
+
function calculateQuarantineStats(entries) {
|
|
104
|
+
const stats = {
|
|
105
|
+
totalQuarantined: entries.length,
|
|
106
|
+
totalRestored: entries.filter(e => e.restored).length,
|
|
107
|
+
byCategory: {},
|
|
108
|
+
bySeverity: {}
|
|
109
|
+
};
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
// Count by category
|
|
112
|
+
stats.byCategory[entry.metadata.category] = (stats.byCategory[entry.metadata.category] ?? 0) + 1;
|
|
113
|
+
// Count by severity
|
|
114
|
+
stats.bySeverity[entry.metadata.severity] = (stats.bySeverity[entry.metadata.severity] ?? 0) + 1;
|
|
115
|
+
}
|
|
116
|
+
return stats;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Quarantine a file based on findings
|
|
120
|
+
*/
|
|
121
|
+
export function quarantineFile(filePath, findings, reason, options = {}) {
|
|
122
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
123
|
+
try {
|
|
124
|
+
// Check if file exists
|
|
125
|
+
if (!existsSync(filePath)) {
|
|
126
|
+
logger.error(`File not found for quarantine: ${filePath}`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
// Check file size
|
|
130
|
+
const stats = statSync(filePath);
|
|
131
|
+
const fileSizeMB = stats.size / (1024 * 1024);
|
|
132
|
+
if (fileSizeMB > config.maxFileSizeMB) {
|
|
133
|
+
logger.warn(`File too large for quarantine: ${filePath} (${fileSizeMB.toFixed(1)}MB)`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
// Generate quarantine entry
|
|
137
|
+
const id = generateQuarantineId();
|
|
138
|
+
const fileName = basename(filePath);
|
|
139
|
+
const quarantineFileName = `${id}_${fileName}`;
|
|
140
|
+
const quarantinePath = resolve(config.quarantineDir, 'files', quarantineFileName);
|
|
141
|
+
// Ensure quarantine directory exists
|
|
142
|
+
mkdirSync(dirname(quarantinePath), { recursive: true });
|
|
143
|
+
// Copy file to quarantine
|
|
144
|
+
copyFileSync(filePath, quarantinePath);
|
|
145
|
+
// Calculate metadata
|
|
146
|
+
const fileHash = calculateFileHash(filePath);
|
|
147
|
+
const maxRiskScore = Math.max(...findings.map(f => f.riskScore));
|
|
148
|
+
const severities = findings.map(f => f.severity);
|
|
149
|
+
const severityOrder = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'];
|
|
150
|
+
const highestSeverity = severityOrder.find(s => severities.includes(s)) ?? 'INFO';
|
|
151
|
+
const entry = {
|
|
152
|
+
id,
|
|
153
|
+
originalPath: filePath,
|
|
154
|
+
quarantinePath,
|
|
155
|
+
reason,
|
|
156
|
+
findings,
|
|
157
|
+
quarantineDate: new Date().toISOString(),
|
|
158
|
+
fileSize: stats.size,
|
|
159
|
+
fileHash,
|
|
160
|
+
restored: false,
|
|
161
|
+
metadata: {
|
|
162
|
+
riskScore: maxRiskScore,
|
|
163
|
+
severity: highestSeverity,
|
|
164
|
+
category: findings[0]?.category ?? 'unknown'
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
// Update quarantine database
|
|
168
|
+
const db = loadQuarantineDatabase(config.quarantineDir);
|
|
169
|
+
db.entries.push(entry);
|
|
170
|
+
saveQuarantineDatabase(db, config.quarantineDir);
|
|
171
|
+
// Optionally remove original file
|
|
172
|
+
if (config.removeOriginal) {
|
|
173
|
+
try {
|
|
174
|
+
unlinkSync(filePath);
|
|
175
|
+
logger.info(`Quarantined and removed: ${filePath}`);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
logger.warn(`Could not remove original file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
logger.info(`Quarantined (original preserved): ${filePath}`);
|
|
183
|
+
}
|
|
184
|
+
return entry;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
logger.error(`Error quarantining file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Restore a quarantined file
|
|
193
|
+
*/
|
|
194
|
+
export function restoreQuarantinedFile(entryId, quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
|
|
195
|
+
try {
|
|
196
|
+
const db = loadQuarantineDatabase(quarantineDir);
|
|
197
|
+
const entry = db.entries.find(e => e.id === entryId);
|
|
198
|
+
if (!entry) {
|
|
199
|
+
logger.error(`Quarantine entry not found: ${entryId}`);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
if (entry.restored) {
|
|
203
|
+
logger.warn(`File already restored: ${entryId}`);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
if (!existsSync(entry.quarantinePath)) {
|
|
207
|
+
logger.error(`Quarantined file not found: ${entry.quarantinePath}`);
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
// Ensure original directory exists
|
|
211
|
+
mkdirSync(dirname(entry.originalPath), { recursive: true });
|
|
212
|
+
// Restore file
|
|
213
|
+
copyFileSync(entry.quarantinePath, entry.originalPath);
|
|
214
|
+
// Update entry
|
|
215
|
+
entry.restored = true;
|
|
216
|
+
entry.restoredDate = new Date().toISOString();
|
|
217
|
+
// Save updated database
|
|
218
|
+
saveQuarantineDatabase(db, quarantineDir);
|
|
219
|
+
logger.info(`Restored quarantined file: ${entry.originalPath}`);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
logger.error(`Error restoring quarantined file: ${error instanceof Error ? error.message : String(error)}`);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Delete a quarantined file permanently
|
|
229
|
+
*/
|
|
230
|
+
export function deleteQuarantinedFile(entryId, quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
|
|
231
|
+
try {
|
|
232
|
+
const db = loadQuarantineDatabase(quarantineDir);
|
|
233
|
+
const entryIndex = db.entries.findIndex(e => e.id === entryId);
|
|
234
|
+
if (entryIndex === -1) {
|
|
235
|
+
logger.error(`Quarantine entry not found: ${entryId}`);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
const entry = db.entries[entryIndex];
|
|
239
|
+
if (!entry) {
|
|
240
|
+
logger.error(`Entry not found at index ${entryIndex}`);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
// Delete quarantined file
|
|
244
|
+
if (existsSync(entry.quarantinePath)) {
|
|
245
|
+
unlinkSync(entry.quarantinePath);
|
|
246
|
+
}
|
|
247
|
+
// Remove entry from database
|
|
248
|
+
db.entries.splice(entryIndex, 1);
|
|
249
|
+
saveQuarantineDatabase(db, quarantineDir);
|
|
250
|
+
logger.info(`Permanently deleted quarantined file: ${entryId}`);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
logger.error(`Error deleting quarantined file: ${error instanceof Error ? error.message : String(error)}`);
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* List quarantined files
|
|
260
|
+
*/
|
|
261
|
+
export function listQuarantinedFiles(quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
|
|
262
|
+
const db = loadQuarantineDatabase(quarantineDir);
|
|
263
|
+
return db.entries.sort((a, b) => new Date(b.quarantineDate).getTime() - new Date(a.quarantineDate).getTime());
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Get quarantine statistics
|
|
267
|
+
*/
|
|
268
|
+
export function getQuarantineStats(quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
|
|
269
|
+
const db = loadQuarantineDatabase(quarantineDir);
|
|
270
|
+
return db.stats;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Clean up old quarantine entries
|
|
274
|
+
*/
|
|
275
|
+
export function cleanupQuarantine(maxAgeDays = 30, quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
|
|
276
|
+
try {
|
|
277
|
+
const db = loadQuarantineDatabase(quarantineDir);
|
|
278
|
+
const cutoffDate = new Date(Date.now() - maxAgeDays * 24 * 60 * 60 * 1000);
|
|
279
|
+
const entriesToDelete = db.entries.filter(entry => {
|
|
280
|
+
const entryDate = new Date(entry.quarantineDate);
|
|
281
|
+
return entryDate < cutoffDate && entry.restored;
|
|
282
|
+
});
|
|
283
|
+
let deletedCount = 0;
|
|
284
|
+
for (const entry of entriesToDelete) {
|
|
285
|
+
if (deleteQuarantinedFile(entry.id, quarantineDir)) {
|
|
286
|
+
deletedCount++;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
logger.info(`Cleaned up ${deletedCount} old quarantine entries`);
|
|
290
|
+
return deletedCount;
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
logger.error(`Error cleaning up quarantine: ${error instanceof Error ? error.message : String(error)}`);
|
|
294
|
+
return 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Check quarantine health
|
|
299
|
+
*/
|
|
300
|
+
export function checkQuarantineHealth(quarantineDir = DEFAULT_OPTIONS.quarantineDir) {
|
|
301
|
+
const issues = [];
|
|
302
|
+
const db = loadQuarantineDatabase(quarantineDir);
|
|
303
|
+
// Check for missing quarantined files
|
|
304
|
+
for (const entry of db.entries) {
|
|
305
|
+
if (!entry.restored && !existsSync(entry.quarantinePath)) {
|
|
306
|
+
issues.push(`Missing quarantined file: ${entry.id} (${entry.originalPath})`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Check quarantine directory structure
|
|
310
|
+
const quarantineFilesDir = resolve(quarantineDir, 'files');
|
|
311
|
+
if (!existsSync(quarantineFilesDir)) {
|
|
312
|
+
issues.push('Quarantine files directory missing');
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
healthy: issues.length === 0,
|
|
316
|
+
issues,
|
|
317
|
+
stats: db.stats
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
export default {
|
|
321
|
+
quarantineFile,
|
|
322
|
+
restoreQuarantinedFile,
|
|
323
|
+
deleteQuarantinedFile,
|
|
324
|
+
listQuarantinedFiles,
|
|
325
|
+
getQuarantineStats,
|
|
326
|
+
cleanupQuarantine,
|
|
327
|
+
checkQuarantineHealth
|
|
328
|
+
};
|
|
329
|
+
//# sourceMappingURL=Quarantine.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConsoleReporter - Beautiful terminal output for scan results
|
|
3
|
+
*/
|
|
4
|
+
import type { ScanResult } from '../types.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generate console report
|
|
7
|
+
*/
|
|
8
|
+
export declare function generateConsoleReport(result: ScanResult, options?: {
|
|
9
|
+
verbose?: boolean;
|
|
10
|
+
ci?: boolean;
|
|
11
|
+
}): string;
|
|
12
|
+
export default generateConsoleReport;
|
|
13
|
+
//# sourceMappingURL=ConsoleReporter.d.ts.map
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConsoleReporter - Beautiful terminal output for scan results
|
|
3
|
+
*/
|
|
4
|
+
// ANSI color codes
|
|
5
|
+
const colors = {
|
|
6
|
+
reset: '\x1b[0m',
|
|
7
|
+
bold: '\x1b[1m',
|
|
8
|
+
dim: '\x1b[2m',
|
|
9
|
+
red: '\x1b[31m',
|
|
10
|
+
green: '\x1b[32m',
|
|
11
|
+
yellow: '\x1b[33m',
|
|
12
|
+
blue: '\x1b[34m',
|
|
13
|
+
magenta: '\x1b[35m',
|
|
14
|
+
cyan: '\x1b[36m',
|
|
15
|
+
white: '\x1b[37m',
|
|
16
|
+
bgRed: '\x1b[41m',
|
|
17
|
+
bgYellow: '\x1b[43m',
|
|
18
|
+
bgBlue: '\x1b[44m',
|
|
19
|
+
};
|
|
20
|
+
const FERRET_BANNER = `
|
|
21
|
+
${colors.cyan} ⡠⢂⠔⠚⠟⠓⠒⠒⢂⠐⢄
|
|
22
|
+
⣷⣧⣀⠀⢀⣀⣤⣄⠈⢢⢸⡀ ${colors.bold}███████╗███████╗██████╗ ██████╗ ███████╗████████╗
|
|
23
|
+
${colors.cyan}⢀⣿⣭⣿⣿⣿⣿⣽⣹⣧⠈⣾⢱⡀ ${colors.bold}██╔════╝██╔════╝██╔══██╗██╔══██╗██╔════╝╚══██╔══╝
|
|
24
|
+
${colors.cyan}⢸⢿⠋⢸⠂⠈⠹⢿⣿⡿⠀⢸⡷⡇ ${colors.bold}█████╗ █████╗ ██████╔╝██████╔╝█████╗ ██║
|
|
25
|
+
${colors.cyan}⠈⣆⠉⢇⢁⠶⠈⠀⠉⠀⢀⣾⣇⡇ ${colors.bold}██╔══╝ ██╔══╝ ██╔══██╗██╔══██╗██╔══╝ ██║
|
|
26
|
+
${colors.cyan} ⢑⣦⣤⣤⣤⣤⣴⣶⣿⡿⢨⠃ ${colors.bold}██║ ███████╗██║ ██║██║ ██║███████╗ ██║
|
|
27
|
+
${colors.cyan} ⢰⣿⣿⣟⣯⡿⣽⣻⣾⣽⣇⠏ ${colors.bold}╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═╝${colors.reset}
|
|
28
|
+
${colors.dim} Security Scanner for AI CLI Configs${colors.reset}
|
|
29
|
+
`;
|
|
30
|
+
const SEVERITY_COLORS = {
|
|
31
|
+
CRITICAL: colors.bgRed + colors.white + colors.bold,
|
|
32
|
+
HIGH: colors.red + colors.bold,
|
|
33
|
+
MEDIUM: colors.yellow,
|
|
34
|
+
LOW: colors.blue,
|
|
35
|
+
INFO: colors.dim,
|
|
36
|
+
};
|
|
37
|
+
const SEVERITY_ICONS = {
|
|
38
|
+
CRITICAL: '!!!',
|
|
39
|
+
HIGH: '!!',
|
|
40
|
+
MEDIUM: '!',
|
|
41
|
+
LOW: '*',
|
|
42
|
+
INFO: '-',
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Format a severity badge
|
|
46
|
+
*/
|
|
47
|
+
function formatSeverity(severity) {
|
|
48
|
+
const color = SEVERITY_COLORS[severity];
|
|
49
|
+
const icon = SEVERITY_ICONS[severity];
|
|
50
|
+
return `${color}[${icon} ${severity}]${colors.reset}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Format a finding for display
|
|
54
|
+
*/
|
|
55
|
+
function formatFinding(finding, verbose) {
|
|
56
|
+
const lines = [];
|
|
57
|
+
// Header
|
|
58
|
+
lines.push(`${formatSeverity(finding.severity)} ${colors.bold}${finding.ruleId}${colors.reset} - ${finding.ruleName}`);
|
|
59
|
+
// Location
|
|
60
|
+
lines.push(` ${colors.cyan}File:${colors.reset} ${finding.relativePath}:${finding.line}`);
|
|
61
|
+
// Match
|
|
62
|
+
const matchDisplay = finding.match.length > 80
|
|
63
|
+
? finding.match.slice(0, 77) + '...'
|
|
64
|
+
: finding.match;
|
|
65
|
+
lines.push(` ${colors.cyan}Match:${colors.reset} ${colors.yellow}${matchDisplay}${colors.reset}`);
|
|
66
|
+
// Context (if verbose)
|
|
67
|
+
if (verbose && finding.context.length > 0) {
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push(` ${colors.dim}Context:${colors.reset}`);
|
|
70
|
+
for (const ctx of finding.context) {
|
|
71
|
+
const lineNum = String(ctx.lineNumber).padStart(4, ' ');
|
|
72
|
+
const marker = ctx.isMatch ? `${colors.red}>${colors.reset}` : ' ';
|
|
73
|
+
const lineColor = ctx.isMatch ? colors.yellow : colors.dim;
|
|
74
|
+
lines.push(` ${marker} ${colors.dim}${lineNum}${colors.reset} ${colors.dim}|${colors.reset} ${lineColor}${ctx.content}${colors.reset}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Remediation
|
|
78
|
+
lines.push(` ${colors.green}Remediation:${colors.reset} ${finding.remediation}`);
|
|
79
|
+
// Risk score
|
|
80
|
+
lines.push(` ${colors.magenta}Risk Score:${colors.reset} ${finding.riskScore}/100`);
|
|
81
|
+
return lines.join('\n');
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Format summary statistics
|
|
85
|
+
*/
|
|
86
|
+
function formatSummary(summary, result) {
|
|
87
|
+
const lines = [];
|
|
88
|
+
lines.push('');
|
|
89
|
+
lines.push(`${colors.bold}${'━'.repeat(60)}${colors.reset}`);
|
|
90
|
+
lines.push(`${colors.bold}SUMMARY${colors.reset}`);
|
|
91
|
+
lines.push(`${colors.bold}${'━'.repeat(60)}${colors.reset}`);
|
|
92
|
+
const stats = [
|
|
93
|
+
summary.critical > 0 ? `${SEVERITY_COLORS['CRITICAL']}Critical: ${summary.critical}${colors.reset}` : `Critical: ${summary.critical}`,
|
|
94
|
+
summary.high > 0 ? `${SEVERITY_COLORS['HIGH']}High: ${summary.high}${colors.reset}` : `High: ${summary.high}`,
|
|
95
|
+
summary.medium > 0 ? `${SEVERITY_COLORS['MEDIUM']}Medium: ${summary.medium}${colors.reset}` : `Medium: ${summary.medium}`,
|
|
96
|
+
`Low: ${summary.low}`,
|
|
97
|
+
`Info: ${summary.info}`,
|
|
98
|
+
];
|
|
99
|
+
lines.push(stats.join(' | '));
|
|
100
|
+
lines.push(`Files scanned: ${result.analyzedFiles} | Time: ${result.duration}ms | Risk Score: ${result.overallRiskScore}/100`);
|
|
101
|
+
return lines.join('\n');
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Format findings grouped by severity
|
|
105
|
+
*/
|
|
106
|
+
function formatGroupedFindings(result, verbose) {
|
|
107
|
+
const lines = [];
|
|
108
|
+
const severities = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'];
|
|
109
|
+
for (const severity of severities) {
|
|
110
|
+
const findings = result.findingsBySeverity[severity];
|
|
111
|
+
if (findings.length === 0)
|
|
112
|
+
continue;
|
|
113
|
+
lines.push('');
|
|
114
|
+
lines.push(`${SEVERITY_COLORS[severity]}${severity} (${findings.length})${colors.reset}`);
|
|
115
|
+
lines.push(`${colors.dim}${'━'.repeat(60)}${colors.reset}`);
|
|
116
|
+
lines.push('');
|
|
117
|
+
for (const finding of findings) {
|
|
118
|
+
lines.push(formatFinding(finding, verbose));
|
|
119
|
+
lines.push('');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Format scan errors
|
|
126
|
+
*/
|
|
127
|
+
function formatErrors(result) {
|
|
128
|
+
if (result.errors.length === 0)
|
|
129
|
+
return '';
|
|
130
|
+
const lines = [];
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push(`${colors.yellow}Errors (${result.errors.length})${colors.reset}`);
|
|
133
|
+
lines.push(`${colors.dim}${'━'.repeat(60)}${colors.reset}`);
|
|
134
|
+
for (const error of result.errors) {
|
|
135
|
+
const file = error.file ? `${error.file}: ` : '';
|
|
136
|
+
lines.push(` ${colors.yellow}!${colors.reset} ${file}${error.message}`);
|
|
137
|
+
}
|
|
138
|
+
return lines.join('\n');
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Generate console report
|
|
142
|
+
*/
|
|
143
|
+
export function generateConsoleReport(result, options = {}) {
|
|
144
|
+
const { verbose = false, ci = false } = options;
|
|
145
|
+
if (ci) {
|
|
146
|
+
return generateCiReport(result);
|
|
147
|
+
}
|
|
148
|
+
const lines = [];
|
|
149
|
+
// Banner
|
|
150
|
+
lines.push(FERRET_BANNER);
|
|
151
|
+
// Scan info
|
|
152
|
+
lines.push(`${colors.dim}Scanning: ${result.scannedPaths.join(', ')}${colors.reset}`);
|
|
153
|
+
lines.push(`${colors.dim}Found: ${result.analyzedFiles} configuration files${colors.reset}`);
|
|
154
|
+
// Findings
|
|
155
|
+
if (result.summary.total === 0) {
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push(`${colors.green}${colors.bold}No security issues found!${colors.reset}`);
|
|
158
|
+
lines.push(`${colors.green}Your AI CLI configurations look clean.${colors.reset}`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
lines.push(formatGroupedFindings(result, verbose));
|
|
162
|
+
}
|
|
163
|
+
// Errors
|
|
164
|
+
if (result.errors.length > 0) {
|
|
165
|
+
lines.push(formatErrors(result));
|
|
166
|
+
}
|
|
167
|
+
// Summary
|
|
168
|
+
lines.push(formatSummary(result.summary, result));
|
|
169
|
+
return lines.join('\n');
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Generate CI-friendly report (minimal formatting)
|
|
173
|
+
*/
|
|
174
|
+
function generateCiReport(result) {
|
|
175
|
+
const lines = [];
|
|
176
|
+
lines.push(`[FERRET] Scanned ${result.analyzedFiles} files in ${result.duration}ms`);
|
|
177
|
+
for (const finding of result.findings) {
|
|
178
|
+
lines.push(`[${finding.severity}] ${finding.ruleId}: ${finding.relativePath}:${finding.line} - ${finding.ruleName}`);
|
|
179
|
+
}
|
|
180
|
+
lines.push(`[SUMMARY] Critical: ${result.summary.critical} | High: ${result.summary.high} | Medium: ${result.summary.medium} | Low: ${result.summary.low} | Info: ${result.summary.info}`);
|
|
181
|
+
lines.push(`[RISK] Overall risk score: ${result.overallRiskScore}/100`);
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
export default generateConsoleReport;
|
|
185
|
+
//# sourceMappingURL=ConsoleReporter.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Reporter - Beautiful HTML reports with interactive filtering
|
|
3
|
+
* Generates standalone HTML files with embedded CSS and JavaScript
|
|
4
|
+
*/
|
|
5
|
+
import type { ScanResult } from '../types.js';
|
|
6
|
+
interface HtmlReportOptions {
|
|
7
|
+
title?: string;
|
|
8
|
+
includeContext?: boolean;
|
|
9
|
+
darkMode?: boolean;
|
|
10
|
+
showCode?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generate complete HTML report
|
|
14
|
+
*/
|
|
15
|
+
export declare function generateHtmlReport(result: ScanResult, options?: HtmlReportOptions): string;
|
|
16
|
+
/**
|
|
17
|
+
* Format HTML report as string
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatHtmlReport(result: ScanResult, options?: HtmlReportOptions): string;
|
|
20
|
+
declare const _default: {
|
|
21
|
+
generateHtmlReport: typeof generateHtmlReport;
|
|
22
|
+
formatHtmlReport: typeof formatHtmlReport;
|
|
23
|
+
};
|
|
24
|
+
export default _default;
|
|
25
|
+
//# sourceMappingURL=HtmlReporter.d.ts.map
|