@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.
@@ -0,0 +1,326 @@
1
+ import type { Rule, Finding } from '../types.js';
2
+
3
+ export const iosRules: Rule[] = [
4
+ {
5
+ id: 'IOS001',
6
+ name: 'App Transport Security Disabled',
7
+ description: 'Detects when App Transport Security (ATS) is disabled or has exceptions',
8
+ severity: 'critical',
9
+ category: 'ios',
10
+ filePatterns: ['**/Info.plist'],
11
+ check: (content: string, filePath: string): Finding[] => {
12
+ const findings: Finding[] = [];
13
+ const lines = content.split('\n');
14
+
15
+ // Check for ATS disabled
16
+ const atsDisabledPatterns = [
17
+ { pattern: /<key>NSAllowsArbitraryLoads<\/key>\s*<true\s*\/>/, message: 'App Transport Security is completely disabled' },
18
+ { pattern: /<key>NSAllowsArbitraryLoadsInWebContent<\/key>\s*<true\s*\/>/, message: 'ATS disabled for WebView content' },
19
+ { pattern: /<key>NSAllowsLocalNetworking<\/key>\s*<true\s*\/>/, message: 'ATS allows local networking' },
20
+ { pattern: /<key>NSExceptionAllowsInsecureHTTPLoads<\/key>\s*<true\s*\/>/, message: 'ATS exception allows insecure HTTP' }
21
+ ];
22
+
23
+ for (const { pattern, message } of atsDisabledPatterns) {
24
+ if (pattern.test(content)) {
25
+ const match = content.match(pattern);
26
+ if (match) {
27
+ const lineNum = content.substring(0, content.indexOf(match[0])).split('\n').length;
28
+ findings.push({
29
+ ruleId: 'IOS001',
30
+ ruleName: 'App Transport Security Disabled',
31
+ severity: 'critical',
32
+ category: 'ios',
33
+ message,
34
+ filePath,
35
+ line: lineNum,
36
+ codeSnippet: lines[lineNum - 1]?.trim(),
37
+ remediation: 'Enable App Transport Security and use HTTPS. Add specific exceptions only for domains that truly require them.',
38
+ references: ['https://developer.apple.com/documentation/security/preventing_insecure_network_connections']
39
+ });
40
+ }
41
+ }
42
+ }
43
+
44
+ return findings;
45
+ },
46
+ remediation: 'Enable ATS and use HTTPS exclusively. Minimize domain exceptions.'
47
+ },
48
+ {
49
+ id: 'IOS002',
50
+ name: 'Insecure Keychain Access',
51
+ description: 'Detects insecure Keychain access configuration',
52
+ severity: 'high',
53
+ category: 'ios',
54
+ filePatterns: ['**/*.swift', '**/*.m', '**/*.ts', '**/*.tsx'],
55
+ check: (content: string, filePath: string): Finding[] => {
56
+ const findings: Finding[] = [];
57
+ const lines = content.split('\n');
58
+
59
+ // Check for insecure Keychain accessibility
60
+ const insecurePatterns = [
61
+ { pattern: /kSecAttrAccessibleAlways/, message: 'Keychain item accessible when device is locked' },
62
+ { pattern: /kSecAttrAccessibleAlwaysThisDeviceOnly/, message: 'Keychain item accessible when device is locked' },
63
+ { pattern: /kSecAttrAccessibleAfterFirstUnlock/, message: 'Keychain item accessible after first unlock - consider more restrictive option' }
64
+ ];
65
+
66
+ for (const { pattern, message } of insecurePatterns) {
67
+ let match;
68
+ const regex = new RegExp(pattern.source, 'g');
69
+ while ((match = regex.exec(content)) !== null) {
70
+ const lineNum = content.substring(0, match.index).split('\n').length;
71
+ findings.push({
72
+ ruleId: 'IOS002',
73
+ ruleName: 'Insecure Keychain Access',
74
+ severity: 'high',
75
+ category: 'ios',
76
+ message,
77
+ filePath,
78
+ line: lineNum,
79
+ codeSnippet: lines[lineNum - 1]?.trim(),
80
+ remediation: 'Use kSecAttrAccessibleWhenUnlockedThisDeviceOnly or kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly for sensitive data.',
81
+ references: ['https://developer.apple.com/documentation/security/keychain_services/keychain_items/item_attribute_keys_and_values']
82
+ });
83
+ }
84
+ }
85
+
86
+ return findings;
87
+ },
88
+ remediation: 'Use the most restrictive Keychain accessibility that meets your needs.'
89
+ },
90
+ {
91
+ id: 'IOS003',
92
+ name: 'URL Scheme Without Validation',
93
+ description: 'Detects custom URL scheme handlers without proper validation',
94
+ severity: 'high',
95
+ category: 'ios',
96
+ filePatterns: ['**/Info.plist', '**/*.swift', '**/AppDelegate.swift'],
97
+ check: (content: string, filePath: string): Finding[] => {
98
+ const findings: Finding[] = [];
99
+ const lines = content.split('\n');
100
+
101
+ if (filePath.includes('Info.plist')) {
102
+ // Check for URL schemes
103
+ if (/<key>CFBundleURLSchemes<\/key>/.test(content)) {
104
+ findings.push({
105
+ ruleId: 'IOS003',
106
+ ruleName: 'URL Scheme Without Validation',
107
+ severity: 'info',
108
+ category: 'ios',
109
+ message: 'Custom URL scheme detected - ensure proper validation in handler',
110
+ filePath,
111
+ line: 1,
112
+ remediation: 'Validate all URL scheme inputs before processing. Implement Universal Links for better security.',
113
+ references: ['https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app']
114
+ });
115
+ }
116
+ }
117
+
118
+ if (filePath.includes('.swift') || filePath.includes('AppDelegate')) {
119
+ // Check for URL handler without validation
120
+ const urlHandlerPattern = /func\s+application.*open\s+url:\s*URL/g;
121
+ if (urlHandlerPattern.test(content)) {
122
+ const hasValidation = /url\.scheme\s*==|url\.host\s*==|validateURL|isValidURL/.test(content);
123
+ if (!hasValidation) {
124
+ findings.push({
125
+ ruleId: 'IOS003',
126
+ ruleName: 'URL Scheme Without Validation',
127
+ severity: 'high',
128
+ category: 'ios',
129
+ message: 'URL scheme handler without apparent validation',
130
+ filePath,
131
+ line: 1,
132
+ remediation: 'Validate URL scheme, host, and parameters before processing.',
133
+ references: ['https://capacitor-sec.dev/docs/rules/url-schemes']
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ return findings;
140
+ },
141
+ remediation: 'Validate all components of incoming URLs before processing.'
142
+ },
143
+ {
144
+ id: 'IOS004',
145
+ name: 'iOS Pasteboard Sensitive Data',
146
+ description: 'Detects potential exposure of sensitive data through pasteboard',
147
+ severity: 'medium',
148
+ category: 'ios',
149
+ filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
150
+ check: (content: string, filePath: string): Finding[] => {
151
+ const findings: Finding[] = [];
152
+ const lines = content.split('\n');
153
+
154
+ // Check for clipboard usage with sensitive data
155
+ const clipboardPattern = /(?:Clipboard|Pasteboard)\.(?:write|set)/gi;
156
+ const sensitiveData = /(?:password|token|secret|key|auth|credential|credit.?card|ssn)/i;
157
+
158
+ let match;
159
+ while ((match = clipboardPattern.exec(content)) !== null) {
160
+ const context = content.substring(Math.max(0, match.index - 100), match.index + 200);
161
+ if (sensitiveData.test(context)) {
162
+ const lineNum = content.substring(0, match.index).split('\n').length;
163
+ findings.push({
164
+ ruleId: 'IOS004',
165
+ ruleName: 'iOS Pasteboard Sensitive Data',
166
+ severity: 'medium',
167
+ category: 'ios',
168
+ message: 'Sensitive data may be written to pasteboard where other apps can access it',
169
+ filePath,
170
+ line: lineNum,
171
+ codeSnippet: lines[lineNum - 1]?.trim(),
172
+ remediation: 'Avoid writing sensitive data to clipboard. If necessary, use expiring clipboard items on iOS 16+.',
173
+ references: ['https://capacitor-sec.dev/docs/rules/pasteboard']
174
+ });
175
+ }
176
+ }
177
+
178
+ return findings;
179
+ },
180
+ remediation: 'Avoid copying sensitive data to clipboard.'
181
+ },
182
+ {
183
+ id: 'IOS005',
184
+ name: 'Insecure iOS Entitlements',
185
+ description: 'Detects potentially dangerous iOS entitlements',
186
+ severity: 'high',
187
+ category: 'ios',
188
+ filePatterns: ['**/*.entitlements'],
189
+ check: (content: string, filePath: string): Finding[] => {
190
+ const findings: Finding[] = [];
191
+ const lines = content.split('\n');
192
+
193
+ const dangerousEntitlements = [
194
+ { key: 'get-task-allow', message: 'get-task-allow entitlement should be false in release builds' },
195
+ { key: 'com.apple.developer.associated-domains', message: 'Review associated domains for security implications' }
196
+ ];
197
+
198
+ for (const { key, message } of dangerousEntitlements) {
199
+ const pattern = new RegExp(`<key>${key}</key>\\s*<true\\s*/>`);
200
+ if (pattern.test(content)) {
201
+ const match = content.match(pattern);
202
+ if (match) {
203
+ const lineNum = content.substring(0, content.indexOf(match[0])).split('\n').length;
204
+ findings.push({
205
+ ruleId: 'IOS005',
206
+ ruleName: 'Insecure iOS Entitlements',
207
+ severity: key === 'get-task-allow' ? 'critical' : 'info',
208
+ category: 'ios',
209
+ message,
210
+ filePath,
211
+ line: lineNum,
212
+ codeSnippet: lines[lineNum - 1]?.trim(),
213
+ remediation: 'Review entitlements for release builds. Disable debugging entitlements.',
214
+ references: ['https://developer.apple.com/documentation/bundleresources/entitlements']
215
+ });
216
+ }
217
+ }
218
+ }
219
+
220
+ return findings;
221
+ },
222
+ remediation: 'Review and minimize iOS entitlements for production.'
223
+ },
224
+ {
225
+ id: 'IOS006',
226
+ name: 'Background App Refresh Data Exposure',
227
+ description: 'Detects background refresh that may expose sensitive operations',
228
+ severity: 'low',
229
+ category: 'ios',
230
+ filePatterns: ['**/Info.plist'],
231
+ check: (content: string, filePath: string): Finding[] => {
232
+ const findings: Finding[] = [];
233
+ const lines = content.split('\n');
234
+
235
+ // Check for background modes
236
+ const backgroundPattern = /<key>UIBackgroundModes<\/key>/;
237
+ if (backgroundPattern.test(content)) {
238
+ findings.push({
239
+ ruleId: 'IOS006',
240
+ ruleName: 'Background App Refresh Data Exposure',
241
+ severity: 'info',
242
+ category: 'ios',
243
+ message: 'App has background modes enabled - ensure sensitive operations are protected',
244
+ filePath,
245
+ line: 1,
246
+ remediation: 'Review background operations for sensitive data handling. Encrypt data in background tasks.',
247
+ references: ['https://capacitor-sec.dev/docs/rules/background']
248
+ });
249
+ }
250
+
251
+ return findings;
252
+ },
253
+ remediation: 'Secure sensitive operations running in background.'
254
+ },
255
+ {
256
+ id: 'IOS007',
257
+ name: 'Missing iOS Jailbreak Detection',
258
+ description: 'No jailbreak detection found for sensitive iOS application',
259
+ severity: 'medium',
260
+ category: 'ios',
261
+ filePatterns: ['**/*.ts', '**/*.tsx', '**/*.swift'],
262
+ check: (content: string, filePath: string): Finding[] => {
263
+ const findings: Finding[] = [];
264
+
265
+ // Only check main entry files
266
+ if (!filePath.includes('App.') && !filePath.includes('main.') && !filePath.includes('AppDelegate')) {
267
+ return findings;
268
+ }
269
+
270
+ const hasSensitiveOps = /(?:payment|banking|wallet|crypto|biometric|auth)/i.test(content);
271
+ const hasJailbreakDetection = /(?:isJailbroken|jailbreak|Cydia|checkra1n|unc0ver|freeRASP)/i.test(content);
272
+
273
+ if (hasSensitiveOps && !hasJailbreakDetection) {
274
+ findings.push({
275
+ ruleId: 'IOS007',
276
+ ruleName: 'Missing iOS Jailbreak Detection',
277
+ severity: 'medium',
278
+ category: 'ios',
279
+ message: 'Sensitive operations without jailbreak detection',
280
+ filePath,
281
+ line: 1,
282
+ remediation: 'Implement jailbreak detection for sensitive apps using @niclas-niclas/capacitor-freerasp.',
283
+ references: [
284
+ 'https://github.com/niclas-niclas/capacitor-freerasp',
285
+ 'https://capacitor-sec.dev/docs/rules/jailbreak'
286
+ ]
287
+ });
288
+ }
289
+
290
+ return findings;
291
+ },
292
+ remediation: 'Add jailbreak detection for sensitive applications.'
293
+ },
294
+ {
295
+ id: 'IOS008',
296
+ name: 'Screenshots Not Disabled for Sensitive Screens',
297
+ description: 'Detects when screenshot protection is missing for sensitive UI',
298
+ severity: 'low',
299
+ category: 'ios',
300
+ filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
301
+ check: (content: string, filePath: string): Finding[] => {
302
+ const findings: Finding[] = [];
303
+
304
+ // Check for password/PIN input without screenshot protection
305
+ const sensitiveUI = /(?:password|pin|otp|cvv|credit.?card).*(?:input|field|form)/i.test(content);
306
+ const hasScreenshotProtection = /(?:preventScreenCapture|secureWindow|isSecure)/i.test(content);
307
+
308
+ if (sensitiveUI && !hasScreenshotProtection) {
309
+ findings.push({
310
+ ruleId: 'IOS008',
311
+ ruleName: 'Screenshots Not Disabled for Sensitive Screens',
312
+ severity: 'low',
313
+ category: 'ios',
314
+ message: 'Sensitive input UI without screenshot protection',
315
+ filePath,
316
+ line: 1,
317
+ remediation: 'Consider preventing screenshots on screens with sensitive data entry.',
318
+ references: ['https://capacitor-sec.dev/docs/rules/screenshots']
319
+ });
320
+ }
321
+
322
+ return findings;
323
+ },
324
+ remediation: 'Implement screenshot protection for sensitive UI screens.'
325
+ }
326
+ ];
@@ -0,0 +1,218 @@
1
+ import type { Rule, Finding } from '../types.js';
2
+
3
+ export const loggingRules: Rule[] = [
4
+ {
5
+ id: 'LOG001',
6
+ name: 'Sensitive Data in Console Logs',
7
+ description: 'Detects logging of sensitive data to console',
8
+ severity: 'high',
9
+ category: 'logging',
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
+ // Skip test files
16
+ if (filePath.includes('.test.') || filePath.includes('.spec.')) {
17
+ return findings;
18
+ }
19
+
20
+ // Check for console logs with sensitive data
21
+ const consolePattern = /console\.(?:log|debug|info|warn|error)\s*\([^)]*(?:password|token|secret|key|auth|credential|apikey|bearer|jwt)[^)]*\)/gi;
22
+
23
+ let match;
24
+ while ((match = consolePattern.exec(content)) !== null) {
25
+ const lineNum = content.substring(0, match.index).split('\n').length;
26
+ findings.push({
27
+ ruleId: 'LOG001',
28
+ ruleName: 'Sensitive Data in Console Logs',
29
+ severity: 'high',
30
+ category: 'logging',
31
+ message: 'Sensitive data may be exposed in console output',
32
+ filePath,
33
+ line: lineNum,
34
+ codeSnippet: lines[lineNum - 1]?.trim(),
35
+ remediation: 'Remove sensitive data from logs. Use structured logging with data masking.',
36
+ references: ['https://capacitor-sec.dev/docs/rules/logging']
37
+ });
38
+ }
39
+
40
+ return findings;
41
+ },
42
+ remediation: 'Remove sensitive data from logs or implement data masking.'
43
+ },
44
+ {
45
+ id: 'LOG002',
46
+ name: 'Console Logs in Production',
47
+ description: 'Detects console.log statements that should be removed in production',
48
+ severity: 'low',
49
+ category: 'logging',
50
+ filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
51
+ check: (content: string, filePath: string): Finding[] => {
52
+ const findings: Finding[] = [];
53
+
54
+ // Skip test files, dev files, and build scripts
55
+ if (filePath.includes('.test.') || filePath.includes('.spec.') ||
56
+ filePath.includes('.dev.') || filePath.includes('scripts/')) {
57
+ return findings;
58
+ }
59
+
60
+ // Count console.log statements
61
+ const consoleLogCount = (content.match(/console\.log\s*\(/g) || []).length;
62
+
63
+ if (consoleLogCount > 5) {
64
+ findings.push({
65
+ ruleId: 'LOG002',
66
+ ruleName: 'Console Logs in Production',
67
+ severity: 'low',
68
+ category: 'logging',
69
+ message: `File contains ${consoleLogCount} console.log statements that may be visible in production`,
70
+ filePath,
71
+ line: 1,
72
+ remediation: 'Remove or conditionally disable console.log in production builds.',
73
+ references: ['https://capacitor-sec.dev/docs/rules/console-logs']
74
+ });
75
+ }
76
+
77
+ return findings;
78
+ },
79
+ remediation: 'Use a proper logging library that can be disabled in production.'
80
+ }
81
+ ];
82
+
83
+ export const debugRules: Rule[] = [
84
+ {
85
+ id: 'DBG001',
86
+ name: 'Debugger Statement',
87
+ description: 'Detects debugger statements left in code',
88
+ severity: 'medium',
89
+ category: 'debug',
90
+ filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
91
+ check: (content: string, filePath: string): Finding[] => {
92
+ const findings: Finding[] = [];
93
+ const lines = content.split('\n');
94
+
95
+ const debuggerPattern = /\bdebugger\b/g;
96
+
97
+ let match;
98
+ while ((match = debuggerPattern.exec(content)) !== null) {
99
+ const lineNum = content.substring(0, match.index).split('\n').length;
100
+ findings.push({
101
+ ruleId: 'DBG001',
102
+ ruleName: 'Debugger Statement',
103
+ severity: 'medium',
104
+ category: 'debug',
105
+ message: 'Debugger statement found - will pause execution in development tools',
106
+ filePath,
107
+ line: lineNum,
108
+ codeSnippet: lines[lineNum - 1]?.trim(),
109
+ remediation: 'Remove debugger statements before production deployment.',
110
+ references: ['https://capacitor-sec.dev/docs/rules/debugger']
111
+ });
112
+ }
113
+
114
+ return findings;
115
+ },
116
+ remediation: 'Remove all debugger statements.'
117
+ },
118
+ {
119
+ id: 'DBG002',
120
+ name: 'Test Credentials in Code',
121
+ description: 'Detects test or demo credentials in source code',
122
+ severity: 'high',
123
+ category: 'debug',
124
+ filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.json'],
125
+ check: (content: string, filePath: string): Finding[] => {
126
+ const findings: Finding[] = [];
127
+ const lines = content.split('\n');
128
+
129
+ // Skip actual test files
130
+ if (filePath.includes('.test.') || filePath.includes('.spec.') || filePath.includes('__tests__')) {
131
+ return findings;
132
+ }
133
+
134
+ const testCredPatterns = [
135
+ /test(?:user|account|email|password)\s*[:=]\s*['"][^'"]+['"]/gi,
136
+ /demo(?:user|account|password)\s*[:=]\s*['"][^'"]+['"]/gi,
137
+ /admin(?:password|pass)\s*[:=]\s*['"][^'"]+['"]/gi,
138
+ /(?:password|email)\s*[:=]\s*['"](?:test|demo|admin|password|123456|qwerty)['"]/gi
139
+ ];
140
+
141
+ for (const pattern of testCredPatterns) {
142
+ let match;
143
+ while ((match = pattern.exec(content)) !== null) {
144
+ const lineNum = content.substring(0, match.index).split('\n').length;
145
+ findings.push({
146
+ ruleId: 'DBG002',
147
+ ruleName: 'Test Credentials in Code',
148
+ severity: 'high',
149
+ category: 'debug',
150
+ message: 'Test/demo credentials found in source code',
151
+ filePath,
152
+ line: lineNum,
153
+ codeSnippet: lines[lineNum - 1]?.trim().replace(/['"][^'"]{5,}['"]/g, '"***"'),
154
+ remediation: 'Remove test credentials from source code. Use environment-based configuration.',
155
+ references: ['https://capacitor-sec.dev/docs/rules/test-credentials']
156
+ });
157
+ }
158
+ }
159
+
160
+ return findings;
161
+ },
162
+ remediation: 'Remove test credentials and use environment variables.'
163
+ },
164
+ {
165
+ id: 'DBG003',
166
+ name: 'Development URL in Production',
167
+ description: 'Detects localhost or development URLs that may be left in code',
168
+ severity: 'medium',
169
+ category: 'debug',
170
+ filePatterns: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/capacitor.config.*'],
171
+ check: (content: string, filePath: string): Finding[] => {
172
+ const findings: Finding[] = [];
173
+ const lines = content.split('\n');
174
+
175
+ // Skip test and config files typically used for dev
176
+ if (filePath.includes('.test.') || filePath.includes('.spec.') ||
177
+ filePath.includes('.local.') || filePath.includes('.dev.')) {
178
+ return findings;
179
+ }
180
+
181
+ // Check for dev URLs in non-conditional code
182
+ const devUrlPatterns = [
183
+ /['"]http:\/\/localhost[^'"]*['"]/g,
184
+ /['"]http:\/\/127\.0\.0\.1[^'"]*['"]/g,
185
+ /['"]http:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}[^'"]*['"]/g,
186
+ /['"]http:\/\/192\.168\.[^'"]+['"]/g
187
+ ];
188
+
189
+ for (const pattern of devUrlPatterns) {
190
+ let match;
191
+ while ((match = pattern.exec(content)) !== null) {
192
+ // Check if it's in a development condition
193
+ const context = content.substring(Math.max(0, match.index - 100), match.index);
194
+ const isDevelopmentGuard = /(?:isDev|process\.env\.NODE_ENV|__DEV__|development)/i.test(context);
195
+
196
+ if (!isDevelopmentGuard) {
197
+ const lineNum = content.substring(0, match.index).split('\n').length;
198
+ findings.push({
199
+ ruleId: 'DBG003',
200
+ ruleName: 'Development URL in Production',
201
+ severity: 'medium',
202
+ category: 'debug',
203
+ message: 'Development URL found that may be accessible in production',
204
+ filePath,
205
+ line: lineNum,
206
+ codeSnippet: lines[lineNum - 1]?.trim(),
207
+ remediation: 'Use environment variables for URLs. Guard development URLs with environment checks.',
208
+ references: ['https://capacitor-sec.dev/docs/rules/dev-urls']
209
+ });
210
+ }
211
+ }
212
+ }
213
+
214
+ return findings;
215
+ },
216
+ remediation: 'Use environment-based URL configuration.'
217
+ }
218
+ ];