@capgo/capgo-sec 1.0.4
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/.github/workflows/bump_version.yml +83 -0
- package/.github/workflows/ci.yml +44 -0
- package/AGENTS.md +125 -0
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/bun.lock +146 -0
- package/dist/cli/index.js +13248 -0
- package/dist/index.js +8273 -0
- package/package.json +53 -0
- package/renovate.json +39 -0
- package/src/cli/index.ts +183 -0
- package/src/index.ts +31 -0
- package/src/rules/android.ts +392 -0
- package/src/rules/authentication.ts +261 -0
- package/src/rules/capacitor.ts +435 -0
- package/src/rules/cryptography.ts +190 -0
- package/src/rules/index.ts +56 -0
- package/src/rules/ios.ts +326 -0
- package/src/rules/logging.ts +218 -0
- package/src/rules/network.ts +310 -0
- package/src/rules/secrets.ts +163 -0
- package/src/rules/storage.ts +241 -0
- package/src/rules/webview.ts +232 -0
- package/src/scanners/engine.ts +233 -0
- package/src/types.ts +96 -0
- package/src/utils/reporter.ts +209 -0
- package/test/rules.test.ts +235 -0
- package/test/scanner.test.ts +292 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import fg from 'fast-glob';
|
|
2
|
+
import { allRules, ruleCount } from '../rules/index.js';
|
|
3
|
+
import type { Rule, Finding, ScanResult, ScanOptions, RuleCategory, Severity } from '../types.js';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_EXCLUDE = [
|
|
6
|
+
'**/node_modules/**',
|
|
7
|
+
'**/dist/**',
|
|
8
|
+
'**/build/**',
|
|
9
|
+
'**/.git/**',
|
|
10
|
+
'**/coverage/**',
|
|
11
|
+
'**/*.min.js',
|
|
12
|
+
'**/*.bundle.js',
|
|
13
|
+
'**/vendor/**',
|
|
14
|
+
'**/.next/**',
|
|
15
|
+
'**/.nuxt/**',
|
|
16
|
+
'**/android/app/build/**',
|
|
17
|
+
'**/ios/Pods/**',
|
|
18
|
+
'**/ios/build/**'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export class SecurityScanner {
|
|
22
|
+
private rules: Rule[];
|
|
23
|
+
private options: ScanOptions;
|
|
24
|
+
|
|
25
|
+
constructor(options: ScanOptions) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.rules = this.filterRules(allRules);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private filterRules(rules: Rule[]): Rule[] {
|
|
31
|
+
let filtered = rules;
|
|
32
|
+
|
|
33
|
+
// Filter by severity
|
|
34
|
+
if (this.options.severity) {
|
|
35
|
+
const severityOrder: Severity[] = ['critical', 'high', 'medium', 'low', 'info'];
|
|
36
|
+
const minIndex = severityOrder.indexOf(this.options.severity);
|
|
37
|
+
filtered = filtered.filter(rule => {
|
|
38
|
+
const ruleIndex = severityOrder.indexOf(rule.severity);
|
|
39
|
+
return ruleIndex <= minIndex;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Filter by category
|
|
44
|
+
if (this.options.categories && this.options.categories.length > 0) {
|
|
45
|
+
filtered = filtered.filter(rule => this.options.categories!.includes(rule.category));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return filtered;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async scan(): Promise<ScanResult> {
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
const findings: Finding[] = [];
|
|
54
|
+
|
|
55
|
+
// Get all files to scan
|
|
56
|
+
const files = await this.getFiles();
|
|
57
|
+
|
|
58
|
+
// Process each file
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const fileFindings = await this.scanFile(file);
|
|
61
|
+
findings.push(...fileFindings);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const duration = Date.now() - startTime;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
projectPath: this.options.path,
|
|
68
|
+
timestamp: new Date().toISOString(),
|
|
69
|
+
duration,
|
|
70
|
+
filesScanned: files.length,
|
|
71
|
+
findings: this.sortFindings(findings),
|
|
72
|
+
summary: this.generateSummary(findings)
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private async getFiles(): Promise<string[]> {
|
|
77
|
+
const excludePatterns = [...DEFAULT_EXCLUDE, ...(this.options.exclude || [])];
|
|
78
|
+
|
|
79
|
+
// Collect all unique file patterns from rules
|
|
80
|
+
const patterns = new Set<string>();
|
|
81
|
+
for (const rule of this.rules) {
|
|
82
|
+
if (rule.filePatterns) {
|
|
83
|
+
rule.filePatterns.forEach(p => patterns.add(p));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// If no patterns, use common source files
|
|
88
|
+
if (patterns.size === 0) {
|
|
89
|
+
patterns.add('**/*.ts');
|
|
90
|
+
patterns.add('**/*.tsx');
|
|
91
|
+
patterns.add('**/*.js');
|
|
92
|
+
patterns.add('**/*.jsx');
|
|
93
|
+
patterns.add('**/*.json');
|
|
94
|
+
patterns.add('**/*.html');
|
|
95
|
+
patterns.add('**/AndroidManifest.xml');
|
|
96
|
+
patterns.add('**/Info.plist');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const files = await fg(Array.from(patterns), {
|
|
100
|
+
cwd: this.options.path,
|
|
101
|
+
ignore: excludePatterns,
|
|
102
|
+
absolute: true,
|
|
103
|
+
onlyFiles: true
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
return files;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private async scanFile(filePath: string): Promise<Finding[]> {
|
|
110
|
+
const findings: Finding[] = [];
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const content = await Bun.file(filePath).text();
|
|
114
|
+
|
|
115
|
+
// Get rules that match this file
|
|
116
|
+
const applicableRules = this.rules.filter(rule => {
|
|
117
|
+
if (!rule.filePatterns) return true;
|
|
118
|
+
return rule.filePatterns.some(pattern => {
|
|
119
|
+
// Convert glob pattern to regex for matching
|
|
120
|
+
// First escape dots, then handle glob patterns
|
|
121
|
+
const regexPattern = pattern
|
|
122
|
+
.replace(/\./g, '\\.')
|
|
123
|
+
.replace(/\*\*/g, '.*')
|
|
124
|
+
.replace(/(?<!\.)(\*)/g, '[^/]*');
|
|
125
|
+
return new RegExp(regexPattern).test(filePath);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Apply each rule
|
|
130
|
+
for (const rule of applicableRules) {
|
|
131
|
+
if (rule.check) {
|
|
132
|
+
const ruleFindings = rule.check(content, filePath);
|
|
133
|
+
findings.push(...ruleFindings);
|
|
134
|
+
} else if (rule.patterns) {
|
|
135
|
+
// Simple pattern matching
|
|
136
|
+
for (const pattern of rule.patterns) {
|
|
137
|
+
let match;
|
|
138
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
139
|
+
const lines = content.split('\n');
|
|
140
|
+
|
|
141
|
+
while ((match = regex.exec(content)) !== null) {
|
|
142
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
143
|
+
findings.push({
|
|
144
|
+
ruleId: rule.id,
|
|
145
|
+
ruleName: rule.name,
|
|
146
|
+
severity: rule.severity,
|
|
147
|
+
category: rule.category,
|
|
148
|
+
message: rule.description,
|
|
149
|
+
filePath,
|
|
150
|
+
line: lineNum,
|
|
151
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
152
|
+
remediation: rule.remediation,
|
|
153
|
+
references: rule.references
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (this.options.verbose) {
|
|
161
|
+
console.error(`Error scanning ${filePath}:`, error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return findings;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private sortFindings(findings: Finding[]): Finding[] {
|
|
169
|
+
const severityOrder: Record<Severity, number> = {
|
|
170
|
+
critical: 0,
|
|
171
|
+
high: 1,
|
|
172
|
+
medium: 2,
|
|
173
|
+
low: 3,
|
|
174
|
+
info: 4
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
return findings.sort((a, b) => {
|
|
178
|
+
const severityDiff = severityOrder[a.severity] - severityOrder[b.severity];
|
|
179
|
+
if (severityDiff !== 0) return severityDiff;
|
|
180
|
+
return a.filePath.localeCompare(b.filePath);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private generateSummary(findings: Finding[]) {
|
|
185
|
+
const byCategory: Record<RuleCategory, number> = {
|
|
186
|
+
storage: 0,
|
|
187
|
+
network: 0,
|
|
188
|
+
authentication: 0,
|
|
189
|
+
secrets: 0,
|
|
190
|
+
cryptography: 0,
|
|
191
|
+
logging: 0,
|
|
192
|
+
capacitor: 0,
|
|
193
|
+
debug: 0,
|
|
194
|
+
android: 0,
|
|
195
|
+
ios: 0,
|
|
196
|
+
config: 0,
|
|
197
|
+
webview: 0,
|
|
198
|
+
permissions: 0
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
let critical = 0;
|
|
202
|
+
let high = 0;
|
|
203
|
+
let medium = 0;
|
|
204
|
+
let low = 0;
|
|
205
|
+
let info = 0;
|
|
206
|
+
|
|
207
|
+
for (const finding of findings) {
|
|
208
|
+
byCategory[finding.category]++;
|
|
209
|
+
|
|
210
|
+
switch (finding.severity) {
|
|
211
|
+
case 'critical': critical++; break;
|
|
212
|
+
case 'high': high++; break;
|
|
213
|
+
case 'medium': medium++; break;
|
|
214
|
+
case 'low': low++; break;
|
|
215
|
+
case 'info': info++; break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
total: findings.length,
|
|
221
|
+
critical,
|
|
222
|
+
high,
|
|
223
|
+
medium,
|
|
224
|
+
low,
|
|
225
|
+
info,
|
|
226
|
+
byCategory
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
static getRuleCount(): number {
|
|
231
|
+
return ruleCount;
|
|
232
|
+
}
|
|
233
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
export type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info';
|
|
2
|
+
|
|
3
|
+
export type RuleCategory =
|
|
4
|
+
| 'storage'
|
|
5
|
+
| 'network'
|
|
6
|
+
| 'authentication'
|
|
7
|
+
| 'secrets'
|
|
8
|
+
| 'cryptography'
|
|
9
|
+
| 'logging'
|
|
10
|
+
| 'capacitor'
|
|
11
|
+
| 'debug'
|
|
12
|
+
| 'android'
|
|
13
|
+
| 'ios'
|
|
14
|
+
| 'config'
|
|
15
|
+
| 'webview'
|
|
16
|
+
| 'permissions';
|
|
17
|
+
|
|
18
|
+
export interface Rule {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
severity: Severity;
|
|
23
|
+
category: RuleCategory;
|
|
24
|
+
patterns?: RegExp[];
|
|
25
|
+
filePatterns?: string[];
|
|
26
|
+
check?: (content: string, filePath: string, ast?: any) => Finding[];
|
|
27
|
+
remediation: string;
|
|
28
|
+
references?: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Finding {
|
|
32
|
+
ruleId: string;
|
|
33
|
+
ruleName: string;
|
|
34
|
+
severity: Severity;
|
|
35
|
+
category: RuleCategory;
|
|
36
|
+
message: string;
|
|
37
|
+
filePath: string;
|
|
38
|
+
line?: number;
|
|
39
|
+
column?: number;
|
|
40
|
+
codeSnippet?: string;
|
|
41
|
+
remediation: string;
|
|
42
|
+
references?: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ScanResult {
|
|
46
|
+
projectPath: string;
|
|
47
|
+
timestamp: string;
|
|
48
|
+
duration: number;
|
|
49
|
+
filesScanned: number;
|
|
50
|
+
findings: Finding[];
|
|
51
|
+
summary: {
|
|
52
|
+
total: number;
|
|
53
|
+
critical: number;
|
|
54
|
+
high: number;
|
|
55
|
+
medium: number;
|
|
56
|
+
low: number;
|
|
57
|
+
info: number;
|
|
58
|
+
byCategory: Record<RuleCategory, number>;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ScanOptions {
|
|
63
|
+
path: string;
|
|
64
|
+
output?: 'cli' | 'json' | 'html';
|
|
65
|
+
outputFile?: string;
|
|
66
|
+
severity?: Severity;
|
|
67
|
+
categories?: RuleCategory[];
|
|
68
|
+
exclude?: string[];
|
|
69
|
+
changedOnly?: boolean;
|
|
70
|
+
ci?: boolean;
|
|
71
|
+
verbose?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface CapacitorConfig {
|
|
75
|
+
appId?: string;
|
|
76
|
+
appName?: string;
|
|
77
|
+
webDir?: string;
|
|
78
|
+
plugins?: Record<string, any>;
|
|
79
|
+
android?: {
|
|
80
|
+
allowMixedContent?: boolean;
|
|
81
|
+
captureInput?: boolean;
|
|
82
|
+
webContentsDebuggingEnabled?: boolean;
|
|
83
|
+
loggingBehavior?: string;
|
|
84
|
+
};
|
|
85
|
+
ios?: {
|
|
86
|
+
allowsLinkPreview?: boolean;
|
|
87
|
+
scrollEnabled?: boolean;
|
|
88
|
+
webContentsDebuggingEnabled?: boolean;
|
|
89
|
+
loggingBehavior?: string;
|
|
90
|
+
};
|
|
91
|
+
server?: {
|
|
92
|
+
url?: string;
|
|
93
|
+
cleartext?: boolean;
|
|
94
|
+
allowNavigation?: string[];
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { ScanResult, Finding, Severity } from '../types.js';
|
|
2
|
+
|
|
3
|
+
const SEVERITY_COLORS = {
|
|
4
|
+
critical: '\x1b[41m\x1b[37m', // Red background, white text
|
|
5
|
+
high: '\x1b[31m', // Red
|
|
6
|
+
medium: '\x1b[33m', // Yellow
|
|
7
|
+
low: '\x1b[36m', // Cyan
|
|
8
|
+
info: '\x1b[90m' // Gray
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const RESET = '\x1b[0m';
|
|
12
|
+
const BOLD = '\x1b[1m';
|
|
13
|
+
const DIM = '\x1b[2m';
|
|
14
|
+
|
|
15
|
+
export function formatCliReport(result: ScanResult): string {
|
|
16
|
+
const lines: string[] = [];
|
|
17
|
+
|
|
18
|
+
// Header
|
|
19
|
+
lines.push('');
|
|
20
|
+
lines.push(`${BOLD}╔══════════════════════════════════════════════════════════════╗${RESET}`);
|
|
21
|
+
lines.push(`${BOLD}║ CAPSEC Security Scan Report ║${RESET}`);
|
|
22
|
+
lines.push(`${BOLD}╚══════════════════════════════════════════════════════════════╝${RESET}`);
|
|
23
|
+
lines.push('');
|
|
24
|
+
|
|
25
|
+
// Summary
|
|
26
|
+
lines.push(`${BOLD}Summary${RESET}`);
|
|
27
|
+
lines.push(`${'─'.repeat(60)}`);
|
|
28
|
+
lines.push(`Project: ${result.projectPath}`);
|
|
29
|
+
lines.push(`Scanned: ${result.filesScanned} files in ${result.duration}ms`);
|
|
30
|
+
lines.push(`Time: ${result.timestamp}`);
|
|
31
|
+
lines.push('');
|
|
32
|
+
|
|
33
|
+
// Severity breakdown
|
|
34
|
+
lines.push(`${BOLD}Findings by Severity${RESET}`);
|
|
35
|
+
lines.push(`${'─'.repeat(60)}`);
|
|
36
|
+
|
|
37
|
+
if (result.summary.critical > 0) {
|
|
38
|
+
lines.push(`${SEVERITY_COLORS.critical} CRITICAL ${RESET} ${result.summary.critical}`);
|
|
39
|
+
}
|
|
40
|
+
if (result.summary.high > 0) {
|
|
41
|
+
lines.push(`${SEVERITY_COLORS.high}● HIGH${RESET} ${result.summary.high}`);
|
|
42
|
+
}
|
|
43
|
+
if (result.summary.medium > 0) {
|
|
44
|
+
lines.push(`${SEVERITY_COLORS.medium}● MEDIUM${RESET} ${result.summary.medium}`);
|
|
45
|
+
}
|
|
46
|
+
if (result.summary.low > 0) {
|
|
47
|
+
lines.push(`${SEVERITY_COLORS.low}● LOW${RESET} ${result.summary.low}`);
|
|
48
|
+
}
|
|
49
|
+
if (result.summary.info > 0) {
|
|
50
|
+
lines.push(`${SEVERITY_COLORS.info}● INFO${RESET} ${result.summary.info}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lines.push('');
|
|
54
|
+
lines.push(`${BOLD}Total: ${result.summary.total} findings${RESET}`);
|
|
55
|
+
lines.push('');
|
|
56
|
+
|
|
57
|
+
// Findings
|
|
58
|
+
if (result.findings.length > 0) {
|
|
59
|
+
lines.push(`${BOLD}Detailed Findings${RESET}`);
|
|
60
|
+
lines.push(`${'═'.repeat(60)}`);
|
|
61
|
+
lines.push('');
|
|
62
|
+
|
|
63
|
+
let currentSeverity: Severity | null = null;
|
|
64
|
+
|
|
65
|
+
for (const finding of result.findings) {
|
|
66
|
+
if (finding.severity !== currentSeverity) {
|
|
67
|
+
currentSeverity = finding.severity;
|
|
68
|
+
lines.push(`${SEVERITY_COLORS[finding.severity]}${BOLD}── ${finding.severity.toUpperCase()} ──${RESET}`);
|
|
69
|
+
lines.push('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
lines.push(formatFinding(finding));
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
lines.push(`${BOLD}\x1b[32m✓ No security issues found!${RESET}`);
|
|
76
|
+
lines.push('');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Footer
|
|
80
|
+
lines.push(`${'─'.repeat(60)}`);
|
|
81
|
+
lines.push(`${DIM}Powered by capsec - https://capacitor-sec.dev${RESET}`);
|
|
82
|
+
lines.push('');
|
|
83
|
+
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatFinding(finding: Finding): string {
|
|
88
|
+
const lines: string[] = [];
|
|
89
|
+
const severityColor = SEVERITY_COLORS[finding.severity];
|
|
90
|
+
|
|
91
|
+
lines.push(`${severityColor}[${finding.ruleId}]${RESET} ${BOLD}${finding.ruleName}${RESET}`);
|
|
92
|
+
lines.push(` ${DIM}File:${RESET} ${finding.filePath}${finding.line ? `:${finding.line}` : ''}`);
|
|
93
|
+
lines.push(` ${DIM}Issue:${RESET} ${finding.message}`);
|
|
94
|
+
|
|
95
|
+
if (finding.codeSnippet) {
|
|
96
|
+
lines.push(` ${DIM}Code:${RESET} ${finding.codeSnippet.substring(0, 80)}${finding.codeSnippet.length > 80 ? '...' : ''}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
lines.push(` ${DIM}Fix:${RESET} ${finding.remediation}`);
|
|
100
|
+
|
|
101
|
+
if (finding.references && finding.references.length > 0) {
|
|
102
|
+
lines.push(` ${DIM}Refs:${RESET} ${finding.references[0]}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
lines.push('');
|
|
106
|
+
|
|
107
|
+
return lines.join('\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function formatJsonReport(result: ScanResult): string {
|
|
111
|
+
return JSON.stringify(result, null, 2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function formatHtmlReport(result: ScanResult): string {
|
|
115
|
+
const severityClasses: Record<Severity, string> = {
|
|
116
|
+
critical: 'bg-red-600 text-white',
|
|
117
|
+
high: 'bg-red-500 text-white',
|
|
118
|
+
medium: 'bg-yellow-500 text-black',
|
|
119
|
+
low: 'bg-blue-500 text-white',
|
|
120
|
+
info: 'bg-gray-500 text-white'
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const findingsHtml = result.findings.map(finding => `
|
|
124
|
+
<div class="finding border-l-4 ${finding.severity === 'critical' ? 'border-red-600' : finding.severity === 'high' ? 'border-red-500' : finding.severity === 'medium' ? 'border-yellow-500' : finding.severity === 'low' ? 'border-blue-500' : 'border-gray-500'} bg-white shadow-md rounded-r-lg p-4 mb-4">
|
|
125
|
+
<div class="flex items-center gap-2 mb-2">
|
|
126
|
+
<span class="px-2 py-1 rounded text-xs font-bold ${severityClasses[finding.severity]}">${finding.severity.toUpperCase()}</span>
|
|
127
|
+
<span class="text-gray-500 text-sm">${finding.ruleId}</span>
|
|
128
|
+
<span class="font-semibold">${finding.ruleName}</span>
|
|
129
|
+
</div>
|
|
130
|
+
<p class="text-gray-700 mb-2">${finding.message}</p>
|
|
131
|
+
<p class="text-sm text-gray-500 mb-2">
|
|
132
|
+
<span class="font-mono">${finding.filePath}${finding.line ? `:${finding.line}` : ''}</span>
|
|
133
|
+
</p>
|
|
134
|
+
${finding.codeSnippet ? `<pre class="bg-gray-100 p-2 rounded text-sm overflow-x-auto mb-2"><code>${escapeHtml(finding.codeSnippet)}</code></pre>` : ''}
|
|
135
|
+
<p class="text-sm"><strong>Remediation:</strong> ${finding.remediation}</p>
|
|
136
|
+
${finding.references && finding.references.length > 0 ? `<p class="text-sm text-blue-600 mt-2"><a href="${finding.references[0]}" target="_blank" rel="noopener">Learn more →</a></p>` : ''}
|
|
137
|
+
</div>
|
|
138
|
+
`).join('');
|
|
139
|
+
|
|
140
|
+
return `<!DOCTYPE html>
|
|
141
|
+
<html lang="en">
|
|
142
|
+
<head>
|
|
143
|
+
<meta charset="UTF-8">
|
|
144
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
145
|
+
<title>Capsec Security Report</title>
|
|
146
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
147
|
+
<style>
|
|
148
|
+
body { font-family: system-ui, -apple-system, sans-serif; }
|
|
149
|
+
</style>
|
|
150
|
+
</head>
|
|
151
|
+
<body class="bg-gray-100 min-h-screen">
|
|
152
|
+
<div class="container mx-auto px-4 py-8 max-w-4xl">
|
|
153
|
+
<header class="bg-gradient-to-r from-purple-600 to-indigo-600 text-white rounded-lg shadow-lg p-8 mb-8">
|
|
154
|
+
<h1 class="text-3xl font-bold mb-2">🔒 Capsec Security Report</h1>
|
|
155
|
+
<p class="opacity-80">Capacitor Security Scanner</p>
|
|
156
|
+
</header>
|
|
157
|
+
|
|
158
|
+
<section class="bg-white rounded-lg shadow-md p-6 mb-8">
|
|
159
|
+
<h2 class="text-xl font-bold mb-4">Summary</h2>
|
|
160
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
|
161
|
+
<div class="text-center p-4 bg-gray-50 rounded">
|
|
162
|
+
<div class="text-2xl font-bold">${result.filesScanned}</div>
|
|
163
|
+
<div class="text-gray-500 text-sm">Files Scanned</div>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="text-center p-4 bg-gray-50 rounded">
|
|
166
|
+
<div class="text-2xl font-bold">${result.summary.total}</div>
|
|
167
|
+
<div class="text-gray-500 text-sm">Total Findings</div>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="text-center p-4 bg-gray-50 rounded">
|
|
170
|
+
<div class="text-2xl font-bold">${result.duration}ms</div>
|
|
171
|
+
<div class="text-gray-500 text-sm">Scan Duration</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div class="text-center p-4 bg-gray-50 rounded">
|
|
174
|
+
<div class="text-2xl font-bold">${new Date(result.timestamp).toLocaleDateString()}</div>
|
|
175
|
+
<div class="text-gray-500 text-sm">Scan Date</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div class="flex flex-wrap gap-2">
|
|
180
|
+
${result.summary.critical > 0 ? `<span class="px-3 py-1 rounded-full text-sm font-bold bg-red-600 text-white">${result.summary.critical} Critical</span>` : ''}
|
|
181
|
+
${result.summary.high > 0 ? `<span class="px-3 py-1 rounded-full text-sm font-bold bg-red-500 text-white">${result.summary.high} High</span>` : ''}
|
|
182
|
+
${result.summary.medium > 0 ? `<span class="px-3 py-1 rounded-full text-sm font-bold bg-yellow-500 text-black">${result.summary.medium} Medium</span>` : ''}
|
|
183
|
+
${result.summary.low > 0 ? `<span class="px-3 py-1 rounded-full text-sm font-bold bg-blue-500 text-white">${result.summary.low} Low</span>` : ''}
|
|
184
|
+
${result.summary.info > 0 ? `<span class="px-3 py-1 rounded-full text-sm font-bold bg-gray-500 text-white">${result.summary.info} Info</span>` : ''}
|
|
185
|
+
</div>
|
|
186
|
+
</section>
|
|
187
|
+
|
|
188
|
+
<section>
|
|
189
|
+
<h2 class="text-xl font-bold mb-4">Findings</h2>
|
|
190
|
+
${result.findings.length > 0 ? findingsHtml : '<p class="text-green-600 text-lg font-semibold">✓ No security issues found!</p>'}
|
|
191
|
+
</section>
|
|
192
|
+
|
|
193
|
+
<footer class="text-center text-gray-500 text-sm mt-8 pt-8 border-t">
|
|
194
|
+
<p>Generated by <a href="https://capacitor-sec.dev" class="text-purple-600 hover:underline">capsec</a> - Capacitor Security Scanner</p>
|
|
195
|
+
<p class="mt-2">Project: ${result.projectPath}</p>
|
|
196
|
+
</footer>
|
|
197
|
+
</div>
|
|
198
|
+
</body>
|
|
199
|
+
</html>`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function escapeHtml(text: string): string {
|
|
203
|
+
return text
|
|
204
|
+
.replace(/&/g, '&')
|
|
205
|
+
.replace(/</g, '<')
|
|
206
|
+
.replace(/>/g, '>')
|
|
207
|
+
.replace(/"/g, '"')
|
|
208
|
+
.replace(/'/g, ''');
|
|
209
|
+
}
|