@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,392 @@
|
|
|
1
|
+
import type { Rule, Finding } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const androidRules: Rule[] = [
|
|
4
|
+
{
|
|
5
|
+
id: 'AND001',
|
|
6
|
+
name: 'Android Cleartext Traffic Allowed',
|
|
7
|
+
description: 'Detects usesCleartextTraffic enabled in Android manifest',
|
|
8
|
+
severity: 'critical',
|
|
9
|
+
category: 'android',
|
|
10
|
+
filePatterns: ['**/AndroidManifest.xml', '**/network_security_config.xml'],
|
|
11
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
12
|
+
const findings: Finding[] = [];
|
|
13
|
+
const lines = content.split('\n');
|
|
14
|
+
|
|
15
|
+
if (filePath.includes('AndroidManifest.xml')) {
|
|
16
|
+
const cleartextPattern = /usesCleartextTraffic\s*=\s*["']true["']/i;
|
|
17
|
+
if (cleartextPattern.test(content)) {
|
|
18
|
+
const match = content.match(cleartextPattern);
|
|
19
|
+
if (match) {
|
|
20
|
+
const lineNum = content.substring(0, content.indexOf(match[0])).split('\n').length;
|
|
21
|
+
findings.push({
|
|
22
|
+
ruleId: 'AND001',
|
|
23
|
+
ruleName: 'Android Cleartext Traffic Allowed',
|
|
24
|
+
severity: 'critical',
|
|
25
|
+
category: 'android',
|
|
26
|
+
message: 'usesCleartextTraffic="true" allows unencrypted HTTP traffic',
|
|
27
|
+
filePath,
|
|
28
|
+
line: lineNum,
|
|
29
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
30
|
+
remediation: 'Set usesCleartextTraffic="false" and use HTTPS exclusively.',
|
|
31
|
+
references: ['https://developer.android.com/guide/topics/manifest/application-element#usesCleartextTraffic']
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (filePath.includes('network_security_config.xml')) {
|
|
38
|
+
const cleartextDomainPattern = /cleartextTrafficPermitted\s*=\s*["']true["']/i;
|
|
39
|
+
if (cleartextDomainPattern.test(content)) {
|
|
40
|
+
const match = content.match(cleartextDomainPattern);
|
|
41
|
+
if (match) {
|
|
42
|
+
const lineNum = content.substring(0, content.indexOf(match[0])).split('\n').length;
|
|
43
|
+
findings.push({
|
|
44
|
+
ruleId: 'AND001',
|
|
45
|
+
ruleName: 'Android Cleartext Traffic Allowed',
|
|
46
|
+
severity: 'critical',
|
|
47
|
+
category: 'android',
|
|
48
|
+
message: 'Network security config allows cleartext traffic for specific domains',
|
|
49
|
+
filePath,
|
|
50
|
+
line: lineNum,
|
|
51
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
52
|
+
remediation: 'Remove cleartext traffic permissions. Use HTTPS for all domains.',
|
|
53
|
+
references: ['https://developer.android.com/training/articles/security-config']
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return findings;
|
|
60
|
+
},
|
|
61
|
+
remediation: 'Disable cleartext traffic and enforce HTTPS.'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'AND002',
|
|
65
|
+
name: 'Android Debug Mode Enabled',
|
|
66
|
+
description: 'Detects debuggable flag enabled in Android manifest',
|
|
67
|
+
severity: 'critical',
|
|
68
|
+
category: 'android',
|
|
69
|
+
filePatterns: ['**/AndroidManifest.xml', '**/build.gradle', '**/build.gradle.kts'],
|
|
70
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
71
|
+
const findings: Finding[] = [];
|
|
72
|
+
const lines = content.split('\n');
|
|
73
|
+
|
|
74
|
+
if (filePath.includes('AndroidManifest.xml')) {
|
|
75
|
+
const debugPattern = /android:debuggable\s*=\s*["']true["']/i;
|
|
76
|
+
if (debugPattern.test(content)) {
|
|
77
|
+
const match = content.match(debugPattern);
|
|
78
|
+
if (match) {
|
|
79
|
+
const lineNum = content.substring(0, content.indexOf(match[0])).split('\n').length;
|
|
80
|
+
findings.push({
|
|
81
|
+
ruleId: 'AND002',
|
|
82
|
+
ruleName: 'Android Debug Mode Enabled',
|
|
83
|
+
severity: 'critical',
|
|
84
|
+
category: 'android',
|
|
85
|
+
message: 'android:debuggable="true" allows debugging and code inspection',
|
|
86
|
+
filePath,
|
|
87
|
+
line: lineNum,
|
|
88
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
89
|
+
remediation: 'Remove android:debuggable or set to false for release builds.',
|
|
90
|
+
references: ['https://developer.android.com/guide/topics/manifest/application-element#debug']
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (filePath.includes('build.gradle')) {
|
|
97
|
+
// Check for debuggable true in release build type
|
|
98
|
+
const releaseDebugPattern = /release\s*\{[^}]*debuggable\s*(?:=\s*)?true/i;
|
|
99
|
+
if (releaseDebugPattern.test(content)) {
|
|
100
|
+
const match = content.match(releaseDebugPattern);
|
|
101
|
+
if (match) {
|
|
102
|
+
const lineNum = content.substring(0, content.indexOf(match[0])).split('\n').length;
|
|
103
|
+
findings.push({
|
|
104
|
+
ruleId: 'AND002',
|
|
105
|
+
ruleName: 'Android Debug Mode Enabled',
|
|
106
|
+
severity: 'critical',
|
|
107
|
+
category: 'android',
|
|
108
|
+
message: 'Release build has debuggable=true enabled',
|
|
109
|
+
filePath,
|
|
110
|
+
line: lineNum,
|
|
111
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
112
|
+
remediation: 'Set debuggable false for release build type.',
|
|
113
|
+
references: ['https://developer.android.com/studio/build/build-variants']
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return findings;
|
|
120
|
+
},
|
|
121
|
+
remediation: 'Disable debugging in release builds.'
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'AND003',
|
|
125
|
+
name: 'Insecure Android Permissions',
|
|
126
|
+
description: 'Detects dangerous or unnecessary Android permissions',
|
|
127
|
+
severity: 'high',
|
|
128
|
+
category: 'android',
|
|
129
|
+
filePatterns: ['**/AndroidManifest.xml'],
|
|
130
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
131
|
+
const findings: Finding[] = [];
|
|
132
|
+
const lines = content.split('\n');
|
|
133
|
+
|
|
134
|
+
const dangerousPermissions = [
|
|
135
|
+
{ name: 'READ_CONTACTS', severity: 'medium' as const },
|
|
136
|
+
{ name: 'WRITE_CONTACTS', severity: 'medium' as const },
|
|
137
|
+
{ name: 'READ_CALL_LOG', severity: 'high' as const },
|
|
138
|
+
{ name: 'WRITE_CALL_LOG', severity: 'high' as const },
|
|
139
|
+
{ name: 'READ_SMS', severity: 'high' as const },
|
|
140
|
+
{ name: 'SEND_SMS', severity: 'high' as const },
|
|
141
|
+
{ name: 'RECEIVE_SMS', severity: 'high' as const },
|
|
142
|
+
{ name: 'RECORD_AUDIO', severity: 'high' as const },
|
|
143
|
+
{ name: 'READ_EXTERNAL_STORAGE', severity: 'medium' as const },
|
|
144
|
+
{ name: 'WRITE_EXTERNAL_STORAGE', severity: 'medium' as const },
|
|
145
|
+
{ name: 'ACCESS_FINE_LOCATION', severity: 'medium' as const },
|
|
146
|
+
{ name: 'ACCESS_BACKGROUND_LOCATION', severity: 'high' as const },
|
|
147
|
+
{ name: 'CAMERA', severity: 'medium' as const },
|
|
148
|
+
{ name: 'SYSTEM_ALERT_WINDOW', severity: 'high' as const },
|
|
149
|
+
{ name: 'REQUEST_INSTALL_PACKAGES', severity: 'critical' as const },
|
|
150
|
+
{ name: 'BIND_ACCESSIBILITY_SERVICE', severity: 'critical' as const }
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
for (const perm of dangerousPermissions) {
|
|
154
|
+
const pattern = new RegExp(`uses-permission[^>]*android.permission.${perm.name}`, 'i');
|
|
155
|
+
if (pattern.test(content)) {
|
|
156
|
+
const match = content.match(pattern);
|
|
157
|
+
if (match) {
|
|
158
|
+
const lineNum = content.substring(0, content.indexOf(match[0])).split('\n').length;
|
|
159
|
+
findings.push({
|
|
160
|
+
ruleId: 'AND003',
|
|
161
|
+
ruleName: 'Insecure Android Permissions',
|
|
162
|
+
severity: perm.severity,
|
|
163
|
+
category: 'android',
|
|
164
|
+
message: `Dangerous permission ${perm.name} declared`,
|
|
165
|
+
filePath,
|
|
166
|
+
line: lineNum,
|
|
167
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
168
|
+
remediation: 'Only request permissions that are strictly necessary. Review if this permission is required.',
|
|
169
|
+
references: ['https://developer.android.com/guide/topics/permissions/overview']
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return findings;
|
|
176
|
+
},
|
|
177
|
+
remediation: 'Review and minimize permission requests to only what is necessary.'
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'AND004',
|
|
181
|
+
name: 'Android Backup Allowed',
|
|
182
|
+
description: 'Detects when Android auto-backup is enabled for app data',
|
|
183
|
+
severity: 'medium',
|
|
184
|
+
category: 'android',
|
|
185
|
+
filePatterns: ['**/AndroidManifest.xml'],
|
|
186
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
187
|
+
const findings: Finding[] = [];
|
|
188
|
+
const lines = content.split('\n');
|
|
189
|
+
|
|
190
|
+
// Check if allowBackup is explicitly set to true or not set (defaults to true)
|
|
191
|
+
const allowBackupTrue = /android:allowBackup\s*=\s*["']true["']/i;
|
|
192
|
+
const allowBackupSet = /android:allowBackup\s*=\s*["'](?:true|false)["']/i;
|
|
193
|
+
|
|
194
|
+
if (allowBackupTrue.test(content)) {
|
|
195
|
+
const match = content.match(allowBackupTrue);
|
|
196
|
+
if (match) {
|
|
197
|
+
const lineNum = content.substring(0, content.indexOf(match[0])).split('\n').length;
|
|
198
|
+
findings.push({
|
|
199
|
+
ruleId: 'AND004',
|
|
200
|
+
ruleName: 'Android Backup Allowed',
|
|
201
|
+
severity: 'medium',
|
|
202
|
+
category: 'android',
|
|
203
|
+
message: 'android:allowBackup="true" allows backup of app data including sensitive information',
|
|
204
|
+
filePath,
|
|
205
|
+
line: lineNum,
|
|
206
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
207
|
+
remediation: 'Set android:allowBackup="false" or configure backup_rules to exclude sensitive data.',
|
|
208
|
+
references: ['https://developer.android.com/guide/topics/data/autobackup']
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
} else if (!allowBackupSet.test(content) && content.includes('<application')) {
|
|
212
|
+
// Not set at all - defaults to true
|
|
213
|
+
findings.push({
|
|
214
|
+
ruleId: 'AND004',
|
|
215
|
+
ruleName: 'Android Backup Allowed',
|
|
216
|
+
severity: 'medium',
|
|
217
|
+
category: 'android',
|
|
218
|
+
message: 'android:allowBackup not set (defaults to true), allowing backup of app data',
|
|
219
|
+
filePath,
|
|
220
|
+
line: 1,
|
|
221
|
+
remediation: 'Explicitly set android:allowBackup="false" or configure backup rules.',
|
|
222
|
+
references: ['https://developer.android.com/guide/topics/data/autobackup']
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return findings;
|
|
227
|
+
},
|
|
228
|
+
remediation: 'Disable auto-backup or configure backup rules to exclude sensitive data.'
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'AND005',
|
|
232
|
+
name: 'Exported Components Without Permission',
|
|
233
|
+
description: 'Detects exported activities/services/receivers without proper permission protection',
|
|
234
|
+
severity: 'high',
|
|
235
|
+
category: 'android',
|
|
236
|
+
filePatterns: ['**/AndroidManifest.xml'],
|
|
237
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
238
|
+
const findings: Finding[] = [];
|
|
239
|
+
const lines = content.split('\n');
|
|
240
|
+
|
|
241
|
+
// Check for exported components without permission
|
|
242
|
+
const componentTypes = ['activity', 'service', 'receiver', 'provider'];
|
|
243
|
+
|
|
244
|
+
for (const type of componentTypes) {
|
|
245
|
+
const exportedPattern = new RegExp(`<${type}[^>]*android:exported\\s*=\\s*["']true["'][^>]*>`, 'gi');
|
|
246
|
+
let match;
|
|
247
|
+
|
|
248
|
+
while ((match = exportedPattern.exec(content)) !== null) {
|
|
249
|
+
const component = match[0];
|
|
250
|
+
const hasPermission = /android:permission\s*=/.test(component);
|
|
251
|
+
|
|
252
|
+
if (!hasPermission) {
|
|
253
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
254
|
+
findings.push({
|
|
255
|
+
ruleId: 'AND005',
|
|
256
|
+
ruleName: 'Exported Components Without Permission',
|
|
257
|
+
severity: 'high',
|
|
258
|
+
category: 'android',
|
|
259
|
+
message: `Exported ${type} without permission protection`,
|
|
260
|
+
filePath,
|
|
261
|
+
line: lineNum,
|
|
262
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
263
|
+
remediation: `Add android:permission to protect the exported ${type} or set exported="false" if external access is not needed.`,
|
|
264
|
+
references: ['https://developer.android.com/guide/components/intents-filters']
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return findings;
|
|
271
|
+
},
|
|
272
|
+
remediation: 'Protect exported components with appropriate permissions.'
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
id: 'AND006',
|
|
276
|
+
name: 'WebView JavaScript Enabled Without Safeguards',
|
|
277
|
+
description: 'Detects WebView with JavaScript enabled but without proper security measures',
|
|
278
|
+
severity: 'high',
|
|
279
|
+
category: 'android',
|
|
280
|
+
filePatterns: ['**/*.java', '**/*.kt'],
|
|
281
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
282
|
+
const findings: Finding[] = [];
|
|
283
|
+
const lines = content.split('\n');
|
|
284
|
+
|
|
285
|
+
const jsEnabledPattern = /setJavaScriptEnabled\s*\(\s*true\s*\)/g;
|
|
286
|
+
|
|
287
|
+
let match;
|
|
288
|
+
while ((match = jsEnabledPattern.exec(content)) !== null) {
|
|
289
|
+
const context = content.substring(Math.max(0, match.index - 500), match.index + 500);
|
|
290
|
+
|
|
291
|
+
// Check for security measures
|
|
292
|
+
const hasFileAccess = /setAllowFileAccess\s*\(\s*false\s*\)/.test(context);
|
|
293
|
+
const hasContentAccess = /setAllowContentAccess\s*\(\s*false\s*\)/.test(context);
|
|
294
|
+
|
|
295
|
+
if (!hasFileAccess || !hasContentAccess) {
|
|
296
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
297
|
+
findings.push({
|
|
298
|
+
ruleId: 'AND006',
|
|
299
|
+
ruleName: 'WebView JavaScript Enabled Without Safeguards',
|
|
300
|
+
severity: 'high',
|
|
301
|
+
category: 'android',
|
|
302
|
+
message: 'WebView has JavaScript enabled without disabling file/content access',
|
|
303
|
+
filePath,
|
|
304
|
+
line: lineNum,
|
|
305
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
306
|
+
remediation: 'Disable setAllowFileAccess(false) and setAllowContentAccess(false) when JavaScript is enabled.',
|
|
307
|
+
references: ['https://developer.android.com/reference/android/webkit/WebSettings']
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return findings;
|
|
313
|
+
},
|
|
314
|
+
remediation: 'Disable file and content access in WebView when JavaScript is enabled.'
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
id: 'AND007',
|
|
318
|
+
name: 'Insecure WebView addJavascriptInterface',
|
|
319
|
+
description: 'Detects addJavascriptInterface usage which can be exploited on older Android versions',
|
|
320
|
+
severity: 'high',
|
|
321
|
+
category: 'android',
|
|
322
|
+
filePatterns: ['**/*.java', '**/*.kt'],
|
|
323
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
324
|
+
const findings: Finding[] = [];
|
|
325
|
+
const lines = content.split('\n');
|
|
326
|
+
|
|
327
|
+
const jsInterfacePattern = /addJavascriptInterface\s*\(/g;
|
|
328
|
+
|
|
329
|
+
let match;
|
|
330
|
+
while ((match = jsInterfacePattern.exec(content)) !== null) {
|
|
331
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
332
|
+
findings.push({
|
|
333
|
+
ruleId: 'AND007',
|
|
334
|
+
ruleName: 'Insecure WebView addJavascriptInterface',
|
|
335
|
+
severity: 'high',
|
|
336
|
+
category: 'android',
|
|
337
|
+
message: 'addJavascriptInterface can be exploited for code injection on Android < 4.2',
|
|
338
|
+
filePath,
|
|
339
|
+
line: lineNum,
|
|
340
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
341
|
+
remediation: 'Ensure minSdkVersion is >= 17 (Android 4.2) or use @JavascriptInterface annotation.',
|
|
342
|
+
references: ['https://developer.android.com/reference/android/webkit/WebView#addJavascriptInterface']
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return findings;
|
|
347
|
+
},
|
|
348
|
+
remediation: 'Use @JavascriptInterface annotation and ensure minSdkVersion >= 17.'
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: 'AND008',
|
|
352
|
+
name: 'Hardcoded Signing Key',
|
|
353
|
+
description: 'Detects hardcoded signing key passwords or keystores in build files',
|
|
354
|
+
severity: 'critical',
|
|
355
|
+
category: 'android',
|
|
356
|
+
filePatterns: ['**/build.gradle', '**/build.gradle.kts'],
|
|
357
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
358
|
+
const findings: Finding[] = [];
|
|
359
|
+
const lines = content.split('\n');
|
|
360
|
+
|
|
361
|
+
// Check for hardcoded signing config
|
|
362
|
+
const patterns = [
|
|
363
|
+
/storePassword\s*(?:=|:)\s*['"][^'"]+['"]/i,
|
|
364
|
+
/keyPassword\s*(?:=|:)\s*['"][^'"]+['"]/i,
|
|
365
|
+
/storeFile\s*(?:=|:)\s*file\s*\(['"][^'"]+\.jks['"]\)/i
|
|
366
|
+
];
|
|
367
|
+
|
|
368
|
+
for (const pattern of patterns) {
|
|
369
|
+
const matches = content.match(pattern);
|
|
370
|
+
if (matches) {
|
|
371
|
+
const match = matches[0];
|
|
372
|
+
const lineNum = content.substring(0, content.indexOf(match)).split('\n').length;
|
|
373
|
+
findings.push({
|
|
374
|
+
ruleId: 'AND008',
|
|
375
|
+
ruleName: 'Hardcoded Signing Key',
|
|
376
|
+
severity: 'critical',
|
|
377
|
+
category: 'android',
|
|
378
|
+
message: 'Signing key credentials appear to be hardcoded in build file',
|
|
379
|
+
filePath,
|
|
380
|
+
line: lineNum,
|
|
381
|
+
codeSnippet: lines[lineNum - 1]?.trim().replace(/['"][^'"]{5,}['"]/g, '"***REDACTED***"'),
|
|
382
|
+
remediation: 'Use environment variables or keystore.properties file excluded from version control.',
|
|
383
|
+
references: ['https://developer.android.com/studio/publish/app-signing']
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return findings;
|
|
389
|
+
},
|
|
390
|
+
remediation: 'Move signing credentials to environment variables or excluded properties files.'
|
|
391
|
+
}
|
|
392
|
+
];
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { Rule, Finding } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const authenticationRules: Rule[] = [
|
|
4
|
+
{
|
|
5
|
+
id: 'AUTH001',
|
|
6
|
+
name: 'Weak JWT Validation',
|
|
7
|
+
description: 'Detects JWT handling without proper validation',
|
|
8
|
+
severity: 'high',
|
|
9
|
+
category: 'authentication',
|
|
10
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
11
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
12
|
+
const findings: Finding[] = [];
|
|
13
|
+
const lines = content.split('\n');
|
|
14
|
+
|
|
15
|
+
// Check for JWT decode without verify
|
|
16
|
+
const jwtDecodePattern = /jwt\.decode\s*\(|jwtDecode\s*\(|atob\s*\([^)]*\.split\s*\(['"]\.['"]|JSON\.parse.*base64/gi;
|
|
17
|
+
|
|
18
|
+
let match;
|
|
19
|
+
while ((match = jwtDecodePattern.exec(content)) !== null) {
|
|
20
|
+
const context = content.substring(match.index, Math.min(content.length, match.index + 300));
|
|
21
|
+
const hasVerify = /verify|validate|check.*signature/i.test(context);
|
|
22
|
+
|
|
23
|
+
if (!hasVerify) {
|
|
24
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
25
|
+
findings.push({
|
|
26
|
+
ruleId: 'AUTH001',
|
|
27
|
+
ruleName: 'Weak JWT Validation',
|
|
28
|
+
severity: 'high',
|
|
29
|
+
category: 'authentication',
|
|
30
|
+
message: 'JWT decoded without signature verification',
|
|
31
|
+
filePath,
|
|
32
|
+
line: lineNum,
|
|
33
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
34
|
+
remediation: 'Always verify JWT signature before trusting claims. Use jwt.verify() instead of jwt.decode().',
|
|
35
|
+
references: [
|
|
36
|
+
'https://capacitor-sec.dev/docs/rules/jwt',
|
|
37
|
+
'https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/06-Session_Management_Testing/10-Testing_JSON_Web_Tokens'
|
|
38
|
+
]
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return findings;
|
|
44
|
+
},
|
|
45
|
+
remediation: 'Verify JWT signature on the backend. Never trust client-side JWT validation alone.'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'AUTH002',
|
|
49
|
+
name: 'Insecure Biometric Implementation',
|
|
50
|
+
description: 'Detects weak biometric authentication implementation',
|
|
51
|
+
severity: 'high',
|
|
52
|
+
category: 'authentication',
|
|
53
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
54
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
55
|
+
const findings: Finding[] = [];
|
|
56
|
+
const lines = content.split('\n');
|
|
57
|
+
|
|
58
|
+
// Check for biometric without proper cryptographic backing
|
|
59
|
+
const biometricPattern = /(?:NativeBiometric|BiometricAuth|FingerprintAIO)\.(?:verifyIdentity|authenticate)/gi;
|
|
60
|
+
|
|
61
|
+
let match;
|
|
62
|
+
while ((match = biometricPattern.exec(content)) !== null) {
|
|
63
|
+
const context = content.substring(match.index, Math.min(content.length, match.index + 500));
|
|
64
|
+
|
|
65
|
+
// Check for cryptographic operations after biometric
|
|
66
|
+
const hasCryptoOp = /getCredentials|decrypt|sign|keychain|keystore/i.test(context);
|
|
67
|
+
|
|
68
|
+
if (!hasCryptoOp) {
|
|
69
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
70
|
+
findings.push({
|
|
71
|
+
ruleId: 'AUTH002',
|
|
72
|
+
ruleName: 'Insecure Biometric Implementation',
|
|
73
|
+
severity: 'high',
|
|
74
|
+
category: 'authentication',
|
|
75
|
+
message: 'Biometric auth not backed by cryptographic operation',
|
|
76
|
+
filePath,
|
|
77
|
+
line: lineNum,
|
|
78
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
79
|
+
remediation: 'Back biometric authentication with cryptographic keys stored in secure enclave.',
|
|
80
|
+
references: ['https://capacitor-sec.dev/docs/rules/biometric']
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return findings;
|
|
86
|
+
},
|
|
87
|
+
remediation: 'Use cryptographically-backed biometric authentication.'
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'AUTH003',
|
|
91
|
+
name: 'Weak Random Number Generation',
|
|
92
|
+
description: 'Detects use of Math.random() for security-sensitive operations',
|
|
93
|
+
severity: 'high',
|
|
94
|
+
category: 'authentication',
|
|
95
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
96
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
97
|
+
const findings: Finding[] = [];
|
|
98
|
+
const lines = content.split('\n');
|
|
99
|
+
|
|
100
|
+
// Skip test files
|
|
101
|
+
if (filePath.includes('.test.') || filePath.includes('.spec.')) {
|
|
102
|
+
return findings;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check for Math.random in security context
|
|
106
|
+
const randomPattern = /Math\.random\s*\(\)/g;
|
|
107
|
+
|
|
108
|
+
let match;
|
|
109
|
+
while ((match = randomPattern.exec(content)) !== null) {
|
|
110
|
+
const context = content.substring(Math.max(0, match.index - 200), match.index + 200);
|
|
111
|
+
const securityContext = /(?:token|key|secret|nonce|salt|iv|session|auth|password|otp|code)/i.test(context);
|
|
112
|
+
|
|
113
|
+
if (securityContext) {
|
|
114
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
115
|
+
findings.push({
|
|
116
|
+
ruleId: 'AUTH003',
|
|
117
|
+
ruleName: 'Weak Random Number Generation',
|
|
118
|
+
severity: 'high',
|
|
119
|
+
category: 'authentication',
|
|
120
|
+
message: 'Math.random() used in security context - not cryptographically secure',
|
|
121
|
+
filePath,
|
|
122
|
+
line: lineNum,
|
|
123
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
124
|
+
remediation: 'Use crypto.getRandomValues() or crypto.randomUUID() for security-sensitive operations.',
|
|
125
|
+
references: ['https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues']
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return findings;
|
|
131
|
+
},
|
|
132
|
+
remediation: 'Use crypto.getRandomValues() for secure random number generation.'
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'AUTH004',
|
|
136
|
+
name: 'Missing Session Timeout',
|
|
137
|
+
description: 'Detects authentication without session timeout handling',
|
|
138
|
+
severity: 'medium',
|
|
139
|
+
category: 'authentication',
|
|
140
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
141
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
142
|
+
const findings: Finding[] = [];
|
|
143
|
+
|
|
144
|
+
// Only check auth-related files
|
|
145
|
+
if (!/auth|login|session/i.test(filePath) && !/auth|login|session/i.test(content)) {
|
|
146
|
+
return findings;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const hasLogin = /(?:login|signIn|authenticate)\s*\(/i.test(content);
|
|
150
|
+
const hasTimeout = /(?:timeout|expire|ttl|maxAge|expiresIn|expiresAt)/i.test(content);
|
|
151
|
+
|
|
152
|
+
if (hasLogin && !hasTimeout) {
|
|
153
|
+
findings.push({
|
|
154
|
+
ruleId: 'AUTH004',
|
|
155
|
+
ruleName: 'Missing Session Timeout',
|
|
156
|
+
severity: 'medium',
|
|
157
|
+
category: 'authentication',
|
|
158
|
+
message: 'Authentication flow without apparent session timeout',
|
|
159
|
+
filePath,
|
|
160
|
+
line: 1,
|
|
161
|
+
remediation: 'Implement session timeouts with automatic logout for inactive sessions.',
|
|
162
|
+
references: ['https://capacitor-sec.dev/docs/rules/session-timeout']
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return findings;
|
|
167
|
+
},
|
|
168
|
+
remediation: 'Implement session timeout and automatic logout.'
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'AUTH005',
|
|
172
|
+
name: 'OAuth State Parameter Missing',
|
|
173
|
+
description: 'Detects OAuth flows without state parameter for CSRF protection',
|
|
174
|
+
severity: 'high',
|
|
175
|
+
category: 'authentication',
|
|
176
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
177
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
178
|
+
const findings: Finding[] = [];
|
|
179
|
+
const lines = content.split('\n');
|
|
180
|
+
|
|
181
|
+
// Check for OAuth URLs without state parameter
|
|
182
|
+
const oauthPattern = /(?:authorize|oauth|auth).*(?:client_id|response_type)/gi;
|
|
183
|
+
|
|
184
|
+
let match;
|
|
185
|
+
while ((match = oauthPattern.exec(content)) !== null) {
|
|
186
|
+
const context = content.substring(match.index, Math.min(content.length, match.index + 300));
|
|
187
|
+
const hasState = /state\s*[:=]/.test(context);
|
|
188
|
+
|
|
189
|
+
if (!hasState) {
|
|
190
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
191
|
+
findings.push({
|
|
192
|
+
ruleId: 'AUTH005',
|
|
193
|
+
ruleName: 'OAuth State Parameter Missing',
|
|
194
|
+
severity: 'high',
|
|
195
|
+
category: 'authentication',
|
|
196
|
+
message: 'OAuth flow without state parameter for CSRF protection',
|
|
197
|
+
filePath,
|
|
198
|
+
line: lineNum,
|
|
199
|
+
codeSnippet: lines[lineNum - 1]?.trim(),
|
|
200
|
+
remediation: 'Always include a unique state parameter in OAuth flows and validate it on callback.',
|
|
201
|
+
references: ['https://tools.ietf.org/html/rfc6749#section-10.12']
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return findings;
|
|
207
|
+
},
|
|
208
|
+
remediation: 'Add state parameter to OAuth flows for CSRF protection.'
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: 'AUTH006',
|
|
212
|
+
name: 'Hardcoded Credentials in Auth',
|
|
213
|
+
description: 'Detects hardcoded credentials in authentication code',
|
|
214
|
+
severity: 'critical',
|
|
215
|
+
category: 'authentication',
|
|
216
|
+
filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
|
|
217
|
+
check: (content: string, filePath: string): Finding[] => {
|
|
218
|
+
const findings: Finding[] = [];
|
|
219
|
+
const lines = content.split('\n');
|
|
220
|
+
|
|
221
|
+
// Skip test files
|
|
222
|
+
if (filePath.includes('.test.') || filePath.includes('.spec.') || filePath.includes('__mocks__')) {
|
|
223
|
+
return findings;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check for hardcoded auth credentials
|
|
227
|
+
const patterns = [
|
|
228
|
+
/password\s*[:=]\s*['"][^'"]{4,}['"]/gi,
|
|
229
|
+
/username\s*[:=]\s*['"][^'"@]+['"]/gi,
|
|
230
|
+
/clientSecret\s*[:=]\s*['"][^'"]{8,}['"]/gi
|
|
231
|
+
];
|
|
232
|
+
|
|
233
|
+
for (const pattern of patterns) {
|
|
234
|
+
let match;
|
|
235
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
236
|
+
// Skip obvious examples/placeholders
|
|
237
|
+
if (/example|placeholder|your|xxx|changeme/i.test(match[0])) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
242
|
+
findings.push({
|
|
243
|
+
ruleId: 'AUTH006',
|
|
244
|
+
ruleName: 'Hardcoded Credentials in Auth',
|
|
245
|
+
severity: 'critical',
|
|
246
|
+
category: 'authentication',
|
|
247
|
+
message: 'Hardcoded credentials detected in authentication code',
|
|
248
|
+
filePath,
|
|
249
|
+
line: lineNum,
|
|
250
|
+
codeSnippet: lines[lineNum - 1]?.trim().replace(/['"][^'"]+['"]/g, '"***REDACTED***"'),
|
|
251
|
+
remediation: 'Remove hardcoded credentials. Use secure configuration or environment variables.',
|
|
252
|
+
references: ['https://capacitor-sec.dev/docs/rules/hardcoded-creds']
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return findings;
|
|
258
|
+
},
|
|
259
|
+
remediation: 'Remove hardcoded credentials and use secure configuration.'
|
|
260
|
+
}
|
|
261
|
+
];
|