security-mcp 1.1.4 → 1.3.3
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/README.md +341 -1018
- package/defaults/checklists/ai.json +20 -1
- package/defaults/checklists/api.json +35 -1
- package/defaults/checklists/infra.json +34 -1
- package/defaults/checklists/mobile.json +23 -1
- package/defaults/checklists/payments.json +15 -1
- package/defaults/checklists/web.json +11 -1
- package/defaults/cloud-controls/aws.json +10712 -0
- package/defaults/cloud-controls/azure.json +7201 -0
- package/defaults/cloud-controls/gcp.json +4061 -0
- package/defaults/control-catalog.json +24 -0
- package/defaults/security-policy.json +2 -2
- package/dist/ci/pr-gate.js +22 -5
- package/dist/cli/index.js +73 -2
- package/dist/cli/install.js +4 -55
- package/dist/cli/onboarding.js +18 -10
- package/dist/gate/baseline.js +82 -7
- package/dist/gate/catalog.js +10 -2
- package/dist/gate/checks/agentic-instructions.js +515 -0
- package/dist/gate/checks/ai-governance.js +132 -0
- package/dist/gate/checks/ai.js +757 -39
- package/dist/gate/checks/auth-deep.js +920 -216
- package/dist/gate/checks/business-logic.js +751 -0
- package/dist/gate/checks/ci-pipeline.js +399 -4
- package/dist/gate/checks/cloud-controls.js +69 -0
- package/dist/gate/checks/crypto.js +423 -2
- package/dist/gate/checks/data-platform.js +954 -0
- package/dist/gate/checks/dependencies.js +582 -15
- package/dist/gate/checks/docker-deep.js +1236 -0
- package/dist/gate/checks/gitops.js +724 -0
- package/dist/gate/checks/graphql.js +201 -19
- package/dist/gate/checks/iac.js +1230 -0
- package/dist/gate/checks/infra.js +246 -1
- package/dist/gate/checks/injection-deep.js +827 -184
- package/dist/gate/checks/k8s.js +955 -2
- package/dist/gate/checks/mobile-android.js +917 -3
- package/dist/gate/checks/mobile-ios.js +797 -5
- package/dist/gate/checks/required-artifacts.js +194 -0
- package/dist/gate/checks/runtime.js +178 -0
- package/dist/gate/checks/secrets.js +256 -13
- package/dist/gate/checks/supply-chain-deep.js +787 -0
- package/dist/gate/checks/web-nextjs.js +572 -48
- package/dist/gate/cloud-controls/apply.js +115 -0
- package/dist/gate/cloud-controls/bicep.js +36 -0
- package/dist/gate/cloud-controls/cfn.js +125 -0
- package/dist/gate/cloud-controls/detect.js +104 -0
- package/dist/gate/cloud-controls/hcl.js +140 -0
- package/dist/gate/cloud-controls/types.js +87 -0
- package/dist/gate/diff.js +17 -5
- package/dist/gate/evidence.js +8 -1
- package/dist/gate/exceptions.js +202 -9
- package/dist/gate/findings.js +15 -2
- package/dist/gate/policy.js +316 -130
- package/dist/gate/threat-intel.js +6 -0
- package/dist/mcp/audit-chain.js +131 -28
- package/dist/mcp/auth.js +169 -0
- package/dist/mcp/learning.js +129 -4
- package/dist/mcp/model-router.js +161 -24
- package/dist/mcp/orchestration.js +377 -89
- package/dist/mcp/server.js +460 -69
- package/dist/mcp/tool-audit.js +193 -0
- package/dist/repo/fs.js +37 -1
- package/dist/repo/search.js +31 -6
- package/dist/review/store.js +56 -3
- package/dist/tests/run.js +124 -1
- package/package.json +9 -9
- package/skills/_TEMPLATE/SKILL.md +99 -0
- package/skills/advanced-dos-tester/SKILL.md +118 -0
- package/skills/agentic-instruction-auditor/SKILL.md +111 -0
- package/skills/agentic-loop-exploiter/SKILL.md +377 -0
- package/skills/ai-llm-redteam/SKILL.md +113 -0
- package/skills/ai-model-supply-chain-agent/SKILL.md +112 -0
- package/skills/algorithm-implementation-reviewer/SKILL.md +107 -0
- package/skills/android-penetration-tester/SKILL.md +464 -46
- package/skills/anti-replay-tester/SKILL.md +115 -0
- package/skills/appsec-code-auditor/SKILL.md +94 -0
- package/skills/artifact-integrity-analyst/SKILL.md +450 -0
- package/skills/attack-navigator/SKILL.md +476 -8
- package/skills/auth-session-hacker/SKILL.md +111 -0
- package/skills/aws-penetration-tester/SKILL.md +510 -0
- package/skills/azure-penetration-tester/SKILL.md +542 -3
- package/skills/binary-auth-validator/SKILL.md +120 -0
- package/skills/bot-detection-specialist/SKILL.md +118 -0
- package/skills/business-logic-attacker/SKILL.md +240 -0
- package/skills/capec-code-mapper/SKILL.md +93 -0
- package/skills/cert-pin-rotation-specialist/SKILL.md +121 -0
- package/skills/cicd-pipeline-hijacker/SKILL.md +414 -0
- package/skills/ciso-orchestrator/SKILL.md +465 -43
- package/skills/cloud-infra-specialist/SKILL.md +127 -0
- package/skills/compliance-gap-analyst/SKILL.md +431 -0
- package/skills/compliance-grc/SKILL.md +94 -0
- package/skills/compliance-lifecycle-tracker/SKILL.md +93 -0
- package/skills/container-hardening-auditor/SKILL.md +125 -0
- package/skills/credential-stuffing-specialist/SKILL.md +111 -0
- package/skills/crypto-pki-specialist/SKILL.md +96 -0
- package/skills/csa-ccm-mapper/SKILL.md +93 -0
- package/skills/csf2-governance-mapper/SKILL.md +93 -0
- package/skills/data-platform-auditor/SKILL.md +125 -0
- package/skills/deep-link-fuzzer/SKILL.md +118 -0
- package/skills/dependency-confusion-attacker/SKILL.md +424 -0
- package/skills/device-integrity-aggregator/SKILL.md +117 -0
- package/skills/dos-resilience-tester/SKILL.md +106 -0
- package/skills/dread-scorer/SKILL.md +93 -0
- package/skills/egress-policy-enforcer/SKILL.md +108 -0
- package/skills/evidence-collector/SKILL.md +107 -0
- package/skills/file-upload-attacker/SKILL.md +118 -0
- package/skills/gcp-penetration-tester/SKILL.md +510 -2
- package/skills/git-history-secret-scanner/SKILL.md +115 -0
- package/skills/gitops-delivery-auditor/SKILL.md +120 -0
- package/skills/iac-security-auditor/SKILL.md +125 -0
- package/skills/iam-privesc-graph-builder/SKILL.md +161 -0
- package/skills/incident-responder/SKILL.md +120 -0
- package/skills/injection-specialist/SKILL.md +111 -0
- package/skills/ios-security-auditor/SKILL.md +291 -0
- package/skills/json-ambiguity-tester/SKILL.md +145 -0
- package/skills/k8s-container-escaper/SKILL.md +406 -0
- package/skills/key-management-lifecycle-analyst/SKILL.md +107 -0
- package/skills/kill-switch-engineer/SKILL.md +111 -0
- package/skills/linddun-privacy-analyst/SKILL.md +111 -0
- package/skills/logic-race-fuzzer/SKILL.md +452 -0
- package/skills/mobile-api-network-attacker/SKILL.md +430 -0
- package/skills/mobile-binary-hardener/SKILL.md +111 -0
- package/skills/mobile-security-specialist/SKILL.md +94 -0
- package/skills/mobile-webview-auditor/SKILL.md +105 -0
- package/skills/model-extraction-attacker/SKILL.md +228 -0
- package/skills/multipart-abuse-tester/SKILL.md +93 -0
- package/skills/oauth-pkce-specialist/SKILL.md +113 -0
- package/skills/parser-exhaustion-tester/SKILL.md +151 -0
- package/skills/pentest-infra/SKILL.md +107 -0
- package/skills/pentest-social/SKILL.md +210 -0
- package/skills/pentest-team/SKILL.md +96 -0
- package/skills/pentest-web-api/SKILL.md +107 -0
- package/skills/privacy-flow-analyst/SKILL.md +243 -0
- package/skills/prompt-injection-specialist/SKILL.md +403 -0
- package/skills/quantum-migration-planner/SKILL.md +105 -0
- package/skills/rag-poisoning-specialist/SKILL.md +367 -0
- package/skills/registry-mirror-enforcer/SKILL.md +93 -0
- package/skills/rotation-validation-agent/SKILL.md +121 -0
- package/skills/samm-assessor/SKILL.md +94 -0
- package/skills/secrets-mask-bypass-tester/SKILL.md +109 -0
- package/skills/senior-security-engineer/SKILL.md +178 -0
- package/skills/serialization-memory-attacker/SKILL.md +341 -0
- package/skills/session-timeout-tester/SKILL.md +170 -0
- package/skills/slsa-level3-enforcer/SKILL.md +121 -0
- package/skills/slsa-provenance-enforcer/SKILL.md +111 -0
- package/skills/ssrf-detection-validator/SKILL.md +117 -0
- package/skills/step-up-auth-enforcer/SKILL.md +93 -0
- package/skills/stride-pasta-analyst/SKILL.md +429 -0
- package/skills/supply-chain-devsecops/SKILL.md +107 -0
- package/skills/threat-infrastructure-analyst/SKILL.md +93 -0
- package/skills/threat-modeler/SKILL.md +94 -0
- package/skills/tls-certificate-auditor/SKILL.md +582 -18
- package/skills/token-reuse-detector/SKILL.md +104 -0
- package/skills/trike-risk-modeler/SKILL.md +93 -0
- package/skills/unicode-homograph-tester/SKILL.md +93 -0
- package/skills/waf-rule-lifecycle-agent/SKILL.md +106 -0
- package/skills/webhook-security-tester/SKILL.md +111 -0
- package/skills/zero-trust-architect/SKILL.md +118 -0
|
@@ -1,8 +1,71 @@
|
|
|
1
1
|
import fg from "fast-glob";
|
|
2
2
|
import { readFileSafe } from "../../repo/fs.js";
|
|
3
|
-
|
|
3
|
+
import { searchRepo } from "../../repo/search.js";
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// File-discovery helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
async function findManifests() {
|
|
8
|
+
return fg(["**/AndroidManifest.xml"], {
|
|
9
|
+
dot: true,
|
|
10
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/build/**", "**/dist/**"]
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
async function findNetworkSecurityConfigs() {
|
|
14
|
+
return fg(["**/network_security_config.xml", "**/res/xml/network_security_config.xml"], {
|
|
15
|
+
dot: true,
|
|
16
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/build/**"]
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
async function findSourceFiles() {
|
|
20
|
+
return fg(["**/*.kt", "**/*.java"], {
|
|
21
|
+
dot: true,
|
|
22
|
+
ignore: [
|
|
23
|
+
"**/node_modules/**", "**/.git/**", "**/build/**",
|
|
24
|
+
"**/dist/**", "**/test/**", "**/androidTest/**"
|
|
25
|
+
]
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function findGradleFiles() {
|
|
29
|
+
return fg(["**/build.gradle", "**/build.gradle.kts"], {
|
|
30
|
+
dot: true,
|
|
31
|
+
ignore: ["**/node_modules/**", "**/.git/**"]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
async function findStringResources() {
|
|
35
|
+
return fg(["**/res/values/strings.xml", "**/res/values*.xml"], {
|
|
36
|
+
dot: true,
|
|
37
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/build/**"]
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
async function findProviderPathFiles() {
|
|
41
|
+
return fg(["**/res/xml/file_paths.xml", "**/res/xml/*_paths.xml", "**/res/xml/provider_paths.xml"], { dot: true, ignore: ["**/node_modules/**", "**/.git/**", "**/build/**"] });
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Text-search helpers
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
function grepLines(content, needle, limit = 10) {
|
|
47
|
+
const lower = needle.toLowerCase();
|
|
48
|
+
return content
|
|
49
|
+
.split("\n")
|
|
50
|
+
.filter(l => l.toLowerCase().includes(lower))
|
|
51
|
+
.slice(0, limit)
|
|
52
|
+
.map(l => l.trim());
|
|
53
|
+
}
|
|
54
|
+
function grepLinesRe(content, re, limit = 10) {
|
|
55
|
+
return content
|
|
56
|
+
.split("\n")
|
|
57
|
+
.filter(l => re.test(l))
|
|
58
|
+
.slice(0, limit)
|
|
59
|
+
.map(l => l.trim());
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Sub-checker: AndroidManifest.xml checks
|
|
63
|
+
// MASVS-RESILIENCE-2, MASVS-NETWORK-1, MASVS-STORAGE-2,
|
|
64
|
+
// MASVS-PLATFORM-1, MASVS-PLATFORM-3, MASVS-NETWORK-2
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
async function checkManifests() {
|
|
4
67
|
const findings = [];
|
|
5
|
-
const manifests = await
|
|
68
|
+
const manifests = await findManifests();
|
|
6
69
|
for (const m of manifests) {
|
|
7
70
|
const xml = await readFileSafe(m).catch(() => "");
|
|
8
71
|
const lower = xml.toLowerCase();
|
|
@@ -13,7 +76,7 @@ export async function checkMobileAndroid(_) {
|
|
|
13
76
|
severity: "CRITICAL",
|
|
14
77
|
files: [m],
|
|
15
78
|
requiredActions: [
|
|
16
|
-
|
|
79
|
+
'Remove android:debuggable="true" for release builds.',
|
|
17
80
|
"Ensure signing configs and build variants enforce non-debuggable release artifacts."
|
|
18
81
|
]
|
|
19
82
|
});
|
|
@@ -30,6 +93,857 @@ export async function checkMobileAndroid(_) {
|
|
|
30
93
|
]
|
|
31
94
|
});
|
|
32
95
|
}
|
|
96
|
+
if (lower.includes('android:allowbackup="true"')) {
|
|
97
|
+
findings.push({
|
|
98
|
+
id: "ANDROID_BACKUP_ALLOWED",
|
|
99
|
+
title: "Android allowBackup enabled — ADB backup can extract app data without root",
|
|
100
|
+
severity: "HIGH",
|
|
101
|
+
files: [m],
|
|
102
|
+
evidence: grepLines(xml, 'allowBackup="true"'),
|
|
103
|
+
requiredActions: [
|
|
104
|
+
'Set android:allowBackup="false" in <application> unless full backup rules are defined.',
|
|
105
|
+
"If selective backup is required, use android:fullBackupContent and exclude sensitive files.",
|
|
106
|
+
"Review MASVS-STORAGE-2 for data backup guidance."
|
|
107
|
+
]
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const exportedLines = collectExportedWithoutPermission(xml);
|
|
111
|
+
if (exportedLines.length > 0) {
|
|
112
|
+
findings.push({
|
|
113
|
+
id: "ANDROID_EXPORTED_NO_PERMISSION",
|
|
114
|
+
title: "Exported component(s) have no android:permission — unauthorized invocation possible",
|
|
115
|
+
severity: "HIGH",
|
|
116
|
+
files: [m],
|
|
117
|
+
evidence: exportedLines.slice(0, 5),
|
|
118
|
+
requiredActions: [
|
|
119
|
+
"Add android:permission to every exported Activity, Service, Receiver, and Provider.",
|
|
120
|
+
'Use a signature-level permission (android:protectionLevel="signature") for internal IPC.',
|
|
121
|
+
"Audit all exported components against MASVS-PLATFORM-1 and OWASP M1."
|
|
122
|
+
]
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const deepLinkEvidence = collectUnverifiedDeepLinks(xml);
|
|
126
|
+
if (deepLinkEvidence.length > 0) {
|
|
127
|
+
findings.push({
|
|
128
|
+
id: "ANDROID_DEEPLINK_NO_VERIFY",
|
|
129
|
+
title: 'Deep link intent-filter missing android:autoVerify="true" — App Links unverified',
|
|
130
|
+
severity: "HIGH",
|
|
131
|
+
files: [m],
|
|
132
|
+
evidence: deepLinkEvidence.slice(0, 3),
|
|
133
|
+
requiredActions: [
|
|
134
|
+
'Add android:autoVerify="true" to all https-scheme intent-filters.',
|
|
135
|
+
"Host a valid .well-known/assetlinks.json on the linked domain.",
|
|
136
|
+
"Test verification with: adb shell pm get-app-links --user 0 <package>"
|
|
137
|
+
]
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
if (!/android:networkSecurityConfig\s*=/.test(xml)) {
|
|
141
|
+
findings.push({
|
|
142
|
+
id: "ANDROID_NSC_MISSING",
|
|
143
|
+
title: "No networkSecurityConfig referenced in AndroidManifest — relying on platform defaults",
|
|
144
|
+
severity: "HIGH",
|
|
145
|
+
files: [m],
|
|
146
|
+
requiredActions: [
|
|
147
|
+
"Create res/xml/network_security_config.xml and reference it in <application> via android:networkSecurityConfig.",
|
|
148
|
+
"Define base-config with cleartextTrafficPermitted=false and restrict trust anchors to the system store.",
|
|
149
|
+
"See MASVS-NETWORK-2 for a compliant template."
|
|
150
|
+
]
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return findings;
|
|
155
|
+
}
|
|
156
|
+
function collectExportedWithoutPermission(xml) {
|
|
157
|
+
const re = /<(activity|service|receiver|provider)[^>]*android:exported\s*=\s*"true"[^>]*>/gi;
|
|
158
|
+
const results = [];
|
|
159
|
+
let m;
|
|
160
|
+
while ((m = re.exec(xml)) !== null) {
|
|
161
|
+
if (!/android:permission\s*=/.test(m[0])) {
|
|
162
|
+
results.push(m[0].slice(0, 200).trim());
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
167
|
+
function collectUnverifiedDeepLinks(xml) {
|
|
168
|
+
const re = /<intent-filter[^>]*>[\s\S]*?<data[^>]*android:scheme\s*=\s*"https?"[^>]*>[\s\S]*?<\/intent-filter>/gi;
|
|
169
|
+
const results = [];
|
|
170
|
+
let m;
|
|
171
|
+
while ((m = re.exec(xml)) !== null) {
|
|
172
|
+
if (!/android:autoVerify\s*=\s*"true"/i.test(m[0])) {
|
|
173
|
+
results.push(m[0].slice(0, 200).trim());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Sub-checker: Network Security Config checks
|
|
180
|
+
// MASVS-NETWORK-2
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
/** Build NSC_WEAK evidence lines from an NSC XML string. Returns null if no weakness found. */
|
|
183
|
+
function nscWeakEvidence(xml) {
|
|
184
|
+
const hasCleartextDomain = /cleartexttrafficpermitted\s*=\s*"true"/i.test(xml);
|
|
185
|
+
const hasUserCerts = /<certificates\s+src\s*=\s*"user"/i.test(xml);
|
|
186
|
+
const hasSystemOnly = /<certificates\s+src\s*=\s*"system"\s*\/>/i.test(xml);
|
|
187
|
+
const hasBroadTrust = /<trust-anchors>/i.test(xml) && !hasSystemOnly;
|
|
188
|
+
if (!hasCleartextDomain && !hasUserCerts && !hasBroadTrust)
|
|
189
|
+
return null;
|
|
190
|
+
const evidence = [];
|
|
191
|
+
if (hasCleartextDomain)
|
|
192
|
+
evidence.push('cleartextTrafficPermitted="true" found in domain config');
|
|
193
|
+
if (hasUserCerts)
|
|
194
|
+
evidence.push('<certificates src="user"> trust anchor allows user-installed CAs');
|
|
195
|
+
if (hasBroadTrust)
|
|
196
|
+
evidence.push(...grepLines(xml, "<trust-anchors>", 3));
|
|
197
|
+
return evidence;
|
|
198
|
+
}
|
|
199
|
+
/** Returns a CERT_PINNING_MISSING finding for `nsc` if no pinning is detected anywhere. */
|
|
200
|
+
async function checkNscPinning(nsc, xml) {
|
|
201
|
+
if (xml.toLowerCase().includes("<pin-set"))
|
|
202
|
+
return null;
|
|
203
|
+
const [okHttp, trustKit] = await Promise.all([
|
|
204
|
+
searchRepo({ query: "CertificatePinner", isRegex: false, maxMatches: 3 }),
|
|
205
|
+
searchRepo({ query: "TrustKit", isRegex: false, maxMatches: 3 })
|
|
206
|
+
]);
|
|
207
|
+
if (okHttp.length > 0 || trustKit.length > 0)
|
|
208
|
+
return null;
|
|
209
|
+
return {
|
|
210
|
+
id: "ANDROID_CERT_PINNING_MISSING",
|
|
211
|
+
title: "No certificate pinning found in NSC or OkHttp/TrustKit",
|
|
212
|
+
severity: "HIGH",
|
|
213
|
+
files: [nsc],
|
|
214
|
+
requiredActions: [
|
|
215
|
+
"Add a <pin-set> to network_security_config.xml for production domains, or",
|
|
216
|
+
"Configure OkHttp CertificatePinner / TrustKit for high-risk API endpoints.",
|
|
217
|
+
"Include backup pins and a rotation plan. See MASVS-NETWORK-2."
|
|
218
|
+
]
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
async function checkNetworkSecurityConfig() {
|
|
222
|
+
const findings = [];
|
|
223
|
+
for (const nsc of await findNetworkSecurityConfigs()) {
|
|
224
|
+
const xml = await readFileSafe(nsc).catch(() => "");
|
|
225
|
+
if (!xml)
|
|
226
|
+
continue;
|
|
227
|
+
const weakEvidence = nscWeakEvidence(xml);
|
|
228
|
+
if (weakEvidence !== null) {
|
|
229
|
+
findings.push({
|
|
230
|
+
id: "ANDROID_NSC_WEAK",
|
|
231
|
+
title: "Network Security Config has weakened trust settings (user CAs or domain cleartext)",
|
|
232
|
+
severity: "CRITICAL",
|
|
233
|
+
files: [nsc],
|
|
234
|
+
evidence: weakEvidence,
|
|
235
|
+
requiredActions: [
|
|
236
|
+
'Remove <certificates src="user"> trust anchors — they allow MITM by any user-installed CA.',
|
|
237
|
+
'Remove domain-level cleartextTrafficPermitted="true" entries.',
|
|
238
|
+
"Restrict <base-config> to system CAs only and enforce TLS globally.",
|
|
239
|
+
"Reference MASVS-NETWORK-2 and OWASP M3."
|
|
240
|
+
]
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
const pinFinding = await checkNscPinning(nsc, xml);
|
|
244
|
+
if (pinFinding !== null)
|
|
245
|
+
findings.push(pinFinding);
|
|
246
|
+
}
|
|
247
|
+
return findings;
|
|
248
|
+
}
|
|
249
|
+
function newAccumulator() {
|
|
250
|
+
return { files: [], evidence: [] };
|
|
251
|
+
}
|
|
252
|
+
function recordHit(acc, src, lines) {
|
|
253
|
+
acc.files.push(src);
|
|
254
|
+
acc.evidence.push(...lines.map(l => `${src}: ${l}`));
|
|
255
|
+
}
|
|
256
|
+
function scanWebviewJsi(code, src, acc) {
|
|
257
|
+
if (!code.includes("addJavascriptInterface"))
|
|
258
|
+
return;
|
|
259
|
+
recordHit(acc, src, grepLines(code, "addJavascriptInterface", 5));
|
|
260
|
+
}
|
|
261
|
+
function scanWebviewJs(code, src, acc) {
|
|
262
|
+
if (!code.includes("setJavaScriptEnabled(true)"))
|
|
263
|
+
return;
|
|
264
|
+
if (code.includes("setSaveFormData(false)") && code.includes("setSavePassword(false)"))
|
|
265
|
+
return;
|
|
266
|
+
recordHit(acc, src, grepLines(code, "setJavaScriptEnabled", 3));
|
|
267
|
+
}
|
|
268
|
+
function scanSharedPrefs(code, src, acc, sensitiveNeedles) {
|
|
269
|
+
if (!code.includes("getSharedPreferences") && !code.includes("defaultSharedPreferences"))
|
|
270
|
+
return;
|
|
271
|
+
if (code.includes("EncryptedSharedPreferences"))
|
|
272
|
+
return;
|
|
273
|
+
const spLines = grepLinesRe(code, /getSharedPreferences|defaultSharedPreferences/i, 20);
|
|
274
|
+
const sensitiveLinesNear = spLines.some(l => sensitiveNeedles.some(n => l.toLowerCase().includes(n)));
|
|
275
|
+
const codeLower = code.toLowerCase();
|
|
276
|
+
const idx = codeLower.indexOf("getsharedpreferences");
|
|
277
|
+
const windowText = idx === -1 ? "" : codeLower.slice(Math.max(0, idx - 300), idx + 300);
|
|
278
|
+
const hasSensitiveNearby = sensitiveNeedles.some(n => windowText.includes(n));
|
|
279
|
+
if (sensitiveLinesNear || hasSensitiveNearby) {
|
|
280
|
+
recordHit(acc, src, grepLines(code, "getSharedPreferences", 3));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function scanLogcat(code, src, acc, sensitiveNeedles) {
|
|
284
|
+
const logRe = /Log\s*\.\s*[diwve]\s*\(.*?(password|token|secret|apikey|credential)/i;
|
|
285
|
+
if (!logRe.test(code))
|
|
286
|
+
return;
|
|
287
|
+
const lines = grepLinesRe(code, /Log\s*\.\s*[diwve]/i, 5)
|
|
288
|
+
.filter(l => sensitiveNeedles.some(n => l.toLowerCase().includes(n)));
|
|
289
|
+
if (lines.length > 0)
|
|
290
|
+
recordHit(acc, src, lines);
|
|
291
|
+
}
|
|
292
|
+
function scanHardcodedSecret(code, src, acc) {
|
|
293
|
+
const re = /(apiKey|api_key|secret|password|token)\s*[=:]\s*["'][^"']{8,}/i;
|
|
294
|
+
if (!re.test(code))
|
|
295
|
+
return;
|
|
296
|
+
const lines = grepLinesRe(code, re, 5);
|
|
297
|
+
if (lines.length > 0)
|
|
298
|
+
recordHit(acc, src, lines);
|
|
299
|
+
}
|
|
300
|
+
function scanRawQuery(code, src, acc) {
|
|
301
|
+
if (!/rawQuery|execSQL/.test(code))
|
|
302
|
+
return;
|
|
303
|
+
if (!/rawQuery.*\+|execSQL.*\+/.test(code))
|
|
304
|
+
return;
|
|
305
|
+
recordHit(acc, src, grepLinesRe(code, /rawQuery.*\+|execSQL.*\+/, 5));
|
|
306
|
+
}
|
|
307
|
+
function scanImplicitIntent(code, src, acc) {
|
|
308
|
+
const re = /new\s+Intent\s*\(\s*["'][^"']+["']\s*\)/i;
|
|
309
|
+
if (!re.test(code))
|
|
310
|
+
return;
|
|
311
|
+
recordHit(acc, src, grepLinesRe(code, re, 5));
|
|
312
|
+
}
|
|
313
|
+
function scanPendingIntentMutable(code, src, acc) {
|
|
314
|
+
if (!code.includes("FLAG_MUTABLE"))
|
|
315
|
+
return;
|
|
316
|
+
recordHit(acc, src, grepLines(code, "FLAG_MUTABLE", 5));
|
|
317
|
+
}
|
|
318
|
+
function scanExternalStorage(code, src, acc) {
|
|
319
|
+
if (!/getExternalStorageDirectory|getExternalFilesDir/.test(code))
|
|
320
|
+
return;
|
|
321
|
+
recordHit(acc, src, grepLinesRe(code, /getExternalStorageDirectory|getExternalFilesDir/, 5));
|
|
322
|
+
}
|
|
323
|
+
function scanBiometricWeak(code, src, acc) {
|
|
324
|
+
if (!code.includes("BiometricPrompt"))
|
|
325
|
+
return;
|
|
326
|
+
if (code.includes("CryptoObject"))
|
|
327
|
+
return;
|
|
328
|
+
recordHit(acc, src, grepLines(code, "BiometricPrompt", 3));
|
|
329
|
+
}
|
|
330
|
+
function emitSourceFindings(accs) {
|
|
331
|
+
const { jsi, jsEnabled, sharedPrefs, logcat, hardcoded, rawQuery, implicitIntent, pendingIntent, externalStorage, biometric } = accs;
|
|
332
|
+
const findings = [];
|
|
333
|
+
const push = (f) => findings.push(f);
|
|
334
|
+
const files = (acc) => [...new Set(acc.files)];
|
|
335
|
+
const ev = (acc) => acc.evidence.slice(0, 8);
|
|
336
|
+
if (jsi.files.length > 0)
|
|
337
|
+
push({ id: "ANDROID_WEBVIEW_JS_INTERFACE", title: "addJavascriptInterface exposes Java methods to WebView JavaScript", severity: "CRITICAL", files: files(jsi), evidence: ev(jsi), requiredActions: ["Remove addJavascriptInterface unless the WebView loads only trusted, local content.", "If required, annotate only the specific methods with @JavascriptInterface and validate all inputs.", "Ensure minSdkVersion >= 17 where the annotation is the attack-surface gate.", "See MASVS-PLATFORM-7 and OWASP M1."] });
|
|
338
|
+
if (jsEnabled.files.length > 0)
|
|
339
|
+
push({ id: "ANDROID_WEBVIEW_JS_ENABLED", title: "WebView has JavaScript enabled without full hardening (setSaveFormData/setSavePassword false)", severity: "HIGH", files: files(jsEnabled), evidence: ev(jsEnabled), requiredActions: ["Call setSaveFormData(false) and setSavePassword(false) wherever setJavaScriptEnabled(true) is called.", "Also set setAllowFileAccessFromFileURLs(false) and setAllowUniversalAccessFromFileURLs(false).", "Load only HTTPS content and validate URLs before loading. See MASVS-PLATFORM-7."] });
|
|
340
|
+
if (sharedPrefs.files.length > 0)
|
|
341
|
+
push({ id: "ANDROID_SHARED_PREFS_SENSITIVE", title: "Sensitive data (password/token/secret) stored in unencrypted SharedPreferences", severity: "HIGH", files: files(sharedPrefs), evidence: ev(sharedPrefs), requiredActions: ["Replace SharedPreferences with EncryptedSharedPreferences (Jetpack Security) for all secrets.", "Never store passwords, tokens, or private keys in plaintext on-device.", "See MASVS-STORAGE-1 and OWASP M2."] });
|
|
342
|
+
if (logcat.files.length > 0)
|
|
343
|
+
push({ id: "ANDROID_LOGCAT_SENSITIVE", title: "Sensitive data (password/token/secret) logged via Logcat", severity: "HIGH", files: files(logcat), evidence: ev(logcat), requiredActions: ["Remove all Log.d/i/w/e/v calls that include passwords, tokens, or secrets.", "In ProGuard/R8 rules, strip all Log calls for release builds as a safety net.", "See MASVS-RESILIENCE-3 and OWASP M2."] });
|
|
344
|
+
if (hardcoded.files.length > 0)
|
|
345
|
+
push({ id: "ANDROID_HARDCODED_SECRET", title: "Hardcoded API key / secret / password found in Kotlin/Java source", severity: "CRITICAL", files: files(hardcoded), evidence: ev(hardcoded), requiredActions: ["Remove hardcoded credentials immediately. Rotate any exposed keys.", "Load secrets at runtime from a secure backend or Android Keystore.", "For CI, inject secrets via environment variables — never commit them to source control.", "See MASVS-STORAGE-14 and OWASP M9."] });
|
|
346
|
+
if (rawQuery.files.length > 0)
|
|
347
|
+
push({ id: "ANDROID_SQL_RAW_QUERY", title: "rawQuery / execSQL called with string concatenation — potential SQLite injection", severity: "HIGH", files: files(rawQuery), evidence: ev(rawQuery), requiredActions: ["Replace string-concatenated queries with parameterized placeholders (rawQuery(sql, selectionArgs)).", "Prefer Room DAO @Query methods which enforce parameterization by default.", "See MASVS-CODE-4 and CWE-89."] });
|
|
348
|
+
if (implicitIntent.files.length > 0)
|
|
349
|
+
push({ id: "ANDROID_INTENT_IMPLICIT", title: "Implicit Intent detected — may be intercepted by a malicious app", severity: "HIGH", files: files(implicitIntent), evidence: ev(implicitIntent), requiredActions: ["Use explicit intents (specifying target class or ComponentName) for intra-app communication.", "If broadcasting, use LocalBroadcastManager or sendBroadcast with permissions.", "See MASVS-PLATFORM-2 and OWASP M1."] });
|
|
350
|
+
if (pendingIntent.files.length > 0)
|
|
351
|
+
push({ id: "ANDROID_PENDING_INTENT_MUTABLE", title: "PendingIntent.FLAG_MUTABLE used — wrapped Intent can be modified by third-party apps", severity: "HIGH", files: files(pendingIntent), evidence: ev(pendingIntent), requiredActions: ["Prefer FLAG_IMMUTABLE for PendingIntents unless the wrapped Intent must be filled in by another app.", "If FLAG_MUTABLE is truly required, use explicit Intents inside the PendingIntent and validate all Intent fields on receipt.", "See Android documentation on PendingIntent mutability and MASVS-PLATFORM-2."] });
|
|
352
|
+
if (externalStorage.files.length > 0)
|
|
353
|
+
push({ id: "ANDROID_EXTERNAL_STORAGE", title: "Sensitive data potentially written to world-readable external storage", severity: "HIGH", files: files(externalStorage), evidence: ev(externalStorage), requiredActions: ["Write sensitive data only to internal storage (filesDir, cacheDir) or Android Keystore-backed encrypted files.", "If external storage is necessary, encrypt the data before writing using Jetpack Security.", "See MASVS-STORAGE-2 and OWASP M2."] });
|
|
354
|
+
if (biometric.files.length > 0)
|
|
355
|
+
push({ id: "ANDROID_BIOMETRIC_WEAK", title: "BiometricPrompt used without CryptoObject — authentication result not bound to a key", severity: "MEDIUM", files: files(biometric), evidence: ev(biometric), requiredActions: ["Pass a CryptoObject backed by an Android Keystore key (KeyPermanentlyInvalidatedException aware) to BiometricPrompt.authenticate().", "This ensures the cryptographic operation only succeeds on genuine biometric confirmation.", "See MASVS-AUTH-2 and Android BiometricPrompt best practices."] });
|
|
356
|
+
return findings;
|
|
357
|
+
}
|
|
358
|
+
async function checkBillingClientOnly(billingFiles) {
|
|
359
|
+
if (billingFiles.length === 0)
|
|
360
|
+
return null;
|
|
361
|
+
const matches = await searchRepo({ query: "purchaseToken", isRegex: false, maxMatches: 5 });
|
|
362
|
+
const hasServerEndpoint = matches.some(r => /retrofit|okhttp|httpurlconnection|volley|ktor|api|endpoint|server/i.test(r.preview));
|
|
363
|
+
if (hasServerEndpoint)
|
|
364
|
+
return null;
|
|
365
|
+
return {
|
|
366
|
+
id: "ANDROID_IN_APP_PURCHASE_CLIENT_ONLY",
|
|
367
|
+
title: "BillingClient detected but no server-side purchase validation found",
|
|
368
|
+
severity: "HIGH",
|
|
369
|
+
files: billingFiles,
|
|
370
|
+
requiredActions: [
|
|
371
|
+
"Validate every purchase server-side using the Google Play Developer API (/purchases/products or /purchases/subscriptions).",
|
|
372
|
+
"Pass the purchaseToken and productId to your backend — never trust client-only verification.",
|
|
373
|
+
"See MASVS-RESILIENCE-3 and Google Play billing best practices."
|
|
374
|
+
]
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async function checkSourceFiles() {
|
|
378
|
+
const sensitiveNeedles = ["password", "token", "secret", "apikey", "api_key", "credential", "auth_token"];
|
|
379
|
+
const accs = {
|
|
380
|
+
jsi: newAccumulator(),
|
|
381
|
+
jsEnabled: newAccumulator(),
|
|
382
|
+
sharedPrefs: newAccumulator(),
|
|
383
|
+
logcat: newAccumulator(),
|
|
384
|
+
hardcoded: newAccumulator(),
|
|
385
|
+
rawQuery: newAccumulator(),
|
|
386
|
+
implicitIntent: newAccumulator(),
|
|
387
|
+
pendingIntent: newAccumulator(),
|
|
388
|
+
externalStorage: newAccumulator(),
|
|
389
|
+
biometric: newAccumulator(),
|
|
390
|
+
billingFiles: []
|
|
391
|
+
};
|
|
392
|
+
for (const src of await findSourceFiles()) {
|
|
393
|
+
const code = await readFileSafe(src).catch(() => "");
|
|
394
|
+
if (!code)
|
|
395
|
+
continue;
|
|
396
|
+
scanWebviewJsi(code, src, accs.jsi);
|
|
397
|
+
scanWebviewJs(code, src, accs.jsEnabled);
|
|
398
|
+
scanSharedPrefs(code, src, accs.sharedPrefs, sensitiveNeedles);
|
|
399
|
+
scanLogcat(code, src, accs.logcat, sensitiveNeedles);
|
|
400
|
+
scanHardcodedSecret(code, src, accs.hardcoded);
|
|
401
|
+
scanRawQuery(code, src, accs.rawQuery);
|
|
402
|
+
scanImplicitIntent(code, src, accs.implicitIntent);
|
|
403
|
+
scanPendingIntentMutable(code, src, accs.pendingIntent);
|
|
404
|
+
scanExternalStorage(code, src, accs.externalStorage);
|
|
405
|
+
scanBiometricWeak(code, src, accs.biometric);
|
|
406
|
+
if (code.includes("BillingClient"))
|
|
407
|
+
accs.billingFiles.push(src);
|
|
408
|
+
}
|
|
409
|
+
const findings = emitSourceFindings(accs);
|
|
410
|
+
const billingFinding = await checkBillingClientOnly(accs.billingFiles);
|
|
411
|
+
if (billingFinding !== null)
|
|
412
|
+
findings.push(billingFinding);
|
|
413
|
+
return findings;
|
|
414
|
+
}
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// Sub-checker: Android string resource checks
|
|
417
|
+
// MASVS-STORAGE-14
|
|
418
|
+
// ---------------------------------------------------------------------------
|
|
419
|
+
async function checkStringResources(existingFindings) {
|
|
420
|
+
const findings = [];
|
|
421
|
+
const resFiles = [];
|
|
422
|
+
const resEvidence = [];
|
|
423
|
+
const secretResRe = /name\s*=\s*["'](apiKey|api_key|secret|password|token|auth_token|client_secret)['"]/i;
|
|
424
|
+
for (const res of await findStringResources()) {
|
|
425
|
+
const xml = await readFileSafe(res).catch(() => "");
|
|
426
|
+
if (!xml || !secretResRe.test(xml))
|
|
427
|
+
continue;
|
|
428
|
+
resFiles.push(res);
|
|
429
|
+
resEvidence.push(...grepLinesRe(xml, secretResRe, 5).map(l => `${res}: ${l}`));
|
|
430
|
+
}
|
|
431
|
+
if (resFiles.length === 0)
|
|
432
|
+
return findings;
|
|
433
|
+
const existing = existingFindings.find(f => f.id === "ANDROID_HARDCODED_SECRET");
|
|
434
|
+
if (existing) {
|
|
435
|
+
existing.files = [...new Set([...(existing.files ?? []), ...resFiles])];
|
|
436
|
+
existing.evidence = [...(existing.evidence ?? []), ...resEvidence.slice(0, 4)];
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
findings.push({
|
|
440
|
+
id: "ANDROID_HARDCODED_SECRET",
|
|
441
|
+
title: "Hardcoded API key / secret / password found in Android string resources",
|
|
442
|
+
severity: "CRITICAL",
|
|
443
|
+
files: resFiles,
|
|
444
|
+
evidence: resEvidence.slice(0, 8),
|
|
445
|
+
requiredActions: [
|
|
446
|
+
"Remove secret values from strings.xml and all resource files.",
|
|
447
|
+
"Inject secrets at runtime from a secure backend — never bundle them in the APK.",
|
|
448
|
+
"Rotate any credentials that may have been committed. See MASVS-STORAGE-14."
|
|
449
|
+
]
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return findings;
|
|
453
|
+
}
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
// Sub-checker: FileProvider path checks + tapjacking
|
|
456
|
+
// MASVS-PLATFORM-1, MASVS-PLATFORM-4
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
async function checkProviderPathsAndTapjacking(hasManifests) {
|
|
459
|
+
const findings = [];
|
|
460
|
+
for (const pp of await findProviderPathFiles()) {
|
|
461
|
+
const xml = await readFileSafe(pp).catch(() => "");
|
|
462
|
+
if (!xml)
|
|
463
|
+
continue;
|
|
464
|
+
const hasRootPath = /<root-path/i.test(xml);
|
|
465
|
+
const hasOverbroad = /<external-path[^>]*path\s*=\s*["']\.?["']/i.test(xml) ||
|
|
466
|
+
/<files-path[^>]*path\s*=\s*["']\.?["']/i.test(xml);
|
|
467
|
+
if (!hasRootPath && !hasOverbroad)
|
|
468
|
+
continue;
|
|
469
|
+
const evidence = [];
|
|
470
|
+
if (hasRootPath)
|
|
471
|
+
evidence.push(...grepLines(xml, "<root-path", 3));
|
|
472
|
+
if (hasOverbroad)
|
|
473
|
+
evidence.push(...grepLinesRe(xml, /<external-path|<files-path/, 3));
|
|
474
|
+
findings.push({
|
|
475
|
+
id: "ANDROID_CONTENT_PROVIDER_PATHS",
|
|
476
|
+
title: 'FileProvider path config uses <root-path> or overly broad path — filesystem over-exposure',
|
|
477
|
+
severity: "HIGH",
|
|
478
|
+
files: [pp],
|
|
479
|
+
evidence,
|
|
480
|
+
requiredActions: [
|
|
481
|
+
"Replace <root-path> with the narrowest applicable path element (<files-path>, <cache-path>, etc.).",
|
|
482
|
+
'Set path to a specific subdirectory, not "." or empty.',
|
|
483
|
+
"Review every <external-path> declaration and limit scope to the minimum required directory.",
|
|
484
|
+
"See FileProvider documentation and MASVS-PLATFORM-1."
|
|
485
|
+
]
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (hasManifests) {
|
|
489
|
+
const tapjacking = await searchRepo({ query: "filterTouchesWhenObscured", isRegex: false, maxMatches: 5 });
|
|
490
|
+
if (tapjacking.length === 0) {
|
|
491
|
+
findings.push({
|
|
492
|
+
id: "ANDROID_TAPJACKING",
|
|
493
|
+
title: "No filterTouchesWhenObscured protection found — sensitive views may be vulnerable to tapjacking",
|
|
494
|
+
severity: "MEDIUM",
|
|
495
|
+
requiredActions: [
|
|
496
|
+
'Set filterTouchesWhenObscured="true" on sensitive Views (password fields, payment screens, permission dialogs).',
|
|
497
|
+
"Call setFilterTouchesWhenObscured(true) programmatically for dynamically inflated views.",
|
|
498
|
+
"See Android View security documentation and MASVS-PLATFORM-4."
|
|
499
|
+
]
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return findings;
|
|
504
|
+
}
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
// Sub-checker: Gradle SDK version checks
|
|
507
|
+
// MASVS-RESILIENCE-1
|
|
508
|
+
// ---------------------------------------------------------------------------
|
|
509
|
+
async function checkGradleSdkVersions() {
|
|
510
|
+
const findings = [];
|
|
511
|
+
for (const gradle of await findGradleFiles()) {
|
|
512
|
+
const text = await readFileSafe(gradle).catch(() => "");
|
|
513
|
+
if (!text)
|
|
514
|
+
continue;
|
|
515
|
+
const minSdkMatch = /minSdkVersion\s*[=:]\s*(\d+)/i.exec(text);
|
|
516
|
+
if (minSdkMatch) {
|
|
517
|
+
const minSdk = Number.parseInt(minSdkMatch[1], 10);
|
|
518
|
+
if (minSdk < 21) {
|
|
519
|
+
findings.push({
|
|
520
|
+
id: "ANDROID_MIN_SDK_LOW",
|
|
521
|
+
title: `minSdkVersion ${minSdk} is critically low — missing FBE, modern TLS, and Keystore features`,
|
|
522
|
+
severity: "HIGH",
|
|
523
|
+
files: [gradle],
|
|
524
|
+
evidence: [`minSdkVersion = ${minSdk}`],
|
|
525
|
+
requiredActions: [
|
|
526
|
+
"Raise minSdkVersion to at least 24 (Android 7.0) to gain per-file encryption and TLS 1.3.",
|
|
527
|
+
"Target 28+ for all cleartext-traffic restrictions to apply by default.",
|
|
528
|
+
"Check Google Play distribution data — very few active devices run below API 21."
|
|
529
|
+
]
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
else if (minSdk < 24) {
|
|
533
|
+
findings.push({
|
|
534
|
+
id: "ANDROID_MIN_SDK_LOW",
|
|
535
|
+
title: `minSdkVersion ${minSdk} is below 24 — app runs on devices missing key security features`,
|
|
536
|
+
severity: "MEDIUM",
|
|
537
|
+
files: [gradle],
|
|
538
|
+
evidence: [`minSdkVersion = ${minSdk}`],
|
|
539
|
+
requiredActions: [
|
|
540
|
+
"Consider raising minSdkVersion to 24 (Android 7.0) for TLS 1.3 and improved Keystore guarantees.",
|
|
541
|
+
"At minimum ensure your network_security_config.xml enforces TLS regardless of platform defaults.",
|
|
542
|
+
"See MASVS-RESILIENCE-1 for minimum SDK guidance."
|
|
543
|
+
]
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const targetSdkMatch = /targetSdkVersion\s*[=:]\s*(\d+)/i.exec(text);
|
|
548
|
+
if (targetSdkMatch) {
|
|
549
|
+
const targetSdk = Number.parseInt(targetSdkMatch[1], 10);
|
|
550
|
+
if (targetSdk < 33) {
|
|
551
|
+
findings.push({
|
|
552
|
+
id: "ANDROID_TARGET_SDK_OLD",
|
|
553
|
+
title: `targetSdkVersion ${targetSdk} is below 33 — modern permission model and scoped storage not enforced`,
|
|
554
|
+
severity: "HIGH",
|
|
555
|
+
files: [gradle],
|
|
556
|
+
evidence: [`targetSdkVersion = ${targetSdk}`],
|
|
557
|
+
requiredActions: [
|
|
558
|
+
"Raise targetSdkVersion to 34 (Android 14) or the current Google Play requirement.",
|
|
559
|
+
"Review and adapt code for scoped storage, exact alarm permissions, and foreground service types.",
|
|
560
|
+
"Google Play requires targetSdkVersion >= 33 for new app submissions as of 2023.",
|
|
561
|
+
"See MASVS-RESILIENCE-1 and Android target API level requirements."
|
|
562
|
+
]
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return findings;
|
|
568
|
+
}
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
// Sub-checker: Root detection
|
|
571
|
+
// MASVS-RESILIENCE-1
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
async function checkRootDetection() {
|
|
574
|
+
const findings = [];
|
|
575
|
+
const rootDetectionRe = /RootBeer|isRooted|checkForRoot|BuildConfig.*isRooted|PlayIntegrity|SafetyNet|checkSuBinary/i;
|
|
576
|
+
const sensitiveOpsRe = /Keystore|EncryptedSharedPreferences|BiometricPrompt/i;
|
|
577
|
+
const srcFiles = await findSourceFiles();
|
|
578
|
+
let hasRootDetection = false;
|
|
579
|
+
let hasSensitiveOps = false;
|
|
580
|
+
for (const src of srcFiles) {
|
|
581
|
+
const code = await readFileSafe(src).catch(() => "");
|
|
582
|
+
if (!code)
|
|
583
|
+
continue;
|
|
584
|
+
if (rootDetectionRe.test(code))
|
|
585
|
+
hasRootDetection = true;
|
|
586
|
+
if (sensitiveOpsRe.test(code))
|
|
587
|
+
hasSensitiveOps = true;
|
|
588
|
+
}
|
|
589
|
+
if (!hasRootDetection && hasSensitiveOps) {
|
|
590
|
+
findings.push({
|
|
591
|
+
id: "ANDROID_NO_ROOT_DETECTION",
|
|
592
|
+
title: "Android app performs sensitive operations without root detection — Keystore/EncryptedSharedPreferences accessible on rooted devices (MASVS-RESILIENCE-1)",
|
|
593
|
+
severity: "MEDIUM",
|
|
594
|
+
requiredActions: [
|
|
595
|
+
"Integrate RootBeer or Play Integrity API to detect rooted devices before performing sensitive operations.",
|
|
596
|
+
"Block or warn users when root is detected, especially before accessing Keystore-backed keys or EncryptedSharedPreferences.",
|
|
597
|
+
"See MASVS-RESILIENCE-1 for guidance on runtime integrity checks."
|
|
598
|
+
]
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return findings;
|
|
602
|
+
}
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// Sub-checker: Frida/Magisk/Xposed detection
|
|
605
|
+
// MASVS-RESILIENCE-4
|
|
606
|
+
// ---------------------------------------------------------------------------
|
|
607
|
+
async function checkFridaMagiskDetection() {
|
|
608
|
+
const findings = [];
|
|
609
|
+
const fridaRe = /frida|gadget|magisk|xposed|EdXposed|LSPosed|anti.*frida|fridaDetek/i;
|
|
610
|
+
const highRiskRe = /Keystore|CertificatePinner|BiometricPrompt|EncryptedSharedPreferences/i;
|
|
611
|
+
const srcFiles = await findSourceFiles();
|
|
612
|
+
let hasFridaDetection = false;
|
|
613
|
+
let hasHighRiskOps = false;
|
|
614
|
+
for (const src of srcFiles) {
|
|
615
|
+
const code = await readFileSafe(src).catch(() => "");
|
|
616
|
+
if (!code)
|
|
617
|
+
continue;
|
|
618
|
+
if (fridaRe.test(code))
|
|
619
|
+
hasFridaDetection = true;
|
|
620
|
+
if (highRiskRe.test(code))
|
|
621
|
+
hasHighRiskOps = true;
|
|
622
|
+
}
|
|
623
|
+
if (!hasFridaDetection && hasHighRiskOps) {
|
|
624
|
+
findings.push({
|
|
625
|
+
id: "ANDROID_NO_FRIDA_DETECTION",
|
|
626
|
+
title: "No Frida/Magisk/Xposed detection — runtime instrumentation attacks bypass certificate pinning and exfiltrate secrets silently (MASVS-RESILIENCE-4)",
|
|
627
|
+
severity: "MEDIUM",
|
|
628
|
+
requiredActions: [
|
|
629
|
+
"Implement Frida/Gadget port and library detection at runtime before sensitive operations.",
|
|
630
|
+
"Check for Magisk/Xposed module presence using integrity APIs or native checks.",
|
|
631
|
+
"Consider integrating a Runtime Application Self-Protection (RASP) library.",
|
|
632
|
+
"See MASVS-RESILIENCE-4 for anti-tampering and anti-instrumentation controls."
|
|
633
|
+
]
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
return findings;
|
|
637
|
+
}
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
// Sub-checker: WebView SSL error proceed()
|
|
640
|
+
// MASVS-NETWORK-3
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
async function checkWebViewSslErrorProceed() {
|
|
643
|
+
const findings = [];
|
|
644
|
+
const sslProceedRe = /onReceivedSslError[\s\S]{0,300}handler\.proceed\(\)/;
|
|
645
|
+
const files = [];
|
|
646
|
+
const evidence = [];
|
|
647
|
+
const allFiles = await fg(["**/*.kt", "**/*.java", "**/*.xml"], {
|
|
648
|
+
dot: true,
|
|
649
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/build/**", "**/dist/**"]
|
|
650
|
+
});
|
|
651
|
+
for (const src of allFiles) {
|
|
652
|
+
const code = await readFileSafe(src).catch(() => "");
|
|
653
|
+
if (!code)
|
|
654
|
+
continue;
|
|
655
|
+
if (sslProceedRe.test(code)) {
|
|
656
|
+
files.push(src);
|
|
657
|
+
evidence.push(...grepLines(code, "handler.proceed()", 3).map(l => `${src}: ${l}`));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (files.length > 0) {
|
|
661
|
+
findings.push({
|
|
662
|
+
id: "ANDROID_WEBVIEW_SSL_PROCEED",
|
|
663
|
+
title: "WebViewClient.onReceivedSslError calls proceed() — all TLS errors silently accepted, full MITM possible (MASVS-NETWORK-3)",
|
|
664
|
+
severity: "CRITICAL",
|
|
665
|
+
files: [...new Set(files)],
|
|
666
|
+
evidence: evidence.slice(0, 8),
|
|
667
|
+
requiredActions: [
|
|
668
|
+
"Remove handler.proceed() from onReceivedSslError entirely — always call handler.cancel() on SSL errors.",
|
|
669
|
+
"If a specific domain requires an exception, implement strict hostname and certificate validation instead.",
|
|
670
|
+
"See MASVS-NETWORK-3, CWE-295, and OWASP M3."
|
|
671
|
+
]
|
|
672
|
+
});
|
|
33
673
|
}
|
|
34
674
|
return findings;
|
|
35
675
|
}
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
// Sub-checker: Firebase public rules
|
|
678
|
+
// MASVS-STORAGE-4
|
|
679
|
+
// ---------------------------------------------------------------------------
|
|
680
|
+
async function checkFirebasePublicRules() {
|
|
681
|
+
const findings = [];
|
|
682
|
+
const publicRulesRe = /\.read.*true|\.write.*true|allow read.*if true|allow write.*if true/;
|
|
683
|
+
const ruleFiles = await fg(["**/database.rules.json", "**/firestore.rules", "**/*.rules"], {
|
|
684
|
+
dot: true,
|
|
685
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/build/**", "**/dist/**"]
|
|
686
|
+
});
|
|
687
|
+
const files = [];
|
|
688
|
+
const evidence = [];
|
|
689
|
+
for (const f of ruleFiles) {
|
|
690
|
+
const content = await readFileSafe(f).catch(() => "");
|
|
691
|
+
if (!content)
|
|
692
|
+
continue;
|
|
693
|
+
if (publicRulesRe.test(content)) {
|
|
694
|
+
files.push(f);
|
|
695
|
+
evidence.push(...grepLinesRe(content, publicRulesRe, 3).map(l => `${f}: ${l}`));
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (files.length > 0) {
|
|
699
|
+
findings.push({
|
|
700
|
+
id: "ANDROID_FIREBASE_PUBLIC_RULES",
|
|
701
|
+
title: "Firebase rules allow unauthenticated read/write — entire database accessible by anyone with the project URL (MASVS-STORAGE-4)",
|
|
702
|
+
severity: "CRITICAL",
|
|
703
|
+
files: [...new Set(files)],
|
|
704
|
+
evidence: evidence.slice(0, 8),
|
|
705
|
+
requiredActions: [
|
|
706
|
+
"Replace permissive Firebase rules with authentication checks (auth != null) at minimum.",
|
|
707
|
+
"Use field-level rules and validate user ownership before allowing reads/writes.",
|
|
708
|
+
"Audit the Firebase console rules editor and enable App Check to restrict to your app only.",
|
|
709
|
+
"See MASVS-STORAGE-4 and Firebase Security Rules documentation."
|
|
710
|
+
]
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
return findings;
|
|
714
|
+
}
|
|
715
|
+
// ---------------------------------------------------------------------------
|
|
716
|
+
// Sub-checker: Google Maps API key hardcoded
|
|
717
|
+
// MASVS-STORAGE-2
|
|
718
|
+
// ---------------------------------------------------------------------------
|
|
719
|
+
async function checkGoogleMapsApiKey() {
|
|
720
|
+
const findings = [];
|
|
721
|
+
const mapsKeyRe = /AIza[0-9A-Za-z_-]{35}|com\.google\.android\.geo\.API_KEY/;
|
|
722
|
+
const targetFiles = await fg(["**/AndroidManifest.xml", "**/res/values/strings.xml"], {
|
|
723
|
+
dot: true,
|
|
724
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/build/**", "**/dist/**"]
|
|
725
|
+
});
|
|
726
|
+
const files = [];
|
|
727
|
+
const evidence = [];
|
|
728
|
+
for (const f of targetFiles) {
|
|
729
|
+
const content = await readFileSafe(f).catch(() => "");
|
|
730
|
+
if (!content)
|
|
731
|
+
continue;
|
|
732
|
+
if (mapsKeyRe.test(content)) {
|
|
733
|
+
files.push(f);
|
|
734
|
+
evidence.push(...grepLinesRe(content, mapsKeyRe, 3).map(l => `${f}: ${l}`));
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (files.length > 0) {
|
|
738
|
+
findings.push({
|
|
739
|
+
id: "ANDROID_MAPS_API_KEY_HARDCODED",
|
|
740
|
+
title: "Google Maps API key hardcoded in manifest/resources — extractable from APK for billing fraud or geolocation abuse (MASVS-STORAGE-2)",
|
|
741
|
+
severity: "HIGH",
|
|
742
|
+
files: [...new Set(files)],
|
|
743
|
+
evidence: evidence.slice(0, 8),
|
|
744
|
+
requiredActions: [
|
|
745
|
+
"Move the Maps API key to a secrets manager and inject at build time via a non-committed local.properties file.",
|
|
746
|
+
"Restrict the key in Google Cloud Console to the specific Android app package name and SHA-1 fingerprint.",
|
|
747
|
+
"Rotate any exposed keys immediately. See MASVS-STORAGE-2 and OWASP M9."
|
|
748
|
+
]
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
return findings;
|
|
752
|
+
}
|
|
753
|
+
// ---------------------------------------------------------------------------
|
|
754
|
+
// Sub-checker: Deep link path traversal
|
|
755
|
+
// MASVS-PLATFORM-3
|
|
756
|
+
// ---------------------------------------------------------------------------
|
|
757
|
+
async function checkDeepLinkTraversal() {
|
|
758
|
+
const findings = [];
|
|
759
|
+
const pathAccessRe = /intent\.data\.getPath|uri\.getPath|data\.getLastPathSegment|intent\.getData\(\)\.getPath/;
|
|
760
|
+
const sanitizeRe = /sanitize|normalize|replace.*\.\.|startsWith|validate/;
|
|
761
|
+
const srcFiles = await findSourceFiles();
|
|
762
|
+
const files = [];
|
|
763
|
+
const evidence = [];
|
|
764
|
+
for (const src of srcFiles) {
|
|
765
|
+
const code = await readFileSafe(src).catch(() => "");
|
|
766
|
+
if (!code)
|
|
767
|
+
continue;
|
|
768
|
+
if (!pathAccessRe.test(code))
|
|
769
|
+
continue;
|
|
770
|
+
const lines = grepLinesRe(code, pathAccessRe, 10);
|
|
771
|
+
const unsanitized = lines.filter(l => !sanitizeRe.test(l));
|
|
772
|
+
if (unsanitized.length > 0) {
|
|
773
|
+
files.push(src);
|
|
774
|
+
evidence.push(...unsanitized.map(l => `${src}: ${l}`));
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (files.length > 0) {
|
|
778
|
+
findings.push({
|
|
779
|
+
id: "ANDROID_DEEPLINK_PATH_TRAVERSAL",
|
|
780
|
+
title: "Deep link path parameters not sanitized before use — path traversal via ../.. in intent data URI (MASVS-PLATFORM-3)",
|
|
781
|
+
severity: "HIGH",
|
|
782
|
+
files: [...new Set(files)],
|
|
783
|
+
evidence: evidence.slice(0, 8),
|
|
784
|
+
requiredActions: [
|
|
785
|
+
"Validate and normalize URI paths obtained from intent data before using as file paths or query parameters.",
|
|
786
|
+
"Reject paths containing '..' sequences or absolute paths outside the expected prefix.",
|
|
787
|
+
"Use Uri.Builder and enforce scheme/authority/path whitelist checks. See MASVS-PLATFORM-3 and CWE-22."
|
|
788
|
+
]
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
return findings;
|
|
792
|
+
}
|
|
793
|
+
// ---------------------------------------------------------------------------
|
|
794
|
+
// Sub-checker: SharedPreferences world-readable/writable mode
|
|
795
|
+
// MASVS-STORAGE-1
|
|
796
|
+
// ---------------------------------------------------------------------------
|
|
797
|
+
async function checkSharedPrefsWorldMode() {
|
|
798
|
+
const findings = [];
|
|
799
|
+
const worldModeRe = /MODE_WORLD_READABLE|MODE_WORLD_WRITEABLE|Context\.MODE_WORLD/;
|
|
800
|
+
const srcFiles = await findSourceFiles();
|
|
801
|
+
const files = [];
|
|
802
|
+
const evidence = [];
|
|
803
|
+
for (const src of srcFiles) {
|
|
804
|
+
const code = await readFileSafe(src).catch(() => "");
|
|
805
|
+
if (!code)
|
|
806
|
+
continue;
|
|
807
|
+
if (worldModeRe.test(code)) {
|
|
808
|
+
files.push(src);
|
|
809
|
+
evidence.push(...grepLinesRe(code, worldModeRe, 3).map(l => `${src}: ${l}`));
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (files.length > 0) {
|
|
813
|
+
findings.push({
|
|
814
|
+
id: "ANDROID_SHAREDPREFS_WORLD_MODE",
|
|
815
|
+
title: "SharedPreferences opened with MODE_WORLD_READABLE/WRITEABLE — readable/writable by any app on device (MASVS-STORAGE-1)",
|
|
816
|
+
severity: "CRITICAL",
|
|
817
|
+
files: [...new Set(files)],
|
|
818
|
+
evidence: evidence.slice(0, 8),
|
|
819
|
+
requiredActions: [
|
|
820
|
+
"Replace MODE_WORLD_READABLE / MODE_WORLD_WRITEABLE with MODE_PRIVATE (the default).",
|
|
821
|
+
"These modes have been deprecated since API 17 and throw a SecurityException on API 24+.",
|
|
822
|
+
"If cross-app data sharing is required, use a ContentProvider with explicit permissions instead.",
|
|
823
|
+
"See MASVS-STORAGE-1 and OWASP M2."
|
|
824
|
+
]
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
return findings;
|
|
828
|
+
}
|
|
829
|
+
// ---------------------------------------------------------------------------
|
|
830
|
+
// Sub-checker: ContentProvider exported without permissions
|
|
831
|
+
// MASVS-PLATFORM-1
|
|
832
|
+
// ---------------------------------------------------------------------------
|
|
833
|
+
async function checkContentProviderPermissions() {
|
|
834
|
+
const findings = [];
|
|
835
|
+
const providerExportedRe = /<provider[^>]*android:exported\s*=\s*"true"(?![^>]*android:readPermission)(?![^>]*android:writePermission)/;
|
|
836
|
+
const manifests = await findManifests();
|
|
837
|
+
const files = [];
|
|
838
|
+
const evidence = [];
|
|
839
|
+
for (const m of manifests) {
|
|
840
|
+
const xml = await readFileSafe(m).catch(() => "");
|
|
841
|
+
if (!xml)
|
|
842
|
+
continue;
|
|
843
|
+
if (providerExportedRe.test(xml)) {
|
|
844
|
+
files.push(m);
|
|
845
|
+
const lines = grepLinesRe(xml, /<provider[^>]*android:exported\s*=\s*"true"/i, 5);
|
|
846
|
+
evidence.push(...lines.map(l => `${m}: ${l}`));
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (files.length > 0) {
|
|
850
|
+
findings.push({
|
|
851
|
+
id: "ANDROID_CONTENT_PROVIDER_NO_PERMISSIONS",
|
|
852
|
+
title: "ContentProvider exported=true without readPermission/writePermission — any app can query or modify provider data (MASVS-PLATFORM-1)",
|
|
853
|
+
severity: "HIGH",
|
|
854
|
+
files: [...new Set(files)],
|
|
855
|
+
evidence: evidence.slice(0, 8),
|
|
856
|
+
requiredActions: [
|
|
857
|
+
"Add android:readPermission and android:writePermission to every exported ContentProvider.",
|
|
858
|
+
'Use a signature-level permission (android:protectionLevel="signature") for providers only accessed internally.',
|
|
859
|
+
"If the provider must be public, validate all input and restrict exposed columns/operations.",
|
|
860
|
+
"See MASVS-PLATFORM-1 and OWASP M1."
|
|
861
|
+
]
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
return findings;
|
|
865
|
+
}
|
|
866
|
+
// ---------------------------------------------------------------------------
|
|
867
|
+
// Sub-checker: Flutter insecure storage via shared_preferences
|
|
868
|
+
// MASVS-STORAGE-1
|
|
869
|
+
// ---------------------------------------------------------------------------
|
|
870
|
+
async function checkFlutterSharedPrefs() {
|
|
871
|
+
const findings = [];
|
|
872
|
+
const dartFiles = await fg(["**/*.dart"], {
|
|
873
|
+
dot: true,
|
|
874
|
+
ignore: ["**/node_modules/**", "**/.git/**", "**/build/**", "**/dist/**", "**/.dart_tool/**"]
|
|
875
|
+
});
|
|
876
|
+
if (dartFiles.length === 0)
|
|
877
|
+
return findings;
|
|
878
|
+
const sharedPrefsRe = /shared_preferences|SharedPreferences\.getInstance\(\)|prefs\.setString\s*\([^,]*(?:token|password|secret|key|auth)/i;
|
|
879
|
+
const secureStorageRe = /flutter_secure_storage|FlutterSecureStorage/;
|
|
880
|
+
const files = [];
|
|
881
|
+
const evidence = [];
|
|
882
|
+
for (const src of dartFiles) {
|
|
883
|
+
const code = await readFileSafe(src).catch(() => "");
|
|
884
|
+
if (!code)
|
|
885
|
+
continue;
|
|
886
|
+
// Per-file check: only suppress if THIS file already uses flutter_secure_storage
|
|
887
|
+
if (sharedPrefsRe.test(code) && !secureStorageRe.test(code)) {
|
|
888
|
+
files.push(src);
|
|
889
|
+
evidence.push(...grepLinesRe(code, sharedPrefsRe, 3).map(l => `${src}: ${l}`));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (files.length > 0) {
|
|
893
|
+
findings.push({
|
|
894
|
+
id: "FLUTTER_INSECURE_STORAGE",
|
|
895
|
+
title: "Flutter app stores sensitive data in shared_preferences — use flutter_secure_storage backed by iOS Keychain/Android Keystore instead (MASVS-STORAGE-1)",
|
|
896
|
+
severity: "HIGH",
|
|
897
|
+
files: [...new Set(files)],
|
|
898
|
+
evidence: evidence.slice(0, 8),
|
|
899
|
+
requiredActions: [
|
|
900
|
+
"Replace shared_preferences with flutter_secure_storage for any token, password, secret, or key values.",
|
|
901
|
+
"flutter_secure_storage uses iOS Keychain and Android Keystore, providing hardware-backed encryption.",
|
|
902
|
+
"Audit all prefs.setString / prefs.set* calls and migrate sensitive keys to FlutterSecureStorage.",
|
|
903
|
+
"See MASVS-STORAGE-1 and the flutter_secure_storage package documentation."
|
|
904
|
+
]
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
return findings;
|
|
908
|
+
}
|
|
909
|
+
// ---------------------------------------------------------------------------
|
|
910
|
+
// Orchestrator — runs all sub-checkers and merges results
|
|
911
|
+
// ---------------------------------------------------------------------------
|
|
912
|
+
export async function checkMobileAndroid(_) {
|
|
913
|
+
const [manifestFindings, nscFindings, sourceFindings, providerFindings, gradleFindings, rootDetectionFindings, fridaMagiskFindings, webViewSslFindings, firebaseRulesFindings, mapsApiKeyFindings, deepLinkTraversalFindings, sharedPrefsWorldFindings, contentProviderPermFindings, flutterSharedPrefsFindings] = await Promise.all([
|
|
914
|
+
checkManifests(),
|
|
915
|
+
checkNetworkSecurityConfig(),
|
|
916
|
+
checkSourceFiles(),
|
|
917
|
+
checkProviderPathsAndTapjacking(true),
|
|
918
|
+
checkGradleSdkVersions(),
|
|
919
|
+
checkRootDetection(),
|
|
920
|
+
checkFridaMagiskDetection(),
|
|
921
|
+
checkWebViewSslErrorProceed(),
|
|
922
|
+
checkFirebasePublicRules(),
|
|
923
|
+
checkGoogleMapsApiKey(),
|
|
924
|
+
checkDeepLinkTraversal(),
|
|
925
|
+
checkSharedPrefsWorldMode(),
|
|
926
|
+
checkContentProviderPermissions(),
|
|
927
|
+
checkFlutterSharedPrefs()
|
|
928
|
+
]);
|
|
929
|
+
const findings = [
|
|
930
|
+
...manifestFindings,
|
|
931
|
+
...nscFindings,
|
|
932
|
+
...sourceFindings,
|
|
933
|
+
...providerFindings,
|
|
934
|
+
...gradleFindings,
|
|
935
|
+
...rootDetectionFindings,
|
|
936
|
+
...fridaMagiskFindings,
|
|
937
|
+
...webViewSslFindings,
|
|
938
|
+
...firebaseRulesFindings,
|
|
939
|
+
...mapsApiKeyFindings,
|
|
940
|
+
...deepLinkTraversalFindings,
|
|
941
|
+
...sharedPrefsWorldFindings,
|
|
942
|
+
...contentProviderPermFindings,
|
|
943
|
+
...flutterSharedPrefsFindings
|
|
944
|
+
];
|
|
945
|
+
// String resource check may augment the ANDROID_HARDCODED_SECRET finding already in the list
|
|
946
|
+
const resFindings = await checkStringResources(findings);
|
|
947
|
+
findings.push(...resFindings);
|
|
948
|
+
return findings;
|
|
949
|
+
}
|