@datahogo/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +17 -0
- package/dist/engines/config.d.ts +3 -0
- package/dist/engines/config.d.ts.map +1 -0
- package/dist/engines/config.js +433 -0
- package/dist/engines/config.js.map +1 -0
- package/dist/engines/dast.d.ts +3 -0
- package/dist/engines/dast.d.ts.map +1 -0
- package/dist/engines/dast.js +167 -0
- package/dist/engines/dast.js.map +1 -0
- package/dist/engines/db-rules.d.ts +3 -0
- package/dist/engines/db-rules.d.ts.map +1 -0
- package/dist/engines/db-rules.js +166 -0
- package/dist/engines/db-rules.js.map +1 -0
- package/dist/engines/dependencies.d.ts +3 -0
- package/dist/engines/dependencies.d.ts.map +1 -0
- package/dist/engines/dependencies.js +167 -0
- package/dist/engines/dependencies.js.map +1 -0
- package/dist/engines/github-actions.d.ts +3 -0
- package/dist/engines/github-actions.d.ts.map +1 -0
- package/dist/engines/github-actions.js +215 -0
- package/dist/engines/github-actions.js.map +1 -0
- package/dist/engines/gitleaks.d.ts +3 -0
- package/dist/engines/gitleaks.d.ts.map +1 -0
- package/dist/engines/gitleaks.js +107 -0
- package/dist/engines/gitleaks.js.map +1 -0
- package/dist/engines/npm-audit.d.ts +3 -0
- package/dist/engines/npm-audit.d.ts.map +1 -0
- package/dist/engines/npm-audit.js +113 -0
- package/dist/engines/npm-audit.js.map +1 -0
- package/dist/engines/patterns.d.ts +3 -0
- package/dist/engines/patterns.d.ts.map +1 -0
- package/dist/engines/patterns.js +1330 -0
- package/dist/engines/patterns.js.map +1 -0
- package/dist/engines/secrets.d.ts +4 -0
- package/dist/engines/secrets.d.ts.map +1 -0
- package/dist/engines/secrets.js +260 -0
- package/dist/engines/secrets.js.map +1 -0
- package/dist/engines/semgrep.d.ts +3 -0
- package/dist/engines/semgrep.d.ts.map +1 -0
- package/dist/engines/semgrep.js +173 -0
- package/dist/engines/semgrep.js.map +1 -0
- package/dist/engines/types.d.ts +30 -0
- package/dist/engines/types.d.ts.map +1 -0
- package/dist/engines/types.js +3 -0
- package/dist/engines/types.js.map +1 -0
- package/dist/engines/url-scanner.d.ts +3 -0
- package/dist/engines/url-scanner.d.ts.map +1 -0
- package/dist/engines/url-scanner.js +469 -0
- package/dist/engines/url-scanner.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator.d.ts +32 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +237 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/rules/.gitkeep +0 -0
- package/dist/rules/ai-llm-security.yaml +199 -0
- package/dist/rules/api-security.yaml +489 -0
- package/dist/rules/auth-patterns.yaml +406 -0
- package/dist/rules/crypto-patterns.yaml +227 -0
- package/dist/rules/database-cloud-security.yaml +169 -0
- package/dist/rules/general-security.yaml +588 -0
- package/dist/rules/injection-patterns.yaml +318 -0
- package/dist/rules/nextjs-security.yaml +532 -0
- package/dist/rules/node-security.yaml +380 -0
- package/dist/rules/rules/.gitkeep +0 -0
- package/dist/rules/rules/ai-llm-security.yaml +199 -0
- package/dist/rules/rules/api-security.yaml +489 -0
- package/dist/rules/rules/auth-patterns.yaml +406 -0
- package/dist/rules/rules/crypto-patterns.yaml +227 -0
- package/dist/rules/rules/database-cloud-security.yaml +169 -0
- package/dist/rules/rules/general-security.yaml +588 -0
- package/dist/rules/rules/injection-patterns.yaml +318 -0
- package/dist/rules/rules/nextjs-security.yaml +532 -0
- package/dist/rules/rules/node-security.yaml +380 -0
- package/dist/rules/rules/supabase-security.yaml +387 -0
- package/dist/rules/supabase-security.yaml +387 -0
- package/dist/scanning/agents/csharp-agent.d.ts +12 -0
- package/dist/scanning/agents/csharp-agent.d.ts.map +1 -0
- package/dist/scanning/agents/csharp-agent.js +592 -0
- package/dist/scanning/agents/csharp-agent.js.map +1 -0
- package/dist/scanning/agents/go-agent.d.ts +14 -0
- package/dist/scanning/agents/go-agent.d.ts.map +1 -0
- package/dist/scanning/agents/go-agent.js +641 -0
- package/dist/scanning/agents/go-agent.js.map +1 -0
- package/dist/scanning/agents/java-agent.d.ts +8 -0
- package/dist/scanning/agents/java-agent.d.ts.map +1 -0
- package/dist/scanning/agents/java-agent.js +807 -0
- package/dist/scanning/agents/java-agent.js.map +1 -0
- package/dist/scanning/agents/javascript-agent.d.ts +8 -0
- package/dist/scanning/agents/javascript-agent.d.ts.map +1 -0
- package/dist/scanning/agents/javascript-agent.js +77 -0
- package/dist/scanning/agents/javascript-agent.js.map +1 -0
- package/dist/scanning/agents/mobile-agent.d.ts +8 -0
- package/dist/scanning/agents/mobile-agent.d.ts.map +1 -0
- package/dist/scanning/agents/mobile-agent.js +916 -0
- package/dist/scanning/agents/mobile-agent.js.map +1 -0
- package/dist/scanning/agents/php-agent.d.ts +8 -0
- package/dist/scanning/agents/php-agent.d.ts.map +1 -0
- package/dist/scanning/agents/php-agent.js +679 -0
- package/dist/scanning/agents/php-agent.js.map +1 -0
- package/dist/scanning/agents/python-agent.d.ts +8 -0
- package/dist/scanning/agents/python-agent.d.ts.map +1 -0
- package/dist/scanning/agents/python-agent.js +701 -0
- package/dist/scanning/agents/python-agent.js.map +1 -0
- package/dist/scanning/agents/supabase-agent.d.ts +8 -0
- package/dist/scanning/agents/supabase-agent.d.ts.map +1 -0
- package/dist/scanning/agents/supabase-agent.js +484 -0
- package/dist/scanning/agents/supabase-agent.js.map +1 -0
- package/dist/scanning/orchestrator.d.ts +29 -0
- package/dist/scanning/orchestrator.d.ts.map +1 -0
- package/dist/scanning/orchestrator.js +186 -0
- package/dist/scanning/orchestrator.js.map +1 -0
- package/dist/scanning/tech-detector.d.ts +12 -0
- package/dist/scanning/tech-detector.d.ts.map +1 -0
- package/dist/scanning/tech-detector.js +252 -0
- package/dist/scanning/tech-detector.js.map +1 -0
- package/dist/scanning/types.d.ts +87 -0
- package/dist/scanning/types.d.ts.map +1 -0
- package/dist/scanning/types.js +5 -0
- package/dist/scanning/types.js.map +1 -0
- package/dist/utils/child-process.d.ts +2 -0
- package/dist/utils/child-process.d.ts.map +1 -0
- package/dist/utils/child-process.js +4 -0
- package/dist/utils/child-process.js.map +1 -0
- package/dist/utils/context-classifier.d.ts +11 -0
- package/dist/utils/context-classifier.d.ts.map +1 -0
- package/dist/utils/context-classifier.js +79 -0
- package/dist/utils/context-classifier.js.map +1 -0
- package/dist/utils/exec.d.ts +18 -0
- package/dist/utils/exec.d.ts.map +1 -0
- package/dist/utils/exec.js +51 -0
- package/dist/utils/exec.js.map +1 -0
- package/dist/utils/file-filter.d.ts +4 -0
- package/dist/utils/file-filter.d.ts.map +1 -0
- package/dist/utils/file-filter.js +68 -0
- package/dist/utils/file-filter.js.map +1 -0
- package/dist/utils/fs.d.ts +2 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +5 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/post-processor.d.ts +10 -0
- package/dist/utils/post-processor.d.ts.map +1 -0
- package/dist/utils/post-processor.js +183 -0
- package/dist/utils/post-processor.js.map +1 -0
- package/package.json +44 -0
|
@@ -0,0 +1,916 @@
|
|
|
1
|
+
// Mobile ecosystem security scanner agent.
|
|
2
|
+
// Detects React Native, Expo, and Flutter projects and scans for
|
|
3
|
+
// mobile-specific security vulnerabilities: hardcoded keys, insecure storage,
|
|
4
|
+
// excessive permissions, debug code, missing certificate pinning, and more.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// File type helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
function isJSFile(path) {
|
|
9
|
+
return (path.endsWith(".js") ||
|
|
10
|
+
path.endsWith(".jsx") ||
|
|
11
|
+
path.endsWith(".ts") ||
|
|
12
|
+
path.endsWith(".tsx"));
|
|
13
|
+
}
|
|
14
|
+
function isDartFile(path) {
|
|
15
|
+
return path.endsWith(".dart");
|
|
16
|
+
}
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Agent class
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export class MobileScanAgent {
|
|
21
|
+
async detect(files) {
|
|
22
|
+
for (const [filePath, content] of files) {
|
|
23
|
+
// Expo / React Native: app.json containing "expo"
|
|
24
|
+
if (filePath === "app.json" && content.includes('"expo"')) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
// Flutter: pubspec.yaml containing "flutter"
|
|
28
|
+
if (filePath === "pubspec.yaml" && content.includes("flutter")) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
// Android: AndroidManifest.xml anywhere in the tree
|
|
32
|
+
if (filePath === "AndroidManifest.xml" || filePath.includes("AndroidManifest.xml")) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
// React Native: package.json with react-native dependency
|
|
36
|
+
if (filePath === "package.json" && content.includes('"react-native"')) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
async scan(files) {
|
|
43
|
+
const results = [];
|
|
44
|
+
// Project-level checks (run once, not per file)
|
|
45
|
+
results.push(...checkMissingCertificatePinning(files));
|
|
46
|
+
for (const [filePath, content] of files) {
|
|
47
|
+
// Check 1: Hardcoded API keys (JS + Dart files)
|
|
48
|
+
if (isJSFile(filePath) || isDartFile(filePath)) {
|
|
49
|
+
results.push(...checkHardcodedApiKeys(filePath, content));
|
|
50
|
+
}
|
|
51
|
+
// Check 2: AsyncStorage for sensitive data (JS/TS files only)
|
|
52
|
+
if (isJSFile(filePath)) {
|
|
53
|
+
results.push(...checkAsyncStorageSensitiveData(filePath, content));
|
|
54
|
+
results.push(...checkWebViewJavaScript(filePath, content));
|
|
55
|
+
results.push(...checkReactNativeDotenvExposure(filePath, content));
|
|
56
|
+
results.push(...checkExpoConstantsExposure(filePath, content));
|
|
57
|
+
results.push(...checkDebugCodeReactNative(filePath, content));
|
|
58
|
+
results.push(...checkBiometricWithoutServerVerification(filePath, content));
|
|
59
|
+
results.push(...checkInsecureDataStorageJS(filePath, content));
|
|
60
|
+
}
|
|
61
|
+
// Check 3: SharedPreferences for sensitive data (Dart files only)
|
|
62
|
+
if (isDartFile(filePath)) {
|
|
63
|
+
results.push(...checkSharedPreferencesSensitiveData(filePath, content));
|
|
64
|
+
results.push(...checkFlutterDebugEndpoint(filePath, content));
|
|
65
|
+
results.push(...checkDebugCodeFlutter(filePath, content));
|
|
66
|
+
}
|
|
67
|
+
// Check 4: Excessive Android permissions (AndroidManifest.xml)
|
|
68
|
+
if (filePath === "AndroidManifest.xml" || filePath.endsWith("/AndroidManifest.xml")) {
|
|
69
|
+
results.push(...checkExcessiveAndroidPermissions(filePath, content));
|
|
70
|
+
results.push(...checkDeepLinksWithoutVerification(filePath, content));
|
|
71
|
+
}
|
|
72
|
+
// Check 5: Expo config with secrets (app.json, app.config.js, eas.json)
|
|
73
|
+
if (filePath === "app.json" ||
|
|
74
|
+
filePath === "app.config.js" ||
|
|
75
|
+
filePath === "app.config.ts" ||
|
|
76
|
+
filePath === "eas.json") {
|
|
77
|
+
results.push(...checkExpoConfigWithSecrets(filePath, content));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
getMetadata() {
|
|
83
|
+
return {
|
|
84
|
+
name: "mobile-agent",
|
|
85
|
+
version: "1.0.0",
|
|
86
|
+
technologies: ["react-native", "expo", "dart", "flutter"],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
getChecks() {
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
id: "mobile:hardcoded-api-keys",
|
|
93
|
+
name: "Hardcoded API keys in mobile code",
|
|
94
|
+
severity: "CRITICAL",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "mobile:async-storage-sensitive-data",
|
|
98
|
+
name: "Sensitive data stored in AsyncStorage",
|
|
99
|
+
severity: "HIGH",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "mobile:shared-preferences-sensitive-data",
|
|
103
|
+
name: "Sensitive data stored in SharedPreferences",
|
|
104
|
+
severity: "HIGH",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "mobile:excessive-android-permissions",
|
|
108
|
+
name: "Excessive Android permissions declared",
|
|
109
|
+
severity: "MEDIUM",
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "mobile:expo-config-secrets",
|
|
113
|
+
name: "Secret value in Expo config extra block",
|
|
114
|
+
severity: "CRITICAL",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "mobile:deep-links-without-verification",
|
|
118
|
+
name: "Deep link intent-filter missing autoVerify",
|
|
119
|
+
severity: "HIGH",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "mobile:webview-javascript-enabled",
|
|
123
|
+
name: "WebView with JavaScript enabled and no URL whitelist",
|
|
124
|
+
severity: "HIGH",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: "mobile:missing-certificate-pinning",
|
|
128
|
+
name: "No certificate pinning detected",
|
|
129
|
+
severity: "MEDIUM",
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "mobile:debug-code-react-native",
|
|
133
|
+
name: "Excessive console.log calls without __DEV__ guard",
|
|
134
|
+
severity: "MEDIUM",
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "mobile:debug-code-flutter",
|
|
138
|
+
name: "Excessive print/debugPrint calls without kDebugMode guard",
|
|
139
|
+
severity: "MEDIUM",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: "mobile:biometric-without-server-verification",
|
|
143
|
+
name: "Biometric authentication without server-side verification",
|
|
144
|
+
severity: "MEDIUM",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "mobile:react-native-dotenv-exposure",
|
|
148
|
+
name: "Sensitive variable exposed via react-native-dotenv",
|
|
149
|
+
severity: "HIGH",
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: "mobile:flutter-debug-endpoint",
|
|
153
|
+
name: "Insecure HTTP endpoint in Flutter source",
|
|
154
|
+
severity: "LOW",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: "mobile:expo-constants-exposure",
|
|
158
|
+
name: "Sensitive key accessed via Expo Constants.extra",
|
|
159
|
+
severity: "HIGH",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: "mobile:insecure-data-storage-js",
|
|
163
|
+
name: "Insecure use of localStorage in React Native",
|
|
164
|
+
severity: "MEDIUM",
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Check 1: Hardcoded API keys (CWE-798)
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// Known key patterns with tight boundaries to reduce false positives.
|
|
173
|
+
const API_KEY_PATTERNS = [
|
|
174
|
+
{ name: "AWS Access Key", pattern: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
175
|
+
{ name: "Stripe Secret Key", pattern: /\bsk_(?:live|test)_[a-zA-Z0-9]{24,}\b/ },
|
|
176
|
+
{ name: "Firebase/Google API Key", pattern: /\bAIzaSy[a-zA-Z0-9_-]{33}\b/ },
|
|
177
|
+
// JWT: header.payload — eyJ...eyJ pattern
|
|
178
|
+
{ name: "JWT Token", pattern: /eyJ[a-zA-Z0-9_-]{20,}\.eyJ/ },
|
|
179
|
+
// Supabase service_role or anon key (JWT starting with eyJ)
|
|
180
|
+
{ name: "Supabase Service/Anon Key", pattern: /supabase[^'"\n]*(?:service_role|anon)[^'"\n]*eyJ[a-zA-Z0-9_-]{20,}/ },
|
|
181
|
+
];
|
|
182
|
+
// Placeholder / example strings that should not trigger alerts.
|
|
183
|
+
const PLACEHOLDER_PATTERNS = [
|
|
184
|
+
/your[_-]?(?:api[_-]?)?key/i,
|
|
185
|
+
/placeholder/i,
|
|
186
|
+
/xxx+/i,
|
|
187
|
+
/\*{4,}/i,
|
|
188
|
+
/\bexample\b/i,
|
|
189
|
+
/replace[_-]?me/i,
|
|
190
|
+
/\btest\b(?!_)/i,
|
|
191
|
+
/\bdummy\b/i,
|
|
192
|
+
];
|
|
193
|
+
function isPlaceholder(value) {
|
|
194
|
+
return PLACEHOLDER_PATTERNS.some((p) => p.test(value));
|
|
195
|
+
}
|
|
196
|
+
function isCommentLine(line) {
|
|
197
|
+
const trimmed = line.trim();
|
|
198
|
+
// JS/TS single-line comment
|
|
199
|
+
if (trimmed.startsWith("//"))
|
|
200
|
+
return true;
|
|
201
|
+
// Block comment fragments
|
|
202
|
+
if (trimmed.startsWith("*") || trimmed.startsWith("/*"))
|
|
203
|
+
return true;
|
|
204
|
+
// Dart/Yaml/Python hash comments
|
|
205
|
+
if (trimmed.startsWith("#"))
|
|
206
|
+
return true;
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
function checkHardcodedApiKeys(filePath, content) {
|
|
210
|
+
// Skip .env.example files — they are intentionally placeholder files.
|
|
211
|
+
if (filePath.includes(".env.example") || filePath.includes(".env.sample")) {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
// Skip test files.
|
|
215
|
+
if (filePath.includes("__tests__") ||
|
|
216
|
+
filePath.includes(".test.") ||
|
|
217
|
+
filePath.includes(".spec.")) {
|
|
218
|
+
return [];
|
|
219
|
+
}
|
|
220
|
+
const results = [];
|
|
221
|
+
const lines = content.split("\n");
|
|
222
|
+
for (let i = 0; i < lines.length; i++) {
|
|
223
|
+
const line = lines[i];
|
|
224
|
+
// Skip comment lines.
|
|
225
|
+
if (isCommentLine(line))
|
|
226
|
+
continue;
|
|
227
|
+
for (const { name, pattern } of API_KEY_PATTERNS) {
|
|
228
|
+
if (pattern.test(line)) {
|
|
229
|
+
// Extract the matched value and check if it is a placeholder.
|
|
230
|
+
const match = line.match(pattern);
|
|
231
|
+
if (match && isPlaceholder(match[0]))
|
|
232
|
+
continue;
|
|
233
|
+
results.push({
|
|
234
|
+
checkId: "mobile:hardcoded-api-keys",
|
|
235
|
+
title: `Hardcoded ${name} in source code`,
|
|
236
|
+
severity: "CRITICAL",
|
|
237
|
+
// AWS/Stripe keys have a specific, unambiguous format — high confidence.
|
|
238
|
+
// Firebase/Google key format is also very specific — high confidence.
|
|
239
|
+
confidence: "high",
|
|
240
|
+
file: filePath,
|
|
241
|
+
line: i + 1,
|
|
242
|
+
description: `A ${name} appears to be hardcoded directly in source code. If this repository is ever made public or the code is leaked, attackers gain direct access to your services.`,
|
|
243
|
+
fix: "Use environment variables or a secrets manager. Never commit API keys to source code.",
|
|
244
|
+
cwe: "CWE-798",
|
|
245
|
+
});
|
|
246
|
+
// Report one finding per pattern per line.
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// Check 2: AsyncStorage for sensitive data (CWE-922)
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
const SENSITIVE_KEY_PATTERN = /(?:token|password|secret|key|jwt|session|auth|credential)/i;
|
|
257
|
+
function checkAsyncStorageSensitiveData(filePath, content) {
|
|
258
|
+
// AsyncStorage is React Native specific — JS/TS files only.
|
|
259
|
+
if (!content.includes("AsyncStorage"))
|
|
260
|
+
return [];
|
|
261
|
+
const results = [];
|
|
262
|
+
const lines = content.split("\n");
|
|
263
|
+
for (let i = 0; i < lines.length; i++) {
|
|
264
|
+
const line = lines[i];
|
|
265
|
+
if (isCommentLine(line))
|
|
266
|
+
continue;
|
|
267
|
+
// Look for AsyncStorage.setItem( with a key that sounds sensitive.
|
|
268
|
+
if (/AsyncStorage\.setItem\s*\(/.test(line)) {
|
|
269
|
+
// Extract the first argument (the key).
|
|
270
|
+
const keyMatch = line.match(/AsyncStorage\.setItem\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
271
|
+
const keyName = keyMatch ? keyMatch[1] : line;
|
|
272
|
+
if (SENSITIVE_KEY_PATTERN.test(keyName)) {
|
|
273
|
+
results.push({
|
|
274
|
+
checkId: "mobile:async-storage-sensitive-data",
|
|
275
|
+
title: "Sensitive data stored in AsyncStorage",
|
|
276
|
+
severity: "HIGH",
|
|
277
|
+
// Key name matches a sensitive pattern — medium confidence.
|
|
278
|
+
// The actual value might be benign, but the key name is a clear signal.
|
|
279
|
+
confidence: "medium",
|
|
280
|
+
file: filePath,
|
|
281
|
+
line: i + 1,
|
|
282
|
+
description: `AsyncStorage is unencrypted and stores data in plaintext on the device filesystem. Storing sensitive values like "${keyName}" here exposes them to any app with file system access or physical device access.`,
|
|
283
|
+
fix: "Use react-native-keychain or expo-secure-store for sensitive data.",
|
|
284
|
+
cwe: "CWE-922",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return results;
|
|
290
|
+
}
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// Check 3: SharedPreferences for sensitive data (CWE-922)
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
function checkSharedPreferencesSensitiveData(filePath, content) {
|
|
295
|
+
if (!content.includes("SharedPreferences"))
|
|
296
|
+
return [];
|
|
297
|
+
// If the file uses a secure alternative, skip it.
|
|
298
|
+
if (content.includes("encrypted_shared_preferences") ||
|
|
299
|
+
content.includes("flutter_secure_storage")) {
|
|
300
|
+
return [];
|
|
301
|
+
}
|
|
302
|
+
const results = [];
|
|
303
|
+
const lines = content.split("\n");
|
|
304
|
+
for (let i = 0; i < lines.length; i++) {
|
|
305
|
+
const line = lines[i];
|
|
306
|
+
if (isCommentLine(line))
|
|
307
|
+
continue;
|
|
308
|
+
// Look for .setString( or .setInt( etc. with a sensitive key name.
|
|
309
|
+
if (/\bprefs\b.*\.set(?:String|Int|Bool|Double)\s*\(/.test(line) || /SharedPreferences.*set/.test(line)) {
|
|
310
|
+
const keyMatch = line.match(/\.set(?:String|Int|Bool|Double)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
311
|
+
const keyName = keyMatch ? keyMatch[1] : "";
|
|
312
|
+
if (SENSITIVE_KEY_PATTERN.test(keyName)) {
|
|
313
|
+
results.push({
|
|
314
|
+
checkId: "mobile:shared-preferences-sensitive-data",
|
|
315
|
+
title: "Sensitive data stored in SharedPreferences",
|
|
316
|
+
severity: "HIGH",
|
|
317
|
+
// Key name matches sensitive pattern — medium confidence.
|
|
318
|
+
confidence: "medium",
|
|
319
|
+
file: filePath,
|
|
320
|
+
line: i + 1,
|
|
321
|
+
description: `SharedPreferences stores data unencrypted on the device. Storing "${keyName}" here exposes it to rooted devices, backups, or apps with READ_EXTERNAL_STORAGE permission.`,
|
|
322
|
+
fix: "Use flutter_secure_storage or encrypted_shared_preferences for sensitive data.",
|
|
323
|
+
cwe: "CWE-922",
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return results;
|
|
329
|
+
}
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// Check 4: Excessive Android permissions (CWE-250)
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
const SENSITIVE_ANDROID_PERMISSIONS = [
|
|
334
|
+
"READ_CONTACTS",
|
|
335
|
+
"READ_SMS",
|
|
336
|
+
"READ_CALL_LOG",
|
|
337
|
+
"READ_PHONE_STATE",
|
|
338
|
+
"CAMERA",
|
|
339
|
+
"RECORD_AUDIO",
|
|
340
|
+
"ACCESS_FINE_LOCATION",
|
|
341
|
+
];
|
|
342
|
+
// Threshold: flag if 6 or more sensitive permissions exist.
|
|
343
|
+
// Common apps legitimately need 4-5; 6+ is a strong signal of over-permissioning.
|
|
344
|
+
const PERMISSION_THRESHOLD = 6;
|
|
345
|
+
function checkExcessiveAndroidPermissions(filePath, content) {
|
|
346
|
+
const foundPermissions = [];
|
|
347
|
+
for (const perm of SENSITIVE_ANDROID_PERMISSIONS) {
|
|
348
|
+
if (content.includes(perm)) {
|
|
349
|
+
foundPermissions.push(perm);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (foundPermissions.length < PERMISSION_THRESHOLD)
|
|
353
|
+
return [];
|
|
354
|
+
return [
|
|
355
|
+
{
|
|
356
|
+
checkId: "mobile:excessive-android-permissions",
|
|
357
|
+
title: "Excessive Android permissions declared",
|
|
358
|
+
severity: "MEDIUM",
|
|
359
|
+
// Absence-based finding — low confidence.
|
|
360
|
+
confidence: "low",
|
|
361
|
+
file: filePath,
|
|
362
|
+
line: 1,
|
|
363
|
+
description: `AndroidManifest.xml declares ${foundPermissions.length} sensitive permissions: ${foundPermissions.join(", ")}. Requesting more permissions than needed increases attack surface and privacy risk.`,
|
|
364
|
+
fix: "Request only permissions your app actually needs. Remove unused permissions.",
|
|
365
|
+
cwe: "CWE-250",
|
|
366
|
+
},
|
|
367
|
+
];
|
|
368
|
+
}
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
// Check 5: Expo config with secrets (CWE-798)
|
|
371
|
+
// ---------------------------------------------------------------------------
|
|
372
|
+
// Keys that indicate a real secret — must be a whole word or compound that is
|
|
373
|
+
// unambiguously secret-like. We exclude endpoint/url/host/path/base keys because
|
|
374
|
+
// those hold configuration values, not credentials.
|
|
375
|
+
//
|
|
376
|
+
// Strategy: match the JSON key name against an exact-word allowlist of secret
|
|
377
|
+
// indicators. The regex uses word boundaries to avoid matching "apiEndpoint" as
|
|
378
|
+
// containing "api" — only standalone "api" or compound forms like "api_key" match.
|
|
379
|
+
const SECRET_KEY_EXACT_WORDS = /^(?:secret|token|password|private_key|api_key|api_secret|apiKey|apiSecret|privateKey|accessToken|refreshToken|clientSecret)$/;
|
|
380
|
+
// Keys explicitly known to be safe configuration (not secrets).
|
|
381
|
+
const SAFE_EXPO_KEY_NAMES = new Set([
|
|
382
|
+
"apiUrl",
|
|
383
|
+
"apiEndpoint",
|
|
384
|
+
"apiHost",
|
|
385
|
+
"apiBase",
|
|
386
|
+
"apiPath",
|
|
387
|
+
"appId",
|
|
388
|
+
"appName",
|
|
389
|
+
"bundleId",
|
|
390
|
+
"environment",
|
|
391
|
+
"baseUrl",
|
|
392
|
+
]);
|
|
393
|
+
function isExpoConfigKeySecret(keyName) {
|
|
394
|
+
// Explicitly safe keys are never flagged.
|
|
395
|
+
if (SAFE_EXPO_KEY_NAMES.has(keyName))
|
|
396
|
+
return false;
|
|
397
|
+
// Match against exact secret key names.
|
|
398
|
+
if (SECRET_KEY_EXACT_WORDS.test(keyName))
|
|
399
|
+
return true;
|
|
400
|
+
// Fallback: key contains standalone secret/token/password as a word segment
|
|
401
|
+
// separated by underscores or camelCase boundaries.
|
|
402
|
+
return /(?:^|_)(?:secret|password|token|private_key|api_key)(?:_|$)/i.test(keyName);
|
|
403
|
+
}
|
|
404
|
+
function checkExpoConfigWithSecrets(filePath, content) {
|
|
405
|
+
// Only flag if there is an "extra" block.
|
|
406
|
+
if (!content.includes('"extra"') && !content.includes("extra:"))
|
|
407
|
+
return [];
|
|
408
|
+
const results = [];
|
|
409
|
+
const lines = content.split("\n");
|
|
410
|
+
// Track whether we are inside the "extra" block.
|
|
411
|
+
let insideExtra = false;
|
|
412
|
+
let braceDepth = 0;
|
|
413
|
+
for (let i = 0; i < lines.length; i++) {
|
|
414
|
+
const line = lines[i];
|
|
415
|
+
// Enter "extra" block.
|
|
416
|
+
if (/['"]extra['"]\s*:/.test(line)) {
|
|
417
|
+
insideExtra = true;
|
|
418
|
+
braceDepth = 0;
|
|
419
|
+
}
|
|
420
|
+
if (insideExtra) {
|
|
421
|
+
// Count braces to know when extra block ends.
|
|
422
|
+
for (const char of line) {
|
|
423
|
+
if (char === "{")
|
|
424
|
+
braceDepth++;
|
|
425
|
+
if (char === "}")
|
|
426
|
+
braceDepth--;
|
|
427
|
+
}
|
|
428
|
+
// If braceDepth drops below 0 we have left the extra block.
|
|
429
|
+
if (braceDepth < 0) {
|
|
430
|
+
insideExtra = false;
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
// Look for key: "value" pairs where key looks sensitive and value is not process.env.
|
|
434
|
+
const kvMatch = line.match(/['"](\w+)['"]\s*:\s*['"]([^'"]{4,})['"]/);
|
|
435
|
+
if (kvMatch) {
|
|
436
|
+
const keyName = kvMatch[1];
|
|
437
|
+
const value = kvMatch[2];
|
|
438
|
+
if (isExpoConfigKeySecret(keyName) && !isPlaceholder(value)) {
|
|
439
|
+
results.push({
|
|
440
|
+
checkId: "mobile:expo-config-secrets",
|
|
441
|
+
title: "Secret value in Expo config extra block",
|
|
442
|
+
severity: "CRITICAL",
|
|
443
|
+
// Specific key name matches a known secret pattern — high confidence.
|
|
444
|
+
confidence: "high",
|
|
445
|
+
file: filePath,
|
|
446
|
+
line: i + 1,
|
|
447
|
+
description: `The "${keyName}" field in the Expo config "extra" block contains a hardcoded value "${value.slice(0, 8)}...". The extra block is bundled into the app binary and can be extracted with basic tooling.`,
|
|
448
|
+
fix: "Use EAS Secrets or environment variables. Never put secrets in app.json/eas.json.",
|
|
449
|
+
cwe: "CWE-798",
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return results;
|
|
456
|
+
}
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// Check 6: Deep links without verification (CWE-939)
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
function checkDeepLinksWithoutVerification(filePath, content) {
|
|
461
|
+
const results = [];
|
|
462
|
+
// Split into intent-filter blocks for targeted analysis.
|
|
463
|
+
// Pattern: <intent-filter ...> ... </intent-filter>
|
|
464
|
+
const intentFilterRegex = /<intent-filter([^>]*)>([\s\S]*?)<\/intent-filter>/g;
|
|
465
|
+
let match;
|
|
466
|
+
// We need a line-number reference. Build a line-offset map.
|
|
467
|
+
const lines = content.split("\n");
|
|
468
|
+
const lineOffsets = [];
|
|
469
|
+
let offset = 0;
|
|
470
|
+
for (const line of lines) {
|
|
471
|
+
lineOffsets.push(offset);
|
|
472
|
+
offset += line.length + 1; // +1 for newline
|
|
473
|
+
}
|
|
474
|
+
function charOffsetToLine(charOffset) {
|
|
475
|
+
for (let i = lineOffsets.length - 1; i >= 0; i--) {
|
|
476
|
+
if (charOffset >= lineOffsets[i])
|
|
477
|
+
return i + 1;
|
|
478
|
+
}
|
|
479
|
+
return 1;
|
|
480
|
+
}
|
|
481
|
+
while ((match = intentFilterRegex.exec(content)) !== null) {
|
|
482
|
+
const attrs = match[1];
|
|
483
|
+
const body = match[2];
|
|
484
|
+
// Only flag HTTPS App Links (scheme="https" in data tag).
|
|
485
|
+
if (!body.includes('android:scheme="https"'))
|
|
486
|
+
continue;
|
|
487
|
+
// Check if autoVerify is already set on this intent-filter.
|
|
488
|
+
if (/android:autoVerify\s*=\s*["']true["']/.test(attrs))
|
|
489
|
+
continue;
|
|
490
|
+
const lineNumber = charOffsetToLine(match.index);
|
|
491
|
+
results.push({
|
|
492
|
+
checkId: "mobile:deep-links-without-verification",
|
|
493
|
+
title: "Deep link intent-filter missing autoVerify",
|
|
494
|
+
severity: "HIGH",
|
|
495
|
+
// Direct structural check — high confidence.
|
|
496
|
+
confidence: "high",
|
|
497
|
+
file: filePath,
|
|
498
|
+
line: lineNumber,
|
|
499
|
+
description: "An <intent-filter> handles HTTPS App Links but is missing android:autoVerify=\"true\". Without verification, any app can claim this URI scheme and intercept deep links, enabling link hijacking attacks.",
|
|
500
|
+
fix: 'Add android:autoVerify="true" to intent-filter for App Links verification.',
|
|
501
|
+
cwe: "CWE-939",
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
return results;
|
|
505
|
+
}
|
|
506
|
+
// ---------------------------------------------------------------------------
|
|
507
|
+
// Check 7: WebView with JavaScript enabled (CWE-749)
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
// Returns true if the originWhitelist value is a wildcard (unsafe).
|
|
510
|
+
// originWhitelist={['*']} or originWhitelist={["*"]} are wildcards.
|
|
511
|
+
// originWhitelist={["https://myapp.com"]} is specific — safe.
|
|
512
|
+
function isOriginWhitelistWildcard(content) {
|
|
513
|
+
// Match originWhitelist={[...]} to extract the array contents.
|
|
514
|
+
const match = content.match(/originWhitelist\s*=\s*\{?\s*\[([^\]]*)\]/);
|
|
515
|
+
if (!match)
|
|
516
|
+
return false;
|
|
517
|
+
const arrayContents = match[1].trim();
|
|
518
|
+
// Wildcard: only contains "*" (possibly with quotes/spaces)
|
|
519
|
+
return /^['"\s]*\*['"\s]*$/.test(arrayContents);
|
|
520
|
+
}
|
|
521
|
+
function checkWebViewJavaScript(filePath, content) {
|
|
522
|
+
// Only flag if WebView is used.
|
|
523
|
+
if (!content.includes("WebView") && !content.includes("react-native-webview")) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
// If javaScriptEnabled is not present, there is nothing to flag.
|
|
527
|
+
if (!content.includes("javaScriptEnabled"))
|
|
528
|
+
return [];
|
|
529
|
+
// If originWhitelist is present with specific domains (not wildcard), the developer
|
|
530
|
+
// is restricting URLs — skip. A wildcard originWhitelist={['*']} is still unsafe.
|
|
531
|
+
if (content.includes("originWhitelist")) {
|
|
532
|
+
if (!isOriginWhitelistWildcard(content)) {
|
|
533
|
+
// Specific domains listed — safe.
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
// Wildcard present — still flag, fall through.
|
|
537
|
+
}
|
|
538
|
+
const results = [];
|
|
539
|
+
const lines = content.split("\n");
|
|
540
|
+
for (let i = 0; i < lines.length; i++) {
|
|
541
|
+
const line = lines[i];
|
|
542
|
+
if (isCommentLine(line))
|
|
543
|
+
continue;
|
|
544
|
+
if (/javaScriptEnabled/.test(line)) {
|
|
545
|
+
results.push({
|
|
546
|
+
checkId: "mobile:webview-javascript-enabled",
|
|
547
|
+
title: "WebView with JavaScript enabled and no URL whitelist",
|
|
548
|
+
severity: "HIGH",
|
|
549
|
+
// Medium confidence — requires javaScriptEnabled without a real whitelist.
|
|
550
|
+
confidence: "medium",
|
|
551
|
+
file: filePath,
|
|
552
|
+
line: i + 1,
|
|
553
|
+
description: "WebView has JavaScript enabled without restricting the URLs it can load via originWhitelist. If a user can navigate to an attacker-controlled page, JavaScript can access native bridge APIs.",
|
|
554
|
+
fix: "Restrict WebView to trusted URLs with originWhitelist. Validate all URLs.",
|
|
555
|
+
cwe: "CWE-749",
|
|
556
|
+
});
|
|
557
|
+
// One finding per file is sufficient.
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return results;
|
|
562
|
+
}
|
|
563
|
+
// ---------------------------------------------------------------------------
|
|
564
|
+
// Check 8: Missing certificate pinning (CWE-295) — project-level check
|
|
565
|
+
// ---------------------------------------------------------------------------
|
|
566
|
+
// Indicators that the project makes network calls.
|
|
567
|
+
const NETWORK_CALL_INDICATORS = [
|
|
568
|
+
"fetch(",
|
|
569
|
+
"axios",
|
|
570
|
+
"http.get",
|
|
571
|
+
"http.post",
|
|
572
|
+
"Dio(",
|
|
573
|
+
"HttpClient",
|
|
574
|
+
];
|
|
575
|
+
// Indicators that certificate pinning is configured.
|
|
576
|
+
const CERT_PINNING_INDICATORS = [
|
|
577
|
+
"ssl-pinning",
|
|
578
|
+
"TrustKit",
|
|
579
|
+
"cert-pinner",
|
|
580
|
+
"rn-ssl-pinning",
|
|
581
|
+
"CertificatePinner",
|
|
582
|
+
"cert_pinner",
|
|
583
|
+
];
|
|
584
|
+
function checkMissingCertificatePinning(files) {
|
|
585
|
+
let hasNetworkCalls = false;
|
|
586
|
+
let hasPinning = false;
|
|
587
|
+
for (const [, content] of files) {
|
|
588
|
+
if (!hasNetworkCalls) {
|
|
589
|
+
for (const indicator of NETWORK_CALL_INDICATORS) {
|
|
590
|
+
if (content.includes(indicator)) {
|
|
591
|
+
hasNetworkCalls = true;
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (!hasPinning) {
|
|
597
|
+
for (const indicator of CERT_PINNING_INDICATORS) {
|
|
598
|
+
if (content.includes(indicator)) {
|
|
599
|
+
hasPinning = true;
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (hasNetworkCalls && hasPinning)
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
if (!hasNetworkCalls || hasPinning)
|
|
608
|
+
return [];
|
|
609
|
+
return [
|
|
610
|
+
{
|
|
611
|
+
checkId: "mobile:missing-certificate-pinning",
|
|
612
|
+
title: "No certificate pinning detected",
|
|
613
|
+
severity: "MEDIUM",
|
|
614
|
+
// Absence-based finding — low confidence. App may pin via native config.
|
|
615
|
+
confidence: "low",
|
|
616
|
+
file: "project",
|
|
617
|
+
line: 1,
|
|
618
|
+
description: "The app makes network requests but no certificate pinning library is configured. Without pinning, a network attacker with a trusted CA certificate can perform a man-in-the-middle attack and intercept all HTTPS traffic.",
|
|
619
|
+
fix: "Implement certificate pinning to prevent man-in-the-middle attacks.",
|
|
620
|
+
cwe: "CWE-295",
|
|
621
|
+
},
|
|
622
|
+
];
|
|
623
|
+
}
|
|
624
|
+
// ---------------------------------------------------------------------------
|
|
625
|
+
// Check 9: Debug code in production (CWE-489)
|
|
626
|
+
// ---------------------------------------------------------------------------
|
|
627
|
+
// Threshold: only flag files with 10+ debug calls.
|
|
628
|
+
// Occasional logging (< 10 calls) is normal in production code.
|
|
629
|
+
const DEBUG_CALL_THRESHOLD = 10;
|
|
630
|
+
function checkDebugCodeReactNative(filePath, content) {
|
|
631
|
+
// Skip test files.
|
|
632
|
+
if (filePath.includes("__tests__") ||
|
|
633
|
+
filePath.includes(".test.") ||
|
|
634
|
+
filePath.includes(".spec.")) {
|
|
635
|
+
return [];
|
|
636
|
+
}
|
|
637
|
+
// If the file checks __DEV__ or NODE_ENV, the developer is guarding debug code — skip.
|
|
638
|
+
if (content.includes("__DEV__") || content.includes("process.env.NODE_ENV"))
|
|
639
|
+
return [];
|
|
640
|
+
const lines = content.split("\n");
|
|
641
|
+
const callLines = [];
|
|
642
|
+
for (let i = 0; i < lines.length; i++) {
|
|
643
|
+
const line = lines[i];
|
|
644
|
+
if (isCommentLine(line))
|
|
645
|
+
continue;
|
|
646
|
+
if (/console\.log\s*\(/.test(line)) {
|
|
647
|
+
callLines.push(i + 1);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
if (callLines.length < DEBUG_CALL_THRESHOLD)
|
|
651
|
+
return [];
|
|
652
|
+
return [
|
|
653
|
+
{
|
|
654
|
+
checkId: "mobile:debug-code-react-native",
|
|
655
|
+
title: "Excessive console.log calls without __DEV__ guard",
|
|
656
|
+
severity: "MEDIUM",
|
|
657
|
+
// Count-based threshold — low confidence due to subjectivity.
|
|
658
|
+
confidence: "low",
|
|
659
|
+
file: filePath,
|
|
660
|
+
line: callLines[0],
|
|
661
|
+
description: `Found ${callLines.length} console.log() calls in production code without a __DEV__ guard. Debug logs in production can leak sensitive runtime data and degrade performance.`,
|
|
662
|
+
fix: "Wrap debug logging with __DEV__ (RN) or kDebugMode (Flutter) checks.",
|
|
663
|
+
cwe: "CWE-489",
|
|
664
|
+
},
|
|
665
|
+
];
|
|
666
|
+
}
|
|
667
|
+
function checkDebugCodeFlutter(filePath, content) {
|
|
668
|
+
// Skip test files.
|
|
669
|
+
if (filePath.includes("_test.dart") || filePath.includes("/test/"))
|
|
670
|
+
return [];
|
|
671
|
+
// If the file checks kDebugMode or kReleaseMode, skip.
|
|
672
|
+
if (content.includes("kDebugMode") || content.includes("kReleaseMode"))
|
|
673
|
+
return [];
|
|
674
|
+
const lines = content.split("\n");
|
|
675
|
+
const callLines = [];
|
|
676
|
+
for (let i = 0; i < lines.length; i++) {
|
|
677
|
+
const line = lines[i];
|
|
678
|
+
if (isCommentLine(line))
|
|
679
|
+
continue;
|
|
680
|
+
if (/\bprint\s*\(/.test(line) || /\bdebugPrint\s*\(/.test(line)) {
|
|
681
|
+
callLines.push(i + 1);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (callLines.length < DEBUG_CALL_THRESHOLD)
|
|
685
|
+
return [];
|
|
686
|
+
return [
|
|
687
|
+
{
|
|
688
|
+
checkId: "mobile:debug-code-flutter",
|
|
689
|
+
title: "Excessive print/debugPrint calls without kDebugMode guard",
|
|
690
|
+
severity: "MEDIUM",
|
|
691
|
+
// Count-based threshold — low confidence due to subjectivity.
|
|
692
|
+
confidence: "low",
|
|
693
|
+
file: filePath,
|
|
694
|
+
line: callLines[0],
|
|
695
|
+
description: `Found ${callLines.length} print()/debugPrint() calls in production Dart code without a kDebugMode guard. Debug logs in production can leak sensitive runtime data.`,
|
|
696
|
+
fix: "Wrap debug logging with __DEV__ (RN) or kDebugMode (Flutter) checks.",
|
|
697
|
+
cwe: "CWE-489",
|
|
698
|
+
},
|
|
699
|
+
];
|
|
700
|
+
}
|
|
701
|
+
// ---------------------------------------------------------------------------
|
|
702
|
+
// Check 10: Biometric auth without server verification (CWE-287)
|
|
703
|
+
// ---------------------------------------------------------------------------
|
|
704
|
+
const BIOMETRIC_CALL_PATTERNS = [
|
|
705
|
+
/authenticateAsync\s*\(/,
|
|
706
|
+
/authenticate\s*\(\)/,
|
|
707
|
+
/LocalAuthentication/,
|
|
708
|
+
/react-native-biometrics/,
|
|
709
|
+
/local_auth/,
|
|
710
|
+
];
|
|
711
|
+
// Look for an API call (fetch, axios, http, or a function name strongly suggesting
|
|
712
|
+
// server interaction) within PROXIMITY_LINES lines of the biometric call.
|
|
713
|
+
// This avoids false positives from generic uses of "token" elsewhere in the file.
|
|
714
|
+
const PROXIMITY_LINES = 20;
|
|
715
|
+
const SERVER_CALL_PATTERNS = [
|
|
716
|
+
/\bfetch\s*\(/,
|
|
717
|
+
/\baxios\s*\./,
|
|
718
|
+
/\bhttp\s*\./,
|
|
719
|
+
// Function calls whose name strongly implies server/verify interaction
|
|
720
|
+
/\bverify\w*(?:With|On|At)?Server\b/i,
|
|
721
|
+
/\bserver\w*(?:Verify|Validate|Confirm)\b/i,
|
|
722
|
+
/\bverifyWithServer\b/i,
|
|
723
|
+
/verif(?:y|ication)/i,
|
|
724
|
+
/\bserver\b/i,
|
|
725
|
+
];
|
|
726
|
+
function hasServerCallNearLine(lines, biometricLineIndex) {
|
|
727
|
+
const start = Math.max(0, biometricLineIndex - 2);
|
|
728
|
+
const end = Math.min(lines.length - 1, biometricLineIndex + PROXIMITY_LINES);
|
|
729
|
+
for (let i = start; i <= end; i++) {
|
|
730
|
+
const line = lines[i];
|
|
731
|
+
if (isCommentLine(line))
|
|
732
|
+
continue;
|
|
733
|
+
if (SERVER_CALL_PATTERNS.some((p) => p.test(line)))
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
function checkBiometricWithoutServerVerification(filePath, content) {
|
|
739
|
+
const hasBiometrics = BIOMETRIC_CALL_PATTERNS.some((p) => p.test(content));
|
|
740
|
+
if (!hasBiometrics)
|
|
741
|
+
return [];
|
|
742
|
+
const lines = content.split("\n");
|
|
743
|
+
for (let i = 0; i < lines.length; i++) {
|
|
744
|
+
const line = lines[i];
|
|
745
|
+
if (isCommentLine(line))
|
|
746
|
+
continue;
|
|
747
|
+
if (BIOMETRIC_CALL_PATTERNS.some((p) => p.test(line))) {
|
|
748
|
+
// Check within PROXIMITY_LINES for a server/API call.
|
|
749
|
+
if (hasServerCallNearLine(lines, i))
|
|
750
|
+
return [];
|
|
751
|
+
return [
|
|
752
|
+
{
|
|
753
|
+
checkId: "mobile:biometric-without-server-verification",
|
|
754
|
+
title: "Biometric authentication without server-side verification",
|
|
755
|
+
severity: "MEDIUM",
|
|
756
|
+
// Hard to verify via static analysis — low confidence.
|
|
757
|
+
confidence: "low",
|
|
758
|
+
file: filePath,
|
|
759
|
+
line: i + 1,
|
|
760
|
+
description: "Biometric authentication is performed but no server-side token verification is detected near the authentication call. Client-side-only biometrics can be bypassed on rooted/jailbroken devices by hooking the authentication result.",
|
|
761
|
+
fix: "Combine biometric auth with server-side token verification. Don't rely on client-side biometrics alone.",
|
|
762
|
+
cwe: "CWE-287",
|
|
763
|
+
},
|
|
764
|
+
];
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return [];
|
|
768
|
+
}
|
|
769
|
+
// ---------------------------------------------------------------------------
|
|
770
|
+
// Check 11: React Native dotenv exposure (CWE-200)
|
|
771
|
+
// ---------------------------------------------------------------------------
|
|
772
|
+
const DOTENV_SENSITIVE_NAMES = /(?:secret|key|token|password|api|private)/i;
|
|
773
|
+
function checkReactNativeDotenvExposure(filePath, content) {
|
|
774
|
+
// Look for import from react-native-dotenv or @env.
|
|
775
|
+
if (!content.includes("react-native-dotenv") && !content.includes("from '@env'") && !content.includes('from "@env"')) {
|
|
776
|
+
return [];
|
|
777
|
+
}
|
|
778
|
+
const results = [];
|
|
779
|
+
const lines = content.split("\n");
|
|
780
|
+
for (let i = 0; i < lines.length; i++) {
|
|
781
|
+
const line = lines[i];
|
|
782
|
+
if (isCommentLine(line))
|
|
783
|
+
continue;
|
|
784
|
+
if ((line.includes("react-native-dotenv") || line.includes("@env")) &&
|
|
785
|
+
DOTENV_SENSITIVE_NAMES.test(line)) {
|
|
786
|
+
results.push({
|
|
787
|
+
checkId: "mobile:react-native-dotenv-exposure",
|
|
788
|
+
title: "Sensitive variable exposed via react-native-dotenv",
|
|
789
|
+
severity: "HIGH",
|
|
790
|
+
// Medium confidence — import name matches sensitive pattern.
|
|
791
|
+
confidence: "medium",
|
|
792
|
+
file: filePath,
|
|
793
|
+
line: i + 1,
|
|
794
|
+
description: "react-native-dotenv bundles environment variables into the JavaScript bundle. Sensitive values imported via @env are visible to anyone who inspects the app binary.",
|
|
795
|
+
fix: "Only expose non-sensitive config via dotenv. Use a native module for secrets.",
|
|
796
|
+
cwe: "CWE-200",
|
|
797
|
+
});
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return results;
|
|
802
|
+
}
|
|
803
|
+
// ---------------------------------------------------------------------------
|
|
804
|
+
// Check 12: Flutter debug mode endpoint (CWE-489)
|
|
805
|
+
// ---------------------------------------------------------------------------
|
|
806
|
+
// Localhost and emulator addresses that are acceptable in HTTP context.
|
|
807
|
+
const LOCALHOST_PATTERNS = [
|
|
808
|
+
"localhost",
|
|
809
|
+
"127.0.0.1",
|
|
810
|
+
"10.0.2.2", // Android emulator loopback to host machine
|
|
811
|
+
];
|
|
812
|
+
function isLocalhostUrl(line) {
|
|
813
|
+
return LOCALHOST_PATTERNS.some((h) => line.includes(h));
|
|
814
|
+
}
|
|
815
|
+
function checkFlutterDebugEndpoint(filePath, content) {
|
|
816
|
+
// Skip test files.
|
|
817
|
+
if (filePath.includes("_test.dart") || filePath.includes("/test/"))
|
|
818
|
+
return [];
|
|
819
|
+
// If kReleaseMode or kDebugMode is used, the developer is conditionally setting endpoints.
|
|
820
|
+
if (content.includes("kReleaseMode") || content.includes("kDebugMode"))
|
|
821
|
+
return [];
|
|
822
|
+
const results = [];
|
|
823
|
+
const lines = content.split("\n");
|
|
824
|
+
for (let i = 0; i < lines.length; i++) {
|
|
825
|
+
const line = lines[i];
|
|
826
|
+
if (isCommentLine(line))
|
|
827
|
+
continue;
|
|
828
|
+
// Flag http:// URLs (not https://) — likely debug/dev endpoints.
|
|
829
|
+
// Exempt localhost, 127.0.0.1, and Android emulator address (10.0.2.2).
|
|
830
|
+
if (/['"]http:\/\//.test(line) && !isLocalhostUrl(line)) {
|
|
831
|
+
results.push({
|
|
832
|
+
checkId: "mobile:flutter-debug-endpoint",
|
|
833
|
+
title: "Insecure HTTP endpoint in Flutter source",
|
|
834
|
+
severity: "LOW",
|
|
835
|
+
// Low confidence — may be intentional for local/dev builds.
|
|
836
|
+
confidence: "low",
|
|
837
|
+
file: filePath,
|
|
838
|
+
line: i + 1,
|
|
839
|
+
description: "A non-localhost HTTP (not HTTPS) URL is present in production Dart code. This endpoint transmits data in plaintext and is likely a debug or development server URL left in the codebase.",
|
|
840
|
+
fix: "Use kReleaseMode to conditionally set API endpoints.",
|
|
841
|
+
cwe: "CWE-489",
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return results;
|
|
846
|
+
}
|
|
847
|
+
// ---------------------------------------------------------------------------
|
|
848
|
+
// Check 13: Expo Constants exposure (CWE-200)
|
|
849
|
+
// ---------------------------------------------------------------------------
|
|
850
|
+
const EXPO_CONSTANTS_SENSITIVE = /(?:secret|key|token|password|api)/i;
|
|
851
|
+
function checkExpoConstantsExposure(filePath, content) {
|
|
852
|
+
if (!content.includes("Constants.expoConfig") &&
|
|
853
|
+
!content.includes("Constants.manifest")) {
|
|
854
|
+
return [];
|
|
855
|
+
}
|
|
856
|
+
if (!content.includes(".extra"))
|
|
857
|
+
return [];
|
|
858
|
+
const results = [];
|
|
859
|
+
const lines = content.split("\n");
|
|
860
|
+
for (let i = 0; i < lines.length; i++) {
|
|
861
|
+
const line = lines[i];
|
|
862
|
+
if (isCommentLine(line))
|
|
863
|
+
continue;
|
|
864
|
+
if ((line.includes("Constants.expoConfig") || line.includes("Constants.manifest")) &&
|
|
865
|
+
line.includes(".extra") &&
|
|
866
|
+
EXPO_CONSTANTS_SENSITIVE.test(line)) {
|
|
867
|
+
results.push({
|
|
868
|
+
checkId: "mobile:expo-constants-exposure",
|
|
869
|
+
title: "Sensitive key accessed via Expo Constants.extra",
|
|
870
|
+
severity: "HIGH",
|
|
871
|
+
// Medium confidence — property name matches sensitive pattern.
|
|
872
|
+
confidence: "medium",
|
|
873
|
+
file: filePath,
|
|
874
|
+
line: i + 1,
|
|
875
|
+
description: "Secrets accessed via Constants.expoConfig.extra or Constants.manifest.extra are embedded in the app bundle and can be extracted by decompiling the JavaScript bundle.",
|
|
876
|
+
fix: "Don't store secrets in Expo config extra. Use expo-secure-store.",
|
|
877
|
+
cwe: "CWE-200",
|
|
878
|
+
});
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return results;
|
|
883
|
+
}
|
|
884
|
+
// ---------------------------------------------------------------------------
|
|
885
|
+
// Check 14: Insecure data storage (CWE-922)
|
|
886
|
+
// ---------------------------------------------------------------------------
|
|
887
|
+
function checkInsecureDataStorageJS(filePath, content) {
|
|
888
|
+
const results = [];
|
|
889
|
+
const lines = content.split("\n");
|
|
890
|
+
for (let i = 0; i < lines.length; i++) {
|
|
891
|
+
const line = lines[i];
|
|
892
|
+
if (isCommentLine(line))
|
|
893
|
+
continue;
|
|
894
|
+
// localStorage.setItem in React Native context — should use SecureStore.
|
|
895
|
+
if (/localStorage\.setItem\s*\(/.test(line)) {
|
|
896
|
+
const keyMatch = line.match(/localStorage\.setItem\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
897
|
+
const keyName = keyMatch ? keyMatch[1] : "";
|
|
898
|
+
if (SENSITIVE_KEY_PATTERN.test(keyName) || keyName === "") {
|
|
899
|
+
results.push({
|
|
900
|
+
checkId: "mobile:insecure-data-storage-js",
|
|
901
|
+
title: "Insecure use of localStorage in React Native",
|
|
902
|
+
severity: "MEDIUM",
|
|
903
|
+
// Medium confidence — key name matches sensitive pattern.
|
|
904
|
+
confidence: "medium",
|
|
905
|
+
file: filePath,
|
|
906
|
+
line: i + 1,
|
|
907
|
+
description: "localStorage is a web API that is not securely isolated on mobile platforms. In React Native, it maps to AsyncStorage (unencrypted). Sensitive data should use platform-specific secure storage.",
|
|
908
|
+
fix: "Use platform-specific secure storage APIs.",
|
|
909
|
+
cwe: "CWE-922",
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return results;
|
|
915
|
+
}
|
|
916
|
+
//# sourceMappingURL=mobile-agent.js.map
|