ghostpatch 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/LICENSE +21 -0
- package/README.md +213 -0
- package/__tests__/detectors.test.ts +224 -0
- package/__tests__/rules.test.ts +117 -0
- package/__tests__/scanner.test.ts +222 -0
- package/dist/ai/anthropic.d.ts +11 -0
- package/dist/ai/anthropic.d.ts.map +1 -0
- package/dist/ai/anthropic.js +76 -0
- package/dist/ai/anthropic.js.map +1 -0
- package/dist/ai/huggingface.d.ts +12 -0
- package/dist/ai/huggingface.d.ts.map +1 -0
- package/dist/ai/huggingface.js +95 -0
- package/dist/ai/huggingface.js.map +1 -0
- package/dist/ai/openai.d.ts +11 -0
- package/dist/ai/openai.d.ts.map +1 -0
- package/dist/ai/openai.js +71 -0
- package/dist/ai/openai.js.map +1 -0
- package/dist/ai/prompts.d.ts +5 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +101 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/ai/provider.d.ts +9 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +66 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +318 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/reporter.d.ts +7 -0
- package/dist/core/reporter.d.ts.map +1 -0
- package/dist/core/reporter.js +366 -0
- package/dist/core/reporter.js.map +1 -0
- package/dist/core/rules.d.ts +8 -0
- package/dist/core/rules.d.ts.map +1 -0
- package/dist/core/rules.js +1077 -0
- package/dist/core/rules.js.map +1 -0
- package/dist/core/scanner.d.ts +6 -0
- package/dist/core/scanner.d.ts.map +1 -0
- package/dist/core/scanner.js +217 -0
- package/dist/core/scanner.js.map +1 -0
- package/dist/core/severity.d.ts +100 -0
- package/dist/core/severity.d.ts.map +1 -0
- package/dist/core/severity.js +52 -0
- package/dist/core/severity.js.map +1 -0
- package/dist/detectors/auth.d.ts +3 -0
- package/dist/detectors/auth.d.ts.map +1 -0
- package/dist/detectors/auth.js +138 -0
- package/dist/detectors/auth.js.map +1 -0
- package/dist/detectors/crypto.d.ts +3 -0
- package/dist/detectors/crypto.d.ts.map +1 -0
- package/dist/detectors/crypto.js +128 -0
- package/dist/detectors/crypto.js.map +1 -0
- package/dist/detectors/dependency.d.ts +4 -0
- package/dist/detectors/dependency.d.ts.map +1 -0
- package/dist/detectors/dependency.js +267 -0
- package/dist/detectors/dependency.js.map +1 -0
- package/dist/detectors/deserialize.d.ts +3 -0
- package/dist/detectors/deserialize.d.ts.map +1 -0
- package/dist/detectors/deserialize.js +107 -0
- package/dist/detectors/deserialize.js.map +1 -0
- package/dist/detectors/injection.d.ts +3 -0
- package/dist/detectors/injection.d.ts.map +1 -0
- package/dist/detectors/injection.js +158 -0
- package/dist/detectors/injection.js.map +1 -0
- package/dist/detectors/misconfig.d.ts +3 -0
- package/dist/detectors/misconfig.d.ts.map +1 -0
- package/dist/detectors/misconfig.js +153 -0
- package/dist/detectors/misconfig.js.map +1 -0
- package/dist/detectors/pathtraversal.d.ts +3 -0
- package/dist/detectors/pathtraversal.d.ts.map +1 -0
- package/dist/detectors/pathtraversal.js +90 -0
- package/dist/detectors/pathtraversal.js.map +1 -0
- package/dist/detectors/prototype.d.ts +3 -0
- package/dist/detectors/prototype.d.ts.map +1 -0
- package/dist/detectors/prototype.js +79 -0
- package/dist/detectors/prototype.js.map +1 -0
- package/dist/detectors/secrets.d.ts +4 -0
- package/dist/detectors/secrets.d.ts.map +1 -0
- package/dist/detectors/secrets.js +137 -0
- package/dist/detectors/secrets.js.map +1 -0
- package/dist/detectors/ssrf.d.ts +3 -0
- package/dist/detectors/ssrf.d.ts.map +1 -0
- package/dist/detectors/ssrf.js +78 -0
- package/dist/detectors/ssrf.js.map +1 -0
- package/dist/detectors/zeroday.d.ts +9 -0
- package/dist/detectors/zeroday.d.ts.map +1 -0
- package/dist/detectors/zeroday.js +77 -0
- package/dist/detectors/zeroday.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +358 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.d.ts.map +1 -0
- package/dist/utils/config.js +97 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/fingerprint.d.ts +5 -0
- package/dist/utils/fingerprint.d.ts.map +1 -0
- package/dist/utils/fingerprint.js +55 -0
- package/dist/utils/fingerprint.js.map +1 -0
- package/dist/utils/languages.d.ts +8 -0
- package/dist/utils/languages.d.ts.map +1 -0
- package/dist/utils/languages.js +128 -0
- package/dist/utils/languages.js.map +1 -0
- package/package.json +53 -0
- package/src/ai/anthropic.ts +82 -0
- package/src/ai/huggingface.ts +111 -0
- package/src/ai/openai.ts +75 -0
- package/src/ai/prompts.ts +100 -0
- package/src/ai/provider.ts +68 -0
- package/src/cli/index.ts +314 -0
- package/src/core/reporter.ts +356 -0
- package/src/core/rules.ts +1089 -0
- package/src/core/scanner.ts +201 -0
- package/src/core/severity.ts +140 -0
- package/src/detectors/auth.ts +152 -0
- package/src/detectors/crypto.ts +128 -0
- package/src/detectors/dependency.ts +240 -0
- package/src/detectors/deserialize.ts +106 -0
- package/src/detectors/injection.ts +172 -0
- package/src/detectors/misconfig.ts +152 -0
- package/src/detectors/pathtraversal.ts +89 -0
- package/src/detectors/prototype.ts +77 -0
- package/src/detectors/secrets.ts +138 -0
- package/src/detectors/ssrf.ts +77 -0
- package/src/detectors/zeroday.ts +93 -0
- package/src/index.ts +24 -0
- package/src/mcp/server.ts +379 -0
- package/src/utils/config.ts +64 -0
- package/src/utils/fingerprint.ts +21 -0
- package/src/utils/languages.ts +95 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +8 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
import { scan } from '../core/scanner';
|
|
7
|
+
import { generateReport } from '../core/reporter';
|
|
8
|
+
import { Severity } from '../core/severity';
|
|
9
|
+
import { getAvailableProvider } from '../ai/provider';
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.name('ghostpatch')
|
|
15
|
+
.description('AI-powered security vulnerability scanner')
|
|
16
|
+
.version('1.0.0');
|
|
17
|
+
|
|
18
|
+
// ============================================================
|
|
19
|
+
// scan command
|
|
20
|
+
// ============================================================
|
|
21
|
+
program
|
|
22
|
+
.command('scan')
|
|
23
|
+
.description('Scan for security vulnerabilities')
|
|
24
|
+
.argument('[path]', 'Path to scan', '.')
|
|
25
|
+
.option('-o, --output <format>', 'Output format: terminal, json, sarif, html', 'terminal')
|
|
26
|
+
.option('-s, --severity <level>', 'Minimum severity: critical, high, medium, low, info', 'low')
|
|
27
|
+
.option('--ai', 'Enable AI-enhanced analysis')
|
|
28
|
+
.option('--provider <name>', 'AI provider: huggingface, anthropic, openai')
|
|
29
|
+
.option('--fix', 'Show auto-fix suggestions')
|
|
30
|
+
.option('-q, --quiet', 'Minimal output')
|
|
31
|
+
.option('--exclude <patterns...>', 'Additional exclude patterns')
|
|
32
|
+
.option('--max-file-size <bytes>', 'Maximum file size in bytes', '1048576')
|
|
33
|
+
.option('--config <path>', 'Path to config file')
|
|
34
|
+
.action(async (scanPath: string, options: any) => {
|
|
35
|
+
try {
|
|
36
|
+
const resolvedPath = path.resolve(scanPath);
|
|
37
|
+
|
|
38
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
39
|
+
console.error(`Error: Path does not exist: ${resolvedPath}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!options.quiet) {
|
|
44
|
+
console.log('\n GhostPatch v1.0.0 — AI-Powered Security Scanner\n');
|
|
45
|
+
console.log(` Scanning: ${resolvedPath}\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await scan(resolvedPath, {
|
|
49
|
+
output: options.output,
|
|
50
|
+
severity: options.severity as Severity,
|
|
51
|
+
ai: options.ai,
|
|
52
|
+
provider: options.provider,
|
|
53
|
+
fix: options.fix,
|
|
54
|
+
quiet: options.quiet,
|
|
55
|
+
exclude: options.exclude,
|
|
56
|
+
maxFileSize: parseInt(options.maxFileSize, 10),
|
|
57
|
+
configPath: options.config,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// If AI is enabled, run AI analysis
|
|
61
|
+
if (options.ai) {
|
|
62
|
+
const provider = getAvailableProvider(options.provider);
|
|
63
|
+
if (provider && provider.isAvailable()) {
|
|
64
|
+
if (!options.quiet) {
|
|
65
|
+
console.log(` AI Provider: ${provider.name}\n`);
|
|
66
|
+
}
|
|
67
|
+
// AI analysis would happen here on suspicious findings
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const report = generateReport(result, options.output, options.quiet);
|
|
72
|
+
|
|
73
|
+
if (options.output === 'json' || options.output === 'sarif') {
|
|
74
|
+
console.log(report);
|
|
75
|
+
} else if (options.output === 'html') {
|
|
76
|
+
const outputFile = path.join(process.cwd(), 'ghostpatch-report.html');
|
|
77
|
+
fs.writeFileSync(outputFile, report, 'utf-8');
|
|
78
|
+
console.log(` HTML report saved to: ${outputFile}\n`);
|
|
79
|
+
} else {
|
|
80
|
+
console.log(report);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Exit with non-zero if critical/high findings
|
|
84
|
+
const criticalOrHigh = (result.summary.bySeverity[Severity.CRITICAL] || 0)
|
|
85
|
+
+ (result.summary.bySeverity[Severity.HIGH] || 0);
|
|
86
|
+
if (criticalOrHigh > 0) {
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
} catch (err: any) {
|
|
90
|
+
console.error(`Error: ${err.message}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ============================================================
|
|
96
|
+
// secrets command
|
|
97
|
+
// ============================================================
|
|
98
|
+
program
|
|
99
|
+
.command('secrets')
|
|
100
|
+
.description('Scan for hardcoded secrets and API keys')
|
|
101
|
+
.argument('[path]', 'Path to scan', '.')
|
|
102
|
+
.option('-o, --output <format>', 'Output format', 'terminal')
|
|
103
|
+
.option('-q, --quiet', 'Minimal output')
|
|
104
|
+
.action(async (scanPath: string, options: any) => {
|
|
105
|
+
try {
|
|
106
|
+
const resolvedPath = path.resolve(scanPath);
|
|
107
|
+
if (!options.quiet) {
|
|
108
|
+
console.log('\n GhostPatch — Secrets Scanner\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = await scan(resolvedPath, {
|
|
112
|
+
output: options.output,
|
|
113
|
+
severity: Severity.LOW,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Filter to only secrets-related findings
|
|
117
|
+
const secretsResult = {
|
|
118
|
+
...result,
|
|
119
|
+
findings: result.findings.filter(f =>
|
|
120
|
+
f.ruleId.startsWith('SEC-') ||
|
|
121
|
+
f.ruleId.startsWith('SEC') ||
|
|
122
|
+
f.title.toLowerCase().includes('secret') ||
|
|
123
|
+
f.title.toLowerCase().includes('key') ||
|
|
124
|
+
f.title.toLowerCase().includes('token') ||
|
|
125
|
+
f.title.toLowerCase().includes('password') ||
|
|
126
|
+
f.title.toLowerCase().includes('credential')
|
|
127
|
+
),
|
|
128
|
+
};
|
|
129
|
+
secretsResult.summary.total = secretsResult.findings.length;
|
|
130
|
+
|
|
131
|
+
const report = generateReport(secretsResult, options.output, options.quiet);
|
|
132
|
+
console.log(report);
|
|
133
|
+
} catch (err: any) {
|
|
134
|
+
console.error(`Error: ${err.message}`);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ============================================================
|
|
140
|
+
// deps command
|
|
141
|
+
// ============================================================
|
|
142
|
+
program
|
|
143
|
+
.command('deps')
|
|
144
|
+
.description('Check dependencies for known vulnerabilities')
|
|
145
|
+
.argument('[path]', 'Path to scan', '.')
|
|
146
|
+
.option('-o, --output <format>', 'Output format', 'terminal')
|
|
147
|
+
.action(async (scanPath: string, options: any) => {
|
|
148
|
+
try {
|
|
149
|
+
const resolvedPath = path.resolve(scanPath);
|
|
150
|
+
console.log('\n GhostPatch — Dependency Scanner\n');
|
|
151
|
+
|
|
152
|
+
const result = await scan(resolvedPath, {
|
|
153
|
+
output: options.output,
|
|
154
|
+
severity: Severity.LOW,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const depsResult = {
|
|
158
|
+
...result,
|
|
159
|
+
findings: result.findings.filter(f =>
|
|
160
|
+
f.ruleId.startsWith('DEP-') ||
|
|
161
|
+
f.owasp === 'A06'
|
|
162
|
+
),
|
|
163
|
+
};
|
|
164
|
+
depsResult.summary.total = depsResult.findings.length;
|
|
165
|
+
|
|
166
|
+
const report = generateReport(depsResult, options.output);
|
|
167
|
+
console.log(report);
|
|
168
|
+
} catch (err: any) {
|
|
169
|
+
console.error(`Error: ${err.message}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// ============================================================
|
|
175
|
+
// watch command
|
|
176
|
+
// ============================================================
|
|
177
|
+
program
|
|
178
|
+
.command('watch')
|
|
179
|
+
.description('Watch mode — scan on file changes')
|
|
180
|
+
.argument('[path]', 'Path to watch', '.')
|
|
181
|
+
.option('-s, --severity <level>', 'Minimum severity', 'medium')
|
|
182
|
+
.option('-q, --quiet', 'Minimal output')
|
|
183
|
+
.action(async (watchPath: string, options: any) => {
|
|
184
|
+
try {
|
|
185
|
+
const resolvedPath = path.resolve(watchPath);
|
|
186
|
+
console.log('\n GhostPatch — Watch Mode\n');
|
|
187
|
+
console.log(` Watching: ${resolvedPath}\n`);
|
|
188
|
+
|
|
189
|
+
const chokidar = require('chokidar');
|
|
190
|
+
const watcher = chokidar.watch(resolvedPath, {
|
|
191
|
+
ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
|
|
192
|
+
persistent: true,
|
|
193
|
+
ignoreInitial: true,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
let scanTimeout: NodeJS.Timeout | null = null;
|
|
197
|
+
|
|
198
|
+
const runScan = async () => {
|
|
199
|
+
console.log('\n File change detected. Scanning...\n');
|
|
200
|
+
const result = await scan(resolvedPath, {
|
|
201
|
+
severity: options.severity as Severity,
|
|
202
|
+
quiet: true,
|
|
203
|
+
});
|
|
204
|
+
const report = generateReport(result, 'terminal', options.quiet);
|
|
205
|
+
console.log(report);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
watcher.on('change', () => {
|
|
209
|
+
if (scanTimeout) clearTimeout(scanTimeout);
|
|
210
|
+
scanTimeout = setTimeout(runScan, 500);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
watcher.on('add', () => {
|
|
214
|
+
if (scanTimeout) clearTimeout(scanTimeout);
|
|
215
|
+
scanTimeout = setTimeout(runScan, 500);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// Initial scan
|
|
219
|
+
await runScan();
|
|
220
|
+
|
|
221
|
+
console.log(' Watching for changes... (Ctrl+C to stop)\n');
|
|
222
|
+
} catch (err: any) {
|
|
223
|
+
console.error(`Error: ${err.message}`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// ============================================================
|
|
229
|
+
// report command
|
|
230
|
+
// ============================================================
|
|
231
|
+
program
|
|
232
|
+
.command('report')
|
|
233
|
+
.description('Generate HTML security report')
|
|
234
|
+
.argument('[path]', 'Path to scan', '.')
|
|
235
|
+
.option('-o, --output <file>', 'Output file', 'ghostpatch-report.html')
|
|
236
|
+
.option('-s, --severity <level>', 'Minimum severity', 'low')
|
|
237
|
+
.option('--ai', 'Enable AI analysis')
|
|
238
|
+
.action(async (scanPath: string, options: any) => {
|
|
239
|
+
try {
|
|
240
|
+
const resolvedPath = path.resolve(scanPath);
|
|
241
|
+
console.log('\n GhostPatch — Generating Report\n');
|
|
242
|
+
|
|
243
|
+
const result = await scan(resolvedPath, {
|
|
244
|
+
severity: options.severity as Severity,
|
|
245
|
+
ai: options.ai,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const html = generateReport(result, 'html');
|
|
249
|
+
const outputFile = path.resolve(options.output);
|
|
250
|
+
fs.writeFileSync(outputFile, html, 'utf-8');
|
|
251
|
+
console.log(` Report saved to: ${outputFile}`);
|
|
252
|
+
console.log(` Findings: ${result.summary.total}\n`);
|
|
253
|
+
} catch (err: any) {
|
|
254
|
+
console.error(`Error: ${err.message}`);
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ============================================================
|
|
260
|
+
// serve command (MCP server)
|
|
261
|
+
// ============================================================
|
|
262
|
+
program
|
|
263
|
+
.command('serve')
|
|
264
|
+
.description('Start MCP server for AI coding agents')
|
|
265
|
+
.action(async () => {
|
|
266
|
+
const { startMCPServer } = require('../mcp/server');
|
|
267
|
+
await startMCPServer();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ============================================================
|
|
271
|
+
// install command
|
|
272
|
+
// ============================================================
|
|
273
|
+
program
|
|
274
|
+
.command('install')
|
|
275
|
+
.description('Configure GhostPatch MCP for Claude Code')
|
|
276
|
+
.action(async () => {
|
|
277
|
+
try {
|
|
278
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
279
|
+
const claudeConfigDir = path.join(home, '.claude');
|
|
280
|
+
const mcpConfigPath = path.join(claudeConfigDir, 'mcp_servers.json');
|
|
281
|
+
|
|
282
|
+
if (!fs.existsSync(claudeConfigDir)) {
|
|
283
|
+
fs.mkdirSync(claudeConfigDir, { recursive: true });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let config: any = {};
|
|
287
|
+
if (fs.existsSync(mcpConfigPath)) {
|
|
288
|
+
config = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf-8'));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
config.ghostpatch = {
|
|
292
|
+
command: 'node',
|
|
293
|
+
args: [path.resolve(__dirname, '../mcp/server.js')],
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
fs.writeFileSync(mcpConfigPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
297
|
+
console.log('\n GhostPatch MCP server configured for Claude Code!');
|
|
298
|
+
console.log(` Config: ${mcpConfigPath}\n`);
|
|
299
|
+
} catch (err: any) {
|
|
300
|
+
console.error(`Error: ${err.message}`);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// ============================================================
|
|
306
|
+
// Default command (scan current directory)
|
|
307
|
+
// ============================================================
|
|
308
|
+
program
|
|
309
|
+
.action(async () => {
|
|
310
|
+
// If no command provided, run scan on current directory
|
|
311
|
+
await program.parseAsync(['node', 'ghostpatch', 'scan', '.']);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
program.parse();
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { Finding, ScanResult, Severity, SEVERITY_COLORS, SEVERITY_ICONS } from './severity';
|
|
3
|
+
|
|
4
|
+
const RESET = '\x1b[0m';
|
|
5
|
+
const BOLD = '\x1b[1m';
|
|
6
|
+
const DIM = '\x1b[2m';
|
|
7
|
+
const WHITE = '\x1b[37m';
|
|
8
|
+
const GREEN = '\x1b[32m';
|
|
9
|
+
const YELLOW = '\x1b[33m';
|
|
10
|
+
const RED = '\x1b[31m';
|
|
11
|
+
const CYAN = '\x1b[36m';
|
|
12
|
+
|
|
13
|
+
// ============================================================
|
|
14
|
+
// Terminal Reporter
|
|
15
|
+
// ============================================================
|
|
16
|
+
export function reportTerminal(result: ScanResult, quiet: boolean = false): string {
|
|
17
|
+
const lines: string[] = [];
|
|
18
|
+
|
|
19
|
+
lines.push('');
|
|
20
|
+
lines.push(`${BOLD}${WHITE} GhostPatch Security Scan Report${RESET}`);
|
|
21
|
+
lines.push(`${DIM} ${'='.repeat(50)}${RESET}`);
|
|
22
|
+
lines.push('');
|
|
23
|
+
|
|
24
|
+
// Summary
|
|
25
|
+
lines.push(` ${DIM}Target:${RESET} ${result.target}`);
|
|
26
|
+
lines.push(` ${DIM}Files:${RESET} ${result.filesScanned} scanned, ${result.filesSkipped} skipped`);
|
|
27
|
+
lines.push(` ${DIM}Time:${RESET} ${result.durationMs}ms`);
|
|
28
|
+
lines.push(` ${DIM}AI:${RESET} ${result.aiEnabled ? 'enabled' : 'disabled'}`);
|
|
29
|
+
lines.push('');
|
|
30
|
+
|
|
31
|
+
// Severity breakdown
|
|
32
|
+
const { bySeverity } = result.summary;
|
|
33
|
+
const critCount = bySeverity[Severity.CRITICAL] || 0;
|
|
34
|
+
const highCount = bySeverity[Severity.HIGH] || 0;
|
|
35
|
+
const medCount = bySeverity[Severity.MEDIUM] || 0;
|
|
36
|
+
const lowCount = bySeverity[Severity.LOW] || 0;
|
|
37
|
+
const infoCount = bySeverity[Severity.INFO] || 0;
|
|
38
|
+
|
|
39
|
+
lines.push(` ${SEVERITY_COLORS[Severity.CRITICAL]} CRITICAL ${RESET} ${critCount}`);
|
|
40
|
+
lines.push(` ${SEVERITY_COLORS[Severity.HIGH]} HIGH ${RESET} ${highCount}`);
|
|
41
|
+
lines.push(` ${SEVERITY_COLORS[Severity.MEDIUM]} MEDIUM ${RESET} ${medCount}`);
|
|
42
|
+
lines.push(` ${SEVERITY_COLORS[Severity.LOW]} LOW ${RESET} ${lowCount}`);
|
|
43
|
+
lines.push(` ${DIM} INFO ${RESET} ${infoCount}`);
|
|
44
|
+
lines.push('');
|
|
45
|
+
|
|
46
|
+
if (result.summary.total === 0) {
|
|
47
|
+
lines.push(` ${GREEN}${BOLD}No security issues found!${RESET}`);
|
|
48
|
+
lines.push('');
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
lines.push(` ${DIM}${'─'.repeat(60)}${RESET}`);
|
|
53
|
+
lines.push('');
|
|
54
|
+
|
|
55
|
+
if (quiet) {
|
|
56
|
+
for (const finding of result.findings) {
|
|
57
|
+
const icon = SEVERITY_ICONS[finding.severity];
|
|
58
|
+
const color = SEVERITY_COLORS[finding.severity];
|
|
59
|
+
const relPath = path.relative(result.target, finding.filePath) || finding.filePath;
|
|
60
|
+
lines.push(` ${color}${icon}${RESET} ${finding.title} ${DIM}${relPath}:${finding.line}${RESET}`);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
for (const finding of result.findings) {
|
|
64
|
+
lines.push(formatFinding(finding, result.target));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
lines.push('');
|
|
69
|
+
lines.push(` ${DIM}${'─'.repeat(60)}${RESET}`);
|
|
70
|
+
lines.push(` ${BOLD}Total: ${result.summary.total} issue(s) found${RESET}`);
|
|
71
|
+
|
|
72
|
+
if (critCount > 0) {
|
|
73
|
+
lines.push(` ${RED}${BOLD}${critCount} critical issue(s) require immediate attention!${RESET}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
lines.push('');
|
|
77
|
+
return lines.join('\n');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatFinding(finding: Finding, basePath: string): string {
|
|
81
|
+
const lines: string[] = [];
|
|
82
|
+
const color = SEVERITY_COLORS[finding.severity];
|
|
83
|
+
const icon = SEVERITY_ICONS[finding.severity];
|
|
84
|
+
const relPath = path.relative(basePath, finding.filePath) || finding.filePath;
|
|
85
|
+
|
|
86
|
+
lines.push(` ${color}${BOLD}${icon} ${finding.title}${RESET}`);
|
|
87
|
+
lines.push(` ${DIM}${relPath}:${finding.line}${RESET}${finding.cwe ? ` ${DIM}${finding.cwe}${RESET}` : ''}${finding.aiEnhanced ? ` ${CYAN}[AI]${RESET}` : ''}`);
|
|
88
|
+
lines.push(` ${finding.description}`);
|
|
89
|
+
lines.push('');
|
|
90
|
+
|
|
91
|
+
if (finding.codeSnippet) {
|
|
92
|
+
const snippetLines = finding.codeSnippet.split('\n');
|
|
93
|
+
for (const sl of snippetLines) {
|
|
94
|
+
if (sl.startsWith('>')) {
|
|
95
|
+
lines.push(` ${color}${sl}${RESET}`);
|
|
96
|
+
} else {
|
|
97
|
+
lines.push(` ${DIM}${sl}${RESET}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
lines.push('');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (finding.remediation) {
|
|
104
|
+
lines.push(` ${GREEN}Fix: ${finding.remediation}${RESET}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
lines.push(` ${DIM}${'─'.repeat(60)}${RESET}`);
|
|
108
|
+
lines.push('');
|
|
109
|
+
|
|
110
|
+
return lines.join('\n');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================
|
|
114
|
+
// JSON Reporter
|
|
115
|
+
// ============================================================
|
|
116
|
+
export function reportJSON(result: ScanResult): string {
|
|
117
|
+
return JSON.stringify({
|
|
118
|
+
ghostpatch: {
|
|
119
|
+
version: '1.0.0',
|
|
120
|
+
scanDate: result.startTime.toISOString(),
|
|
121
|
+
},
|
|
122
|
+
target: result.target,
|
|
123
|
+
duration: result.durationMs,
|
|
124
|
+
filesScanned: result.filesScanned,
|
|
125
|
+
filesSkipped: result.filesSkipped,
|
|
126
|
+
aiEnabled: result.aiEnabled,
|
|
127
|
+
summary: result.summary,
|
|
128
|
+
findings: result.findings.map(f => ({
|
|
129
|
+
id: f.id,
|
|
130
|
+
ruleId: f.ruleId,
|
|
131
|
+
title: f.title,
|
|
132
|
+
description: f.description,
|
|
133
|
+
severity: f.severity,
|
|
134
|
+
confidence: f.confidence,
|
|
135
|
+
location: {
|
|
136
|
+
file: f.filePath,
|
|
137
|
+
line: f.line,
|
|
138
|
+
column: f.column,
|
|
139
|
+
endLine: f.endLine,
|
|
140
|
+
endColumn: f.endColumn,
|
|
141
|
+
},
|
|
142
|
+
cwe: f.cwe,
|
|
143
|
+
owasp: f.owasp,
|
|
144
|
+
codeSnippet: f.codeSnippet,
|
|
145
|
+
remediation: f.remediation,
|
|
146
|
+
aiEnhanced: f.aiEnhanced || false,
|
|
147
|
+
fingerprint: f.fingerprint,
|
|
148
|
+
})),
|
|
149
|
+
}, null, 2);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================
|
|
153
|
+
// SARIF Reporter (Static Analysis Results Interchange Format)
|
|
154
|
+
// ============================================================
|
|
155
|
+
export function reportSARIF(result: ScanResult): string {
|
|
156
|
+
const sarif = {
|
|
157
|
+
$schema: 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json',
|
|
158
|
+
version: '2.1.0',
|
|
159
|
+
runs: [{
|
|
160
|
+
tool: {
|
|
161
|
+
driver: {
|
|
162
|
+
name: 'GhostPatch',
|
|
163
|
+
version: '1.0.0',
|
|
164
|
+
informationUri: 'https://github.com/ghostpatch/ghostpatch',
|
|
165
|
+
rules: getUniqueRules(result.findings),
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
results: result.findings.map(f => ({
|
|
169
|
+
ruleId: f.ruleId,
|
|
170
|
+
level: sarifLevel(f.severity),
|
|
171
|
+
message: {
|
|
172
|
+
text: f.description,
|
|
173
|
+
},
|
|
174
|
+
locations: [{
|
|
175
|
+
physicalLocation: {
|
|
176
|
+
artifactLocation: {
|
|
177
|
+
uri: f.filePath.replace(/\\/g, '/'),
|
|
178
|
+
},
|
|
179
|
+
region: {
|
|
180
|
+
startLine: f.line,
|
|
181
|
+
startColumn: f.column || 1,
|
|
182
|
+
endLine: f.endLine || f.line,
|
|
183
|
+
endColumn: f.endColumn,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}],
|
|
187
|
+
fingerprints: {
|
|
188
|
+
'ghostpatch/v1': f.fingerprint,
|
|
189
|
+
},
|
|
190
|
+
fixes: f.remediation ? [{
|
|
191
|
+
description: { text: f.remediation },
|
|
192
|
+
}] : undefined,
|
|
193
|
+
})),
|
|
194
|
+
}],
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return JSON.stringify(sarif, null, 2);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function sarifLevel(severity: Severity): string {
|
|
201
|
+
switch (severity) {
|
|
202
|
+
case Severity.CRITICAL:
|
|
203
|
+
case Severity.HIGH: return 'error';
|
|
204
|
+
case Severity.MEDIUM: return 'warning';
|
|
205
|
+
case Severity.LOW:
|
|
206
|
+
case Severity.INFO: return 'note';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getUniqueRules(findings: Finding[]) {
|
|
211
|
+
const seen = new Set<string>();
|
|
212
|
+
const rules: any[] = [];
|
|
213
|
+
|
|
214
|
+
for (const f of findings) {
|
|
215
|
+
if (!seen.has(f.ruleId)) {
|
|
216
|
+
seen.add(f.ruleId);
|
|
217
|
+
rules.push({
|
|
218
|
+
id: f.ruleId,
|
|
219
|
+
shortDescription: { text: f.title },
|
|
220
|
+
fullDescription: { text: f.description },
|
|
221
|
+
help: { text: f.remediation || '' },
|
|
222
|
+
properties: {
|
|
223
|
+
cwe: f.cwe,
|
|
224
|
+
owasp: f.owasp,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return rules;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================
|
|
234
|
+
// HTML Reporter
|
|
235
|
+
// ============================================================
|
|
236
|
+
export function reportHTML(result: ScanResult): string {
|
|
237
|
+
const { bySeverity } = result.summary;
|
|
238
|
+
|
|
239
|
+
return `<!DOCTYPE html>
|
|
240
|
+
<html lang="en">
|
|
241
|
+
<head>
|
|
242
|
+
<meta charset="UTF-8">
|
|
243
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
244
|
+
<title>GhostPatch Security Report</title>
|
|
245
|
+
<style>
|
|
246
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
247
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0d1117; color: #c9d1d9; line-height: 1.6; }
|
|
248
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
|
249
|
+
h1 { color: #f0f6fc; font-size: 2rem; margin-bottom: 0.5rem; }
|
|
250
|
+
.subtitle { color: #8b949e; margin-bottom: 2rem; }
|
|
251
|
+
.summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
|
252
|
+
.stat { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; text-align: center; }
|
|
253
|
+
.stat-value { font-size: 2rem; font-weight: bold; }
|
|
254
|
+
.stat-label { color: #8b949e; font-size: 0.85rem; text-transform: uppercase; }
|
|
255
|
+
.critical .stat-value { color: #f85149; }
|
|
256
|
+
.high .stat-value { color: #f0883e; }
|
|
257
|
+
.medium .stat-value { color: #d29922; }
|
|
258
|
+
.low .stat-value { color: #3fb950; }
|
|
259
|
+
.info .stat-value { color: #58a6ff; }
|
|
260
|
+
.finding { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 1.5rem; margin-bottom: 1rem; }
|
|
261
|
+
.finding-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; }
|
|
262
|
+
.finding-title { font-size: 1.1rem; font-weight: 600; color: #f0f6fc; }
|
|
263
|
+
.badge { padding: 0.2rem 0.6rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
|
|
264
|
+
.badge-critical { background: #f85149; color: #fff; }
|
|
265
|
+
.badge-high { background: #f0883e; color: #fff; }
|
|
266
|
+
.badge-medium { background: #d29922; color: #000; }
|
|
267
|
+
.badge-low { background: #3fb950; color: #000; }
|
|
268
|
+
.badge-info { background: #58a6ff; color: #000; }
|
|
269
|
+
.finding-meta { color: #8b949e; font-size: 0.85rem; margin-bottom: 0.5rem; }
|
|
270
|
+
.finding-desc { margin-bottom: 1rem; }
|
|
271
|
+
pre { background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; overflow-x: auto; font-size: 0.85rem; }
|
|
272
|
+
.fix { color: #3fb950; background: #0d2818; border: 1px solid #238636; border-radius: 6px; padding: 0.75rem; margin-top: 0.5rem; font-size: 0.9rem; }
|
|
273
|
+
.chart { display: flex; height: 24px; border-radius: 12px; overflow: hidden; margin-bottom: 2rem; }
|
|
274
|
+
.chart-seg { transition: width 0.3s; }
|
|
275
|
+
.chart-critical { background: #f85149; }
|
|
276
|
+
.chart-high { background: #f0883e; }
|
|
277
|
+
.chart-medium { background: #d29922; }
|
|
278
|
+
.chart-low { background: #3fb950; }
|
|
279
|
+
.chart-info { background: #58a6ff; }
|
|
280
|
+
.footer { text-align: center; color: #8b949e; margin-top: 2rem; padding-top: 1rem; border-top: 1px solid #30363d; }
|
|
281
|
+
.ai-badge { background: #a371f7; color: #fff; padding: 0.1rem 0.4rem; border-radius: 8px; font-size: 0.7rem; margin-left: 0.5rem; }
|
|
282
|
+
</style>
|
|
283
|
+
</head>
|
|
284
|
+
<body>
|
|
285
|
+
<div class="container">
|
|
286
|
+
<h1>GhostPatch Security Report</h1>
|
|
287
|
+
<p class="subtitle">Scan completed ${result.startTime.toISOString()} | ${result.filesScanned} files scanned | ${result.durationMs}ms</p>
|
|
288
|
+
|
|
289
|
+
${result.summary.total > 0 ? `<div class="chart">
|
|
290
|
+
${chartSegment('critical', bySeverity[Severity.CRITICAL], result.summary.total)}
|
|
291
|
+
${chartSegment('high', bySeverity[Severity.HIGH], result.summary.total)}
|
|
292
|
+
${chartSegment('medium', bySeverity[Severity.MEDIUM], result.summary.total)}
|
|
293
|
+
${chartSegment('low', bySeverity[Severity.LOW], result.summary.total)}
|
|
294
|
+
${chartSegment('info', bySeverity[Severity.INFO], result.summary.total)}
|
|
295
|
+
</div>` : ''}
|
|
296
|
+
|
|
297
|
+
<div class="summary">
|
|
298
|
+
<div class="stat critical"><div class="stat-value">${bySeverity[Severity.CRITICAL] || 0}</div><div class="stat-label">Critical</div></div>
|
|
299
|
+
<div class="stat high"><div class="stat-value">${bySeverity[Severity.HIGH] || 0}</div><div class="stat-label">High</div></div>
|
|
300
|
+
<div class="stat medium"><div class="stat-value">${bySeverity[Severity.MEDIUM] || 0}</div><div class="stat-label">Medium</div></div>
|
|
301
|
+
<div class="stat low"><div class="stat-value">${bySeverity[Severity.LOW] || 0}</div><div class="stat-label">Low</div></div>
|
|
302
|
+
<div class="stat info"><div class="stat-value">${bySeverity[Severity.INFO] || 0}</div><div class="stat-label">Info</div></div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
${result.findings.length === 0 ? '<div class="finding"><p style="text-align:center; color:#3fb950; font-size:1.2rem;">No security issues found!</p></div>' : ''}
|
|
306
|
+
|
|
307
|
+
${result.findings.map(f => `<div class="finding">
|
|
308
|
+
<div class="finding-header">
|
|
309
|
+
<span class="finding-title">${escapeHtml(f.title)}${f.aiEnhanced ? '<span class="ai-badge">AI</span>' : ''}</span>
|
|
310
|
+
<span class="badge badge-${f.severity}">${f.severity}</span>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="finding-meta">${escapeHtml(path.relative(result.target, f.filePath) || f.filePath)}:${f.line}${f.cwe ? ` | ${f.cwe}` : ''}${f.owasp ? ` | OWASP ${f.owasp}` : ''}</div>
|
|
313
|
+
<div class="finding-desc">${escapeHtml(f.description)}</div>
|
|
314
|
+
${f.codeSnippet ? `<pre><code>${escapeHtml(f.codeSnippet)}</code></pre>` : ''}
|
|
315
|
+
${f.remediation ? `<div class="fix">Fix: ${escapeHtml(f.remediation)}</div>` : ''}
|
|
316
|
+
</div>`).join('\n')}
|
|
317
|
+
|
|
318
|
+
<div class="footer">
|
|
319
|
+
Generated by GhostPatch v1.0.0 | AI-Powered Security Scanner
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
</body>
|
|
323
|
+
</html>`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function chartSegment(severity: string, count: number, total: number): string {
|
|
327
|
+
if (!count || total === 0) return '';
|
|
328
|
+
const pct = (count / total) * 100;
|
|
329
|
+
return `<div class="chart-seg chart-${severity}" style="width:${pct}%" title="${severity}: ${count}"></div>`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function escapeHtml(str: string): string {
|
|
333
|
+
return str
|
|
334
|
+
.replace(/&/g, '&')
|
|
335
|
+
.replace(/</g, '<')
|
|
336
|
+
.replace(/>/g, '>')
|
|
337
|
+
.replace(/"/g, '"')
|
|
338
|
+
.replace(/'/g, ''');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ============================================================
|
|
342
|
+
// Report dispatcher
|
|
343
|
+
// ============================================================
|
|
344
|
+
export function generateReport(
|
|
345
|
+
result: ScanResult,
|
|
346
|
+
format: 'terminal' | 'json' | 'sarif' | 'html' = 'terminal',
|
|
347
|
+
quiet: boolean = false,
|
|
348
|
+
): string {
|
|
349
|
+
switch (format) {
|
|
350
|
+
case 'json': return reportJSON(result);
|
|
351
|
+
case 'sarif': return reportSARIF(result);
|
|
352
|
+
case 'html': return reportHTML(result);
|
|
353
|
+
case 'terminal':
|
|
354
|
+
default: return reportTerminal(result, quiet);
|
|
355
|
+
}
|
|
356
|
+
}
|