@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,310 @@
|
|
|
1
|
+
import type { Rule, Finding } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const networkRules: Rule[] = [
|
|
4
|
+
{
|
|
5
|
+
id: 'NET001',
|
|
6
|
+
name: 'HTTP Cleartext Traffic',
|
|
7
|
+
description: 'Detects usage of HTTP instead of HTTPS for network requests',
|
|
8
|
+
severity: 'high',
|
|
9
|
+
category: 'network',
|
|
10
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/capacitor.config.*'],
|
|
11
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
12
|
+
const findings: Finding[] = [];
|
|
13
|
+
const lines = content.split('\n');
|
|
14
|
+
|
|
15
|
+
// Skip test files and mocks
|
|
16
|
+
if (filePath.includes('.test.') || filePath.includes('.spec.') || filePath.includes('__mocks__')) {
|
|
17
|
+
return findings;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check for HTTP URLs (excluding localhost for development)
|
|
21
|
+
const httpPattern = /['"`]http:\/\/(?!localhost|127\.0\.0\.1|10\.|192\.168\.|172\.(?:1[6-9]|2\d|3[01])\.)[^'"`]+['"`]/g;
|
|
22
|
+
|
|
23
|
+
let match;
|
|
24
|
+
while ((match = httpPattern.exec(content)) !== null) {
|
|
25
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
26
|
+
findings.push({
|
|
27
|
+
ruleId: 'NET001',
|
|
28
|
+
ruleName: 'HTTP Cleartext Traffic',
|
|
29
|
+
severity: 'high',
|
|
30
|
+
category: 'network',
|
|
31
|
+
message: `Insecure HTTP URL detected: ${match[0].substring(0, 50)}...`,
|
|
32
|
+
filePath,
|
|
33
|
+
line: lineNum,
|
|
34
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
35
|
+
remediation: 'Use HTTPS for all network communications to prevent man-in-the-middle attacks.',
|
|
36
|
+
references: ['https://capacitor-sec.dev/docs/rules/https']
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return findings;
|
|
41
|
+
},
|
|
42
|
+
remediation: 'Always use HTTPS. Configure proper SSL/TLS certificates.'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'NET002',
|
|
46
|
+
name: 'SSL/TLS Certificate Pinning Missing',
|
|
47
|
+
description: 'Detects network requests without certificate pinning for sensitive APIs',
|
|
48
|
+
severity: 'medium',
|
|
49
|
+
category: 'network',
|
|
50
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
51
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
52
|
+
const findings: Finding[] = [];
|
|
53
|
+
const lines = content.split('\n');
|
|
54
|
+
|
|
55
|
+
// Check for API calls to sensitive endpoints without pinning context
|
|
56
|
+
const apiCallPattern = /(?:fetch|axios|http\.(?:get|post|put|delete))\s*\(\s*['"`][^'"`]*(?:api|auth|login|payment|bank)[^'"`]*['"`]/gi;
|
|
57
|
+
|
|
58
|
+
// Check if certificate pinning is configured in the file or imports
|
|
59
|
+
const hasPinning = /(?:certificatePinning|ssl-pinning|pinned|TrustKit|cert)/i.test(content);
|
|
60
|
+
|
|
61
|
+
if (!hasPinning) {
|
|
62
|
+
let match;
|
|
63
|
+
while ((match = apiCallPattern.exec(content)) !== null) {
|
|
64
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
65
|
+
findings.push({
|
|
66
|
+
ruleId: 'NET002',
|
|
67
|
+
ruleName: 'SSL/TLS Certificate Pinning Missing',
|
|
68
|
+
severity: 'medium',
|
|
69
|
+
category: 'network',
|
|
70
|
+
message: 'Sensitive API call without certificate pinning detected',
|
|
71
|
+
filePath,
|
|
72
|
+
line: lineNum,
|
|
73
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
74
|
+
remediation: 'Implement SSL certificate pinning for sensitive API endpoints using capacitor-ssl-pinning or similar.',
|
|
75
|
+
references: [
|
|
76
|
+
'https://github.com/niclas-niclas/capacitor-ssl-pinning',
|
|
77
|
+
'https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning'
|
|
78
|
+
]
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return findings;
|
|
84
|
+
},
|
|
85
|
+
remediation: 'Implement certificate pinning for APIs handling sensitive data.'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: 'NET003',
|
|
89
|
+
name: 'Capacitor Server Cleartext Enabled',
|
|
90
|
+
description: 'Detects cleartext traffic enabled in Capacitor configuration',
|
|
91
|
+
severity: 'critical',
|
|
92
|
+
category: 'network',
|
|
93
|
+
filePatterns: ['**/capacitor.config.ts', '**/capacitor.config.js', '**/capacitor.config.json'],
|
|
94
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
95
|
+
const findings: Finding[] = [];
|
|
96
|
+
const lines = content.split('\n');
|
|
97
|
+
|
|
98
|
+
// Check for cleartext: true in server config
|
|
99
|
+
const cleartextPattern = /cleartext\s*:\s*true/i;
|
|
100
|
+
|
|
101
|
+
if (cleartextPattern.test(content)) {
|
|
102
|
+
const match = content.match(cleartextPattern);
|
|
103
|
+
if (match) {
|
|
104
|
+
const lineNum = content.substring(0, content.indexOf(match[0])).split('\n').length;
|
|
105
|
+
findings.push({
|
|
106
|
+
ruleId: 'NET003',
|
|
107
|
+
ruleName: 'Capacitor Server Cleartext Enabled',
|
|
108
|
+
severity: 'critical',
|
|
109
|
+
category: 'network',
|
|
110
|
+
message: 'Cleartext traffic is enabled in Capacitor configuration',
|
|
111
|
+
filePath,
|
|
112
|
+
line: lineNum,
|
|
113
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
114
|
+
remediation: 'Remove cleartext: true from capacitor.config. Use HTTPS for all server communications.',
|
|
115
|
+
references: ['https://capacitorjs.com/docs/config']
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return findings;
|
|
121
|
+
},
|
|
122
|
+
remediation: 'Remove cleartext configuration and use HTTPS exclusively.'
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'NET004',
|
|
126
|
+
name: 'Insecure WebSocket Connection',
|
|
127
|
+
description: 'Detects usage of ws:// instead of wss:// for WebSocket connections',
|
|
128
|
+
severity: 'high',
|
|
129
|
+
category: 'network',
|
|
130
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
131
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
132
|
+
const findings: Finding[] = [];
|
|
133
|
+
const lines = content.split('\n');
|
|
134
|
+
|
|
135
|
+
// Skip localhost connections
|
|
136
|
+
const wsPattern = /['"`]ws:\/\/(?!localhost|127\.0\.0\.1)[^'"`]+['"`]/g;
|
|
137
|
+
|
|
138
|
+
let match;
|
|
139
|
+
while ((match = wsPattern.exec(content)) !== null) {
|
|
140
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
141
|
+
findings.push({
|
|
142
|
+
ruleId: 'NET004',
|
|
143
|
+
ruleName: 'Insecure WebSocket Connection',
|
|
144
|
+
severity: 'high',
|
|
145
|
+
category: 'network',
|
|
146
|
+
message: 'Insecure WebSocket (ws://) connection detected',
|
|
147
|
+
filePath,
|
|
148
|
+
line: lineNum,
|
|
149
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
150
|
+
remediation: 'Use secure WebSocket connections (wss://) for all production traffic.',
|
|
151
|
+
references: ['https://capacitor-sec.dev/docs/rules/websocket']
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return findings;
|
|
156
|
+
},
|
|
157
|
+
remediation: 'Use wss:// for all WebSocket connections.'
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'NET005',
|
|
161
|
+
name: 'CORS Wildcard Configuration',
|
|
162
|
+
description: 'Detects overly permissive CORS configuration',
|
|
163
|
+
severity: 'medium',
|
|
164
|
+
category: 'network',
|
|
165
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/capacitor.config.*'],
|
|
166
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
167
|
+
const findings: Finding[] = [];
|
|
168
|
+
const lines = content.split('\n');
|
|
169
|
+
|
|
170
|
+
// Check for wildcard in allowNavigation or CORS settings
|
|
171
|
+
const wildcardPattern = /(?:allowNavigation|Access-Control-Allow-Origin|cors).*['"]\*['"]/gi;
|
|
172
|
+
|
|
173
|
+
let match;
|
|
174
|
+
while ((match = wildcardPattern.exec(content)) !== null) {
|
|
175
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
176
|
+
findings.push({
|
|
177
|
+
ruleId: 'NET005',
|
|
178
|
+
ruleName: 'CORS Wildcard Configuration',
|
|
179
|
+
severity: 'medium',
|
|
180
|
+
category: 'network',
|
|
181
|
+
message: 'Wildcard (*) CORS configuration detected',
|
|
182
|
+
filePath,
|
|
183
|
+
line: lineNum,
|
|
184
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
185
|
+
remediation: 'Specify explicit allowed origins instead of using wildcards.',
|
|
186
|
+
references: ['https://capacitor-sec.dev/docs/rules/cors']
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return findings;
|
|
191
|
+
},
|
|
192
|
+
remediation: 'Use specific domain allowlists instead of wildcard CORS.'
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: 'NET006',
|
|
196
|
+
name: 'Insecure Deep Link Validation',
|
|
197
|
+
description: 'Detects deep link handlers without proper validation',
|
|
198
|
+
severity: 'high',
|
|
199
|
+
category: 'network',
|
|
200
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
201
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
202
|
+
const findings: Finding[] = [];
|
|
203
|
+
const lines = content.split('\n');
|
|
204
|
+
|
|
205
|
+
// Check for App.addListener for appUrlOpen without validation
|
|
206
|
+
const deepLinkPattern = /App\.addListener\s*\(\s*['"]appUrlOpen['"]/g;
|
|
207
|
+
const hasValidation = /(?:validate|verify|check|sanitize|parse).*url/i;
|
|
208
|
+
|
|
209
|
+
let match;
|
|
210
|
+
while ((match = deepLinkPattern.exec(content)) !== null) {
|
|
211
|
+
const context = content.substring(match.index, Math.min(content.length, match.index + 500));
|
|
212
|
+
if (!hasValidation.test(context)) {
|
|
213
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
214
|
+
findings.push({
|
|
215
|
+
ruleId: 'NET006',
|
|
216
|
+
ruleName: 'Insecure Deep Link Validation',
|
|
217
|
+
severity: 'high',
|
|
218
|
+
category: 'network',
|
|
219
|
+
message: 'Deep link handler without URL validation detected',
|
|
220
|
+
filePath,
|
|
221
|
+
line: lineNum,
|
|
222
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
223
|
+
remediation: 'Validate and sanitize all incoming deep link URLs before processing.',
|
|
224
|
+
references: [
|
|
225
|
+
'https://capacitorjs.com/docs/apis/app#addlistenerappurlopen-',
|
|
226
|
+
'https://capacitor-sec.dev/docs/rules/deeplinks'
|
|
227
|
+
]
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return findings;
|
|
233
|
+
},
|
|
234
|
+
remediation: 'Always validate deep link URLs against an allowlist of expected schemes and hosts.'
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 'NET007',
|
|
238
|
+
name: 'Capacitor HTTP Plugin Misuse',
|
|
239
|
+
description: 'Detects potential security issues with Capacitor HTTP plugin usage',
|
|
240
|
+
severity: 'medium',
|
|
241
|
+
category: 'network',
|
|
242
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
243
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
244
|
+
const findings: Finding[] = [];
|
|
245
|
+
const lines = content.split('\n');
|
|
246
|
+
|
|
247
|
+
// Check for CapacitorHttp without timeout
|
|
248
|
+
const httpPattern = /CapacitorHttp\.(?:get|post|put|delete|patch)\s*\(\s*\{/g;
|
|
249
|
+
const hasTimeout = /timeout\s*:/;
|
|
250
|
+
|
|
251
|
+
let match;
|
|
252
|
+
while ((match = httpPattern.exec(content)) !== null) {
|
|
253
|
+
const context = content.substring(match.index, Math.min(content.length, match.index + 300));
|
|
254
|
+
if (!hasTimeout.test(context)) {
|
|
255
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
256
|
+
findings.push({
|
|
257
|
+
ruleId: 'NET007',
|
|
258
|
+
ruleName: 'Capacitor HTTP Plugin Misuse',
|
|
259
|
+
severity: 'low',
|
|
260
|
+
category: 'network',
|
|
261
|
+
message: 'HTTP request without timeout configuration',
|
|
262
|
+
filePath,
|
|
263
|
+
line: lineNum,
|
|
264
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
265
|
+
remediation: 'Set appropriate timeout values for HTTP requests to prevent hanging connections.',
|
|
266
|
+
references: ['https://capacitorjs.com/docs/apis/http']
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return findings;
|
|
272
|
+
},
|
|
273
|
+
remediation: 'Configure appropriate timeouts and error handling for HTTP requests.'
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
id: 'NET008',
|
|
277
|
+
name: 'Sensitive Data in URL Parameters',
|
|
278
|
+
description: 'Detects sensitive data passed in URL query parameters',
|
|
279
|
+
severity: 'high',
|
|
280
|
+
category: 'network',
|
|
281
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
282
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
283
|
+
const findings: Finding[] = [];
|
|
284
|
+
const lines = content.split('\n');
|
|
285
|
+
|
|
286
|
+
// Check for sensitive params in URLs
|
|
287
|
+
const sensitiveParamPattern = /['"`][^'"`]*\?[^'"`]*(?:password|token|secret|key|auth|apikey|api_key)=[^'"`]*['"`]/gi;
|
|
288
|
+
|
|
289
|
+
let match;
|
|
290
|
+
while ((match = sensitiveParamPattern.exec(content)) !== null) {
|
|
291
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
292
|
+
findings.push({
|
|
293
|
+
ruleId: 'NET008',
|
|
294
|
+
ruleName: 'Sensitive Data in URL Parameters',
|
|
295
|
+
severity: 'high',
|
|
296
|
+
category: 'network',
|
|
297
|
+
message: 'Sensitive data found in URL query parameters',
|
|
298
|
+
filePath,
|
|
299
|
+
line: lineNum,
|
|
300
|
+
codeSnippet: lines[lineNum - 1]?.trim().substring(0, 80),
|
|
301
|
+
remediation: 'Pass sensitive data in request headers or body, not in URL parameters.',
|
|
302
|
+
references: ['https://capacitor-sec.dev/docs/rules/url-params']
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return findings;
|
|
307
|
+
},
|
|
308
|
+
remediation: 'Use request headers or body for sensitive data transmission.'
|
|
309
|
+
}
|
|
310
|
+
];
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type { Rule, Finding } from '../types.js';
|
|
2
|
+
|
|
3
|
+
// API Key and Secret patterns - 30+ patterns
|
|
4
|
+
const SECRET_PATTERNS: Array<{ name: string; pattern: RegExp; severity: 'critical' | 'high' }> = [
|
|
5
|
+
// AWS
|
|
6
|
+
{ name: 'AWS Access Key ID', pattern: /AKIA[0-9A-Z]{16}/g, severity: 'critical' },
|
|
7
|
+
{ name: 'AWS Secret Access Key', pattern: /(?:aws)?_?(?:secret)?_?(?:access)?_?key['"]?\s*[:=]\s*['"][A-Za-z0-9/+=]{40}['"]/gi, severity: 'critical' },
|
|
8
|
+
|
|
9
|
+
// Google
|
|
10
|
+
{ name: 'Google API Key', pattern: /AIza[0-9A-Za-z_-]{35}/g, severity: 'high' },
|
|
11
|
+
{ name: 'Google OAuth Client ID', pattern: /[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com/g, severity: 'high' },
|
|
12
|
+
{ name: 'Firebase API Key', pattern: /(?:firebase|fcm).*['"][A-Za-z0-9_-]{20,}['"]/gi, severity: 'high' },
|
|
13
|
+
|
|
14
|
+
// Stripe
|
|
15
|
+
{ name: 'Stripe Live Secret Key', pattern: /sk_live_[0-9a-zA-Z]{24,}/g, severity: 'critical' },
|
|
16
|
+
{ name: 'Stripe Test Secret Key', pattern: /sk_test_[0-9a-zA-Z]{24,}/g, severity: 'medium' as any },
|
|
17
|
+
{ name: 'Stripe Publishable Key', pattern: /pk_(?:live|test)_[0-9a-zA-Z]{24,}/g, severity: 'high' },
|
|
18
|
+
|
|
19
|
+
// GitHub/GitLab
|
|
20
|
+
{ name: 'GitHub Token', pattern: /(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/g, severity: 'critical' },
|
|
21
|
+
{ name: 'GitHub OAuth', pattern: /github.*['"][0-9a-zA-Z]{35,40}['"]/gi, severity: 'critical' },
|
|
22
|
+
{ name: 'GitLab Token', pattern: /glpat-[A-Za-z0-9_-]{20,}/g, severity: 'critical' },
|
|
23
|
+
|
|
24
|
+
// Slack
|
|
25
|
+
{ name: 'Slack Token', pattern: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*/g, severity: 'critical' },
|
|
26
|
+
{ name: 'Slack Webhook', pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{8}\/B[A-Z0-9]{8,}\/[A-Za-z0-9]{24}/g, severity: 'high' },
|
|
27
|
+
|
|
28
|
+
// Twilio
|
|
29
|
+
{ name: 'Twilio API Key', pattern: /SK[0-9a-fA-F]{32}/g, severity: 'high' },
|
|
30
|
+
{ name: 'Twilio Account SID', pattern: /AC[0-9a-fA-F]{32}/g, severity: 'high' },
|
|
31
|
+
|
|
32
|
+
// SendGrid
|
|
33
|
+
{ name: 'SendGrid API Key', pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g, severity: 'critical' },
|
|
34
|
+
|
|
35
|
+
// Mailgun
|
|
36
|
+
{ name: 'Mailgun API Key', pattern: /key-[0-9a-zA-Z]{32}/g, severity: 'high' },
|
|
37
|
+
|
|
38
|
+
// DigitalOcean
|
|
39
|
+
{ name: 'DigitalOcean Token', pattern: /dop_v1_[a-f0-9]{64}/g, severity: 'critical' },
|
|
40
|
+
{ name: 'DigitalOcean OAuth', pattern: /doo_v1_[a-f0-9]{64}/g, severity: 'critical' },
|
|
41
|
+
|
|
42
|
+
// Heroku
|
|
43
|
+
{ name: 'Heroku API Key', pattern: /heroku.*['"][0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}['"]/gi, severity: 'critical' },
|
|
44
|
+
|
|
45
|
+
// NPM
|
|
46
|
+
{ name: 'NPM Token', pattern: /npm_[A-Za-z0-9]{36}/g, severity: 'critical' },
|
|
47
|
+
|
|
48
|
+
// Supabase
|
|
49
|
+
{ name: 'Supabase Service Key', pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g, severity: 'high' },
|
|
50
|
+
|
|
51
|
+
// Private Keys
|
|
52
|
+
{ name: 'RSA Private Key', pattern: /-----BEGIN RSA PRIVATE KEY-----/g, severity: 'critical' },
|
|
53
|
+
{ name: 'SSH Private Key', pattern: /-----BEGIN (?:DSA|EC|OPENSSH) PRIVATE KEY-----/g, severity: 'critical' },
|
|
54
|
+
{ name: 'PGP Private Key', pattern: /-----BEGIN PGP PRIVATE KEY BLOCK-----/g, severity: 'critical' },
|
|
55
|
+
|
|
56
|
+
// Generic patterns
|
|
57
|
+
{ name: 'Generic API Key', pattern: /(?:api[_-]?key|apikey|api[_-]?secret)['"]?\s*[:=]\s*['"][A-Za-z0-9_-]{20,}['"]/gi, severity: 'high' },
|
|
58
|
+
{ name: 'Generic Secret', pattern: /(?:secret|password|passwd|pwd)['"]?\s*[:=]\s*['"][^'"]{8,}['"]/gi, severity: 'high' },
|
|
59
|
+
{ name: 'Generic Token', pattern: /(?:access[_-]?token|auth[_-]?token|bearer)['"]?\s*[:=]\s*['"][A-Za-z0-9_.-]{20,}['"]/gi, severity: 'high' },
|
|
60
|
+
|
|
61
|
+
// Database URLs
|
|
62
|
+
{ name: 'Database URL with Credentials', pattern: /(?:mongodb|postgres|mysql|redis):\/\/[^:]+:[^@]+@[^/]+/gi, severity: 'critical' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export const secretsRules: Rule[] = [
|
|
66
|
+
{
|
|
67
|
+
id: 'SEC001',
|
|
68
|
+
name: 'Hardcoded API Keys & Secrets',
|
|
69
|
+
description: 'Detects hardcoded API keys, tokens, and secrets in source code',
|
|
70
|
+
severity: 'critical',
|
|
71
|
+
category: 'secrets',
|
|
72
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.json', '**/*.env*'],
|
|
73
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
74
|
+
const findings: Finding[] = [];
|
|
75
|
+
const lines = content.split('\n');
|
|
76
|
+
|
|
77
|
+
// Skip node_modules and common false positives
|
|
78
|
+
if (filePath.includes('node_modules') || filePath.includes('.lock')) {
|
|
79
|
+
return findings;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const secretPattern of SECRET_PATTERNS) {
|
|
83
|
+
let match;
|
|
84
|
+
const pattern = new RegExp(secretPattern.pattern.source, secretPattern.pattern.flags);
|
|
85
|
+
|
|
86
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
87
|
+
// Find line number
|
|
88
|
+
let lineNum = 1;
|
|
89
|
+
let charCount = 0;
|
|
90
|
+
for (let i = 0; i < lines.length; i++) {
|
|
91
|
+
charCount += lines[i].length + 1;
|
|
92
|
+
if (charCount > match.index) {
|
|
93
|
+
lineNum = i + 1;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Mask the secret for display
|
|
99
|
+
const maskedValue = match[0].substring(0, 10) + '***REDACTED***';
|
|
100
|
+
|
|
101
|
+
findings.push({
|
|
102
|
+
ruleId: 'SEC001',
|
|
103
|
+
ruleName: 'Hardcoded API Keys & Secrets',
|
|
104
|
+
severity: secretPattern.severity,
|
|
105
|
+
category: 'secrets',
|
|
106
|
+
message: `Found ${secretPattern.name}: ${maskedValue}`,
|
|
107
|
+
filePath,
|
|
108
|
+
line: lineNum,
|
|
109
|
+
codeSnippet: lines[lineNum - 1]?.trim().substring(0, 100),
|
|
110
|
+
remediation: 'Move secrets to environment variables or a secure secrets manager. Never commit secrets to source control.',
|
|
111
|
+
references: [
|
|
112
|
+
'https://capacitor-sec.dev/docs/rules/secrets',
|
|
113
|
+
'https://owasp.org/www-project-mobile-top-10/'
|
|
114
|
+
]
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return findings;
|
|
120
|
+
},
|
|
121
|
+
remediation: 'Use environment variables or a secure secrets manager. Consider using @capgo/capacitor-social-login for OAuth flows.',
|
|
122
|
+
references: ['https://owasp.org/www-project-mobile-top-10/']
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'SEC002',
|
|
126
|
+
name: 'Exposed .env File',
|
|
127
|
+
description: 'Detects .env files that may contain sensitive configuration',
|
|
128
|
+
severity: 'critical',
|
|
129
|
+
category: 'secrets',
|
|
130
|
+
filePatterns: ['**/.env', '**/.env.*', '!**/.env.example', '!**/.env.template'],
|
|
131
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
132
|
+
if (filePath.includes('.example') || filePath.includes('.template')) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const findings: Finding[] = [];
|
|
137
|
+
const lines = content.split('\n');
|
|
138
|
+
|
|
139
|
+
lines.forEach((line, index) => {
|
|
140
|
+
if (line.trim() && !line.startsWith('#') && line.includes('=')) {
|
|
141
|
+
const [key] = line.split('=');
|
|
142
|
+
if (key && /(?:key|secret|password|token|auth|credential)/i.test(key)) {
|
|
143
|
+
findings.push({
|
|
144
|
+
ruleId: 'SEC002',
|
|
145
|
+
ruleName: 'Exposed .env File',
|
|
146
|
+
severity: 'critical',
|
|
147
|
+
category: 'secrets',
|
|
148
|
+
message: `Sensitive variable "${key.trim()}" found in .env file`,
|
|
149
|
+
filePath,
|
|
150
|
+
line: index + 1,
|
|
151
|
+
codeSnippet: `${key.trim()}=***REDACTED***`,
|
|
152
|
+
remediation: 'Ensure .env files are in .gitignore and never committed to version control.',
|
|
153
|
+
references: ['https://capacitor-sec.dev/docs/rules/env-files']
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return findings;
|
|
160
|
+
},
|
|
161
|
+
remediation: 'Add .env files to .gitignore. Use .env.example for documentation with placeholder values.'
|
|
162
|
+
}
|
|
163
|
+
];
|