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,13 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* iOS MASVS L1/L2 gate check — runs automatically on every PR.
|
|
3
|
+
*
|
|
4
|
+
* Covers the following OWASP MASVS control categories:
|
|
5
|
+
* MASVS-STORAGE : keychain access levels, NSUserDefaults, Core Data encryption, backup exclusion, bundle secrets
|
|
6
|
+
* MASVS-CRYPTO : weak algorithms (MD5/SHA1/DES/ECB), hardcoded secrets/IVs
|
|
7
|
+
* MASVS-AUTH : biometric / LAContext enrollment change detection
|
|
8
|
+
* MASVS-NETWORK : ATS / NSAllowsArbitraryLoads, certificate pinning, SSRF via URL schemes
|
|
9
|
+
* MASVS-PLATFORM : pasteboard leakage, WKWebView JS bridge, custom URL scheme validation
|
|
10
|
+
* MASVS-CODE : ARC disabled, bitcode in release, debug flags in production, network loggers
|
|
11
|
+
* MASVS-RESILIENCE: jailbreak detection, screenshot protection
|
|
12
|
+
*
|
|
13
|
+
* Each check is a standalone function returning Finding | null so results stay
|
|
14
|
+
* actionable and deduplicated. File reads are grouped up-front via loadContext()
|
|
15
|
+
* to avoid redundant I/O across checks.
|
|
16
|
+
*/
|
|
17
|
+
import { sanitizeErrorMessage } from "../result.js";
|
|
1
18
|
import fg from "fast-glob";
|
|
2
19
|
import { readFileSafe } from "../../repo/fs.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
20
|
+
import { searchRepo } from "../../repo/search.js";
|
|
21
|
+
// ── I/O helpers ────────────────────────────────────────────────────────────────
|
|
22
|
+
async function readFileMap(patterns, ignore) {
|
|
23
|
+
const paths = await fg(patterns, { dot: true, ignore });
|
|
24
|
+
const result = new Map();
|
|
25
|
+
await Promise.all(paths.map(async (p) => {
|
|
7
26
|
const text = await readFileSafe(p).catch(() => "");
|
|
27
|
+
result.set(p, text);
|
|
28
|
+
}));
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
async function loadContext() {
|
|
32
|
+
const baseIgnore = ["**/node_modules/**", "**/.git/**"];
|
|
33
|
+
const iosIgnore = [...baseIgnore, "**/Pods/**"];
|
|
34
|
+
const allPlists = await readFileMap(["**/*.plist"], baseIgnore);
|
|
35
|
+
const infoPlistEntries = [];
|
|
36
|
+
const resourcePlistEntries = [];
|
|
37
|
+
for (const [p, text] of allPlists) {
|
|
38
|
+
if (p.toLowerCase().endsWith("info.plist")) {
|
|
39
|
+
infoPlistEntries.push([p, text]);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
resourcePlistEntries.push([p, text]);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const swiftSources = await readFileMap(["**/*.swift"], iosIgnore);
|
|
46
|
+
const objcSources = await readFileMap(["**/*.m", "**/*.mm"], iosIgnore);
|
|
47
|
+
const xcconfigs = await readFileMap(["**/*.xcconfig"], iosIgnore);
|
|
48
|
+
const pbxprojs = await readFileMap(["**/*.pbxproj"], iosIgnore);
|
|
49
|
+
const allNativeSources = new Map([...swiftSources, ...objcSources]);
|
|
50
|
+
return { infoPlistEntries, resourcePlistEntries, allNativeSources, objcSources, xcconfigs, pbxprojs };
|
|
51
|
+
}
|
|
52
|
+
// ── scan utilities ─────────────────────────────────────────────────────────────
|
|
53
|
+
function filesWithMatch(sourceMap, re) {
|
|
54
|
+
const matched = [];
|
|
55
|
+
for (const [path, content] of sourceMap) {
|
|
56
|
+
if (re.test(content))
|
|
57
|
+
matched.push(path);
|
|
58
|
+
}
|
|
59
|
+
return matched;
|
|
60
|
+
}
|
|
61
|
+
function evidenceLines(sourceMap, re, maxLines = 10) {
|
|
62
|
+
const lines = [];
|
|
63
|
+
for (const [path, content] of sourceMap) {
|
|
64
|
+
if (lines.length >= maxLines)
|
|
65
|
+
break;
|
|
66
|
+
const fileLines = content.split("\n");
|
|
67
|
+
for (let i = 0; i < fileLines.length && lines.length < maxLines; i++) {
|
|
68
|
+
if (re.test(fileLines[i])) {
|
|
69
|
+
lines.push(`${path}:${i + 1}:${fileLines[i].slice(0, 200)}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return lines;
|
|
74
|
+
}
|
|
75
|
+
// ── individual checks ─────────────────────────────────────────────────────────
|
|
76
|
+
/** CHECK 1: ATS weakened — NSAllowsArbitraryLoads present in Info.plist. MASVS-NETWORK-1 */
|
|
77
|
+
function checkAtsWeak(ctx) {
|
|
78
|
+
const found = [];
|
|
79
|
+
for (const [p, text] of ctx.infoPlistEntries) {
|
|
8
80
|
const lower = text.toLowerCase();
|
|
9
81
|
if (lower.includes("nsallowsarbitraryloads") || lower.includes("allowsarbitraryloads")) {
|
|
10
|
-
|
|
82
|
+
found.push({
|
|
11
83
|
id: "IOS_ATS_WEAK",
|
|
12
84
|
title: "iOS ATS appears weakened (NSAllowsArbitraryLoads)",
|
|
13
85
|
severity: "CRITICAL",
|
|
@@ -19,5 +91,725 @@ export async function checkMobileIos(_) {
|
|
|
19
91
|
});
|
|
20
92
|
}
|
|
21
93
|
}
|
|
94
|
+
return found;
|
|
95
|
+
}
|
|
96
|
+
/** CHECK 2: Sensitive file paths written without iCloud backup exclusion. MASVS-STORAGE-1 / CWE-312 */
|
|
97
|
+
function checkBackupAllowed(ctx) {
|
|
98
|
+
const SENSITIVE_PATH_RE = /(?:documents|library|caches|applicationSupport)[/\\](?:user|account|session|token|credential|secret|key|db|database)/i;
|
|
99
|
+
const BACKUP_EXCLUDE_RE = /NSURLIsExcludedFromBackupKey|isExcludedFromBackup\s*=\s*true/;
|
|
100
|
+
const violations = filesWithMatch(ctx.allNativeSources, SENSITIVE_PATH_RE).filter((f) => !BACKUP_EXCLUDE_RE.test(ctx.allNativeSources.get(f) ?? ""));
|
|
101
|
+
if (violations.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
return {
|
|
104
|
+
id: "IOS_BACKUP_ALLOWED",
|
|
105
|
+
title: "Sensitive file paths written without iCloud backup exclusion (NSURLIsExcludedFromBackupKey absent)",
|
|
106
|
+
severity: "HIGH",
|
|
107
|
+
files: violations.slice(0, 10),
|
|
108
|
+
requiredActions: [
|
|
109
|
+
"Set NSURLIsExcludedFromBackupKey to true on all URLs pointing to sensitive files (credentials, DB, tokens).",
|
|
110
|
+
"MASVS-STORAGE-1: sensitive local data must not be backed up to iCloud without explicit user consent.",
|
|
111
|
+
"Fix: try fileURL.setResourceValue(true, forKey: .isExcludedFromBackupKey)"
|
|
112
|
+
]
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/** CHECK 3: kSecAttrAccessibleAlways* keychain access — accessible while device is locked. MASVS-STORAGE-1 / CWE-311 */
|
|
116
|
+
function checkKeychainWeakAccess(ctx) {
|
|
117
|
+
const KEYCHAIN_WEAK_RE = /kSecAttrAccessibleAlways(?:ThisDeviceOnly)?(?!\w)/;
|
|
118
|
+
const files = filesWithMatch(ctx.allNativeSources, KEYCHAIN_WEAK_RE);
|
|
119
|
+
if (files.length === 0)
|
|
120
|
+
return null;
|
|
121
|
+
return {
|
|
122
|
+
id: "IOS_KEYCHAIN_WEAK_ACCESS",
|
|
123
|
+
title: "Keychain items use kSecAttrAccessibleAlways or kSecAttrAccessibleAlwaysThisDeviceOnly — accessible while device is locked",
|
|
124
|
+
severity: "CRITICAL",
|
|
125
|
+
files: files.slice(0, 10),
|
|
126
|
+
evidence: evidenceLines(ctx.allNativeSources, KEYCHAIN_WEAK_RE),
|
|
127
|
+
requiredActions: [
|
|
128
|
+
"Replace kSecAttrAccessibleAlways with kSecAttrAccessibleWhenUnlockedThisDeviceOnly for most secrets.",
|
|
129
|
+
"Use kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly for highest-value credentials.",
|
|
130
|
+
"MASVS-STORAGE-1 / CWE-311: data accessible while the device is locked undermines the iOS Secure Enclave model."
|
|
131
|
+
]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** CHECK 4: Credentials or tokens stored in NSUserDefaults (unencrypted). MASVS-STORAGE-1 / CWE-312 */
|
|
135
|
+
function checkUserDefaultsSensitive(ctx) {
|
|
136
|
+
const DIRECT_RE = /UserDefaults[^;\n]*(?:password|token|secret|apikey|api_key|credential|authToken|auth_token)/i;
|
|
137
|
+
const directFiles = filesWithMatch(ctx.allNativeSources, DIRECT_RE);
|
|
138
|
+
if (directFiles.length > 0) {
|
|
139
|
+
return {
|
|
140
|
+
id: "IOS_USERDEFAULTS_SENSITIVE",
|
|
141
|
+
title: "Sensitive credentials or tokens stored in NSUserDefaults (unencrypted)",
|
|
142
|
+
severity: "HIGH",
|
|
143
|
+
files: directFiles.slice(0, 10),
|
|
144
|
+
evidence: evidenceLines(ctx.allNativeSources, DIRECT_RE),
|
|
145
|
+
requiredActions: [
|
|
146
|
+
"Move all credential-class data to the iOS Keychain (Security framework kSecClassGenericPassword).",
|
|
147
|
+
"NSUserDefaults is unencrypted and backed up by default — never store tokens, passwords, or secrets here.",
|
|
148
|
+
"MASVS-STORAGE-1 / CWE-312"
|
|
149
|
+
]
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// Broader pass: any file using UserDefaults that also contains sensitive-sounding identifiers
|
|
153
|
+
const USERDEFAULTS_RE = /UserDefaults/;
|
|
154
|
+
const SENSITIVE_KEY_RE = /(?:password|token|secret|credential)/i;
|
|
155
|
+
const broadRisk = filesWithMatch(ctx.allNativeSources, USERDEFAULTS_RE).filter((f) => SENSITIVE_KEY_RE.test(ctx.allNativeSources.get(f) ?? ""));
|
|
156
|
+
if (broadRisk.length === 0)
|
|
157
|
+
return null;
|
|
158
|
+
return {
|
|
159
|
+
id: "IOS_USERDEFAULTS_SENSITIVE",
|
|
160
|
+
title: "Potential sensitive data stored in NSUserDefaults — verify no credentials or tokens are persisted here",
|
|
161
|
+
severity: "HIGH",
|
|
162
|
+
files: broadRisk.slice(0, 10),
|
|
163
|
+
requiredActions: [
|
|
164
|
+
"Do not store passwords, tokens, or secrets in NSUserDefaults — it is not encrypted and is included in iTunes/iCloud backups by default.",
|
|
165
|
+
"Use the Keychain (Security framework) for all credential-class data.",
|
|
166
|
+
"MASVS-STORAGE-1 / CWE-312"
|
|
167
|
+
]
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** CHECK 5: NSLog/print/os_log leaking sensitive data. MASVS-STORAGE-3 / CWE-532 */
|
|
171
|
+
async function checkLogSensitive(ctx) {
|
|
172
|
+
const searchHits = await searchRepo({
|
|
173
|
+
query: String.raw `(?:NSLog|os_log|print|debugPrint)\s*\([^;\n]*(?:password|token|secret|apiKey|credential)`,
|
|
174
|
+
isRegex: true,
|
|
175
|
+
maxMatches: 200
|
|
176
|
+
});
|
|
177
|
+
const iosHits = searchHits.filter((h) => /\.swift$|\.m$|\.mm$/.test(h.file));
|
|
178
|
+
if (iosHits.length > 0) {
|
|
179
|
+
return {
|
|
180
|
+
id: "IOS_LOG_SENSITIVE",
|
|
181
|
+
title: "NSLog/print/os_log call with potentially sensitive data (password, token, secret)",
|
|
182
|
+
severity: "HIGH",
|
|
183
|
+
files: [...new Set(iosHits.map((h) => h.file))].slice(0, 10),
|
|
184
|
+
evidence: iosHits.slice(0, 10).map((h) => `${h.file}:${h.line}:${h.preview}`),
|
|
185
|
+
requiredActions: [
|
|
186
|
+
"Remove all log statements that print passwords, tokens, API keys, or PII.",
|
|
187
|
+
"Use a logging wrapper that redacts sensitive fields in production builds.",
|
|
188
|
+
"MASVS-STORAGE-3 / CWE-532: log files persist on device and may be exfiltrated or read in crash reports."
|
|
189
|
+
]
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
// Fallback: direct scan of loaded source files
|
|
193
|
+
const LOG_SENSITIVE_RE = /(?:NSLog|os_log|print|debugPrint|NSLogv)\s*\([^;)]*(?:password|token|secret|apiKey|api_key|credential|ssn|cardNumber)/i;
|
|
194
|
+
const logFiles = filesWithMatch(ctx.allNativeSources, LOG_SENSITIVE_RE);
|
|
195
|
+
if (logFiles.length === 0)
|
|
196
|
+
return null;
|
|
197
|
+
return {
|
|
198
|
+
id: "IOS_LOG_SENSITIVE",
|
|
199
|
+
title: "NSLog/print/os_log call with potentially sensitive data (password, token, secret)",
|
|
200
|
+
severity: "HIGH",
|
|
201
|
+
files: logFiles.slice(0, 10),
|
|
202
|
+
evidence: evidenceLines(ctx.allNativeSources, LOG_SENSITIVE_RE),
|
|
203
|
+
requiredActions: [
|
|
204
|
+
"Remove all log statements that print passwords, tokens, API keys, or PII.",
|
|
205
|
+
"Use a logging wrapper that redacts sensitive fields in production builds.",
|
|
206
|
+
"MASVS-STORAGE-3 / CWE-532: log files persist on device and may be exfiltrated or read in crash reports."
|
|
207
|
+
]
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/** CHECK 6: Hardcoded API keys/secrets in Swift/ObjC source. MASVS-STORAGE-2 / CWE-798 */
|
|
211
|
+
function checkHardcodedSecret(ctx) {
|
|
212
|
+
const HARDCODED_RE = /(?:apiKey|APIKey|api_key|secret\s*=|password\s*=|secretKey|accessKey)\s*=\s*["'][A-Za-z0-9+=_-]{8,}["']/i;
|
|
213
|
+
const files = filesWithMatch(ctx.allNativeSources, HARDCODED_RE);
|
|
214
|
+
if (files.length === 0)
|
|
215
|
+
return null;
|
|
216
|
+
return {
|
|
217
|
+
id: "IOS_HARDCODED_SECRET",
|
|
218
|
+
title: "Hardcoded API key, secret, or password literal found in iOS source (CWE-798)",
|
|
219
|
+
severity: "CRITICAL",
|
|
220
|
+
files: files.slice(0, 10),
|
|
221
|
+
evidence: evidenceLines(ctx.allNativeSources, HARDCODED_RE),
|
|
222
|
+
requiredActions: [
|
|
223
|
+
"Remove all hardcoded secrets from source files — treat the current secret as compromised and rotate it immediately.",
|
|
224
|
+
"Store secrets server-side and fetch them at runtime with authentication, or use encrypted config injected at build time via CI secrets.",
|
|
225
|
+
"MASVS-STORAGE-2 / CWE-798: hardcoded secrets are trivially extracted from compiled binaries using strings(1) or disassembly."
|
|
226
|
+
]
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Matches a plist key whose name suggests a credential followed by a non-trivial string value.
|
|
230
|
+
// Forward-slash excluded from value class to avoid /…/ delimiter ambiguity (S5869).
|
|
231
|
+
const PLIST_SECRET_RE = /<key>[^<]*(?:Key|Secret|Password|Token|Credential)[^<]*<\/key>\s*<string>[A-Za-z0-9+=_-]{8,}<\/string>/i;
|
|
232
|
+
const PLIST_KEY_NAME_RE = /(?:Key|Secret|Password|Token|Credential)/i;
|
|
233
|
+
const PLIST_STRING_TAG_RE = /<string>/;
|
|
234
|
+
/** Extract up to maxLines evidence snippets from a single plist file. */
|
|
235
|
+
function extractPlistEvidence(path, text, maxLines) {
|
|
236
|
+
const evidence = [];
|
|
237
|
+
const lines = text.split("\n");
|
|
238
|
+
for (let i = 0; i < lines.length && evidence.length < maxLines; i++) {
|
|
239
|
+
if (PLIST_KEY_NAME_RE.test(lines[i]) && i + 1 < lines.length && PLIST_STRING_TAG_RE.test(lines[i + 1])) {
|
|
240
|
+
evidence.push(`${path}:${i + 1}:${lines[i].slice(0, 200)}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return evidence;
|
|
244
|
+
}
|
|
245
|
+
/** CHECK 6b: Secrets embedded in bundled .plist resource files. MASVS-STORAGE-2 / CWE-798 */
|
|
246
|
+
function checkBundleSecrets(ctx) {
|
|
247
|
+
const infoFiles = [];
|
|
248
|
+
const infoEvidence = [];
|
|
249
|
+
for (const [p, text] of ctx.infoPlistEntries) {
|
|
250
|
+
if (!PLIST_SECRET_RE.test(text))
|
|
251
|
+
continue;
|
|
252
|
+
infoFiles.push(p);
|
|
253
|
+
if (infoEvidence.length < 10) {
|
|
254
|
+
infoEvidence.push(...extractPlistEvidence(p, text, 10 - infoEvidence.length));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const resourceFiles = ctx.resourcePlistEntries
|
|
258
|
+
.filter(([, text]) => PLIST_SECRET_RE.test(text))
|
|
259
|
+
.map(([p]) => p);
|
|
260
|
+
const allFiles = [...infoFiles, ...resourceFiles];
|
|
261
|
+
if (allFiles.length === 0)
|
|
262
|
+
return null;
|
|
263
|
+
return {
|
|
264
|
+
id: "IOS_BUNDLE_SECRETS",
|
|
265
|
+
title: "Potential secret or API key embedded in Info.plist or bundled .plist resource file (CWE-798)",
|
|
266
|
+
severity: "CRITICAL",
|
|
267
|
+
files: allFiles.slice(0, 10),
|
|
268
|
+
evidence: infoEvidence.slice(0, 10),
|
|
269
|
+
requiredActions: [
|
|
270
|
+
"Do not ship API keys or secrets in bundled plist files — the app bundle is extractable from any device.",
|
|
271
|
+
"Fetch secrets from a secure backend endpoint authenticated by the user's identity, or use a key derivation approach.",
|
|
272
|
+
"MASVS-STORAGE-2 / CWE-798"
|
|
273
|
+
]
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/** CHECK 7: MD5/SHA1/DES/ECB usage in CommonCrypto. MASVS-CRYPTO-1 / CWE-327 */
|
|
277
|
+
function checkWeakCrypto(ctx) {
|
|
278
|
+
const WEAK_CRYPTO_RE = /kCCAlgorithmDES|kCCAlgorithmRC2|kCCAlgorithmRC4|CC_MD5|CC_SHA1\b|kCCOptionECBMode/;
|
|
279
|
+
const files = filesWithMatch(ctx.allNativeSources, WEAK_CRYPTO_RE);
|
|
280
|
+
if (files.length === 0)
|
|
281
|
+
return null;
|
|
282
|
+
return {
|
|
283
|
+
id: "IOS_WEAK_CRYPTO",
|
|
284
|
+
title: "Weak cryptographic algorithm used: MD5/SHA1/DES/ECB via CommonCrypto (MASVS-CRYPTO-1)",
|
|
285
|
+
severity: "CRITICAL",
|
|
286
|
+
files: files.slice(0, 10),
|
|
287
|
+
evidence: evidenceLines(ctx.allNativeSources, WEAK_CRYPTO_RE),
|
|
288
|
+
requiredActions: [
|
|
289
|
+
"Replace DES/RC2/RC4 with AES-256-GCM (kCCAlgorithmAES + kCCOptionPKCS7Padding with GCM mode via CryptoKit).",
|
|
290
|
+
"Replace CC_MD5/CC_SHA1 with SHA-256 or SHA-3 (CC_SHA256 or CryptoKit's SHA256/SHA512).",
|
|
291
|
+
"Never use ECB mode — it leaks plaintext patterns; use CBC with random IV or GCM.",
|
|
292
|
+
"MASVS-CRYPTO-1 / CWE-327 / NIST SP 800-131A Rev 2"
|
|
293
|
+
]
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** CHECK 8: Objective-C files compiled without ARC (-fno-objc-arc). MASVS-CODE-4 / CWE-401 */
|
|
297
|
+
function checkArcDisabled(ctx) {
|
|
298
|
+
const ARC_DISABLED_RE = /-fno-objc-arc/;
|
|
299
|
+
const srcFiles = filesWithMatch(ctx.objcSources, ARC_DISABLED_RE);
|
|
300
|
+
const pbxFiles = filesWithMatch(ctx.pbxprojs, ARC_DISABLED_RE);
|
|
301
|
+
const allFiles = [...new Set([...srcFiles, ...pbxFiles])];
|
|
302
|
+
if (allFiles.length === 0)
|
|
303
|
+
return null;
|
|
304
|
+
return {
|
|
305
|
+
id: "IOS_ARC_DISABLED",
|
|
306
|
+
title: "Objective-C source compiled without ARC (-fno-objc-arc) — memory safety risk",
|
|
307
|
+
severity: "HIGH",
|
|
308
|
+
files: allFiles.slice(0, 10),
|
|
309
|
+
evidence: evidenceLines(ctx.objcSources, ARC_DISABLED_RE),
|
|
310
|
+
requiredActions: [
|
|
311
|
+
"Enable ARC for all Objective-C files. Remove -fno-objc-arc compiler flag from build settings and file-level flags.",
|
|
312
|
+
"Manual memory management is error-prone and substantially increases the risk of use-after-free and heap corruption vulnerabilities.",
|
|
313
|
+
"MASVS-CODE-4 / CWE-401"
|
|
314
|
+
]
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
/** CHECK 9: No jailbreak detection in iOS codebase. MASVS-RESILIENCE-1 */
|
|
318
|
+
function checkJailbreakDetectionMissing(ctx) {
|
|
319
|
+
if (ctx.allNativeSources.size === 0)
|
|
320
|
+
return null;
|
|
321
|
+
const JAILBREAK_RE = /jailbreak|cydia|substrate|MobileSubstrate|fileExistsAtPath.*Applications|canOpenURL.*cydia|checkJailbreak/i;
|
|
322
|
+
const found = filesWithMatch(ctx.allNativeSources, JAILBREAK_RE);
|
|
323
|
+
if (found.length > 0)
|
|
324
|
+
return null;
|
|
325
|
+
return {
|
|
326
|
+
id: "IOS_JAILBREAK_DETECTION_MISSING",
|
|
327
|
+
title: "No jailbreak detection found in iOS codebase (MASVS-RESILIENCE-1)",
|
|
328
|
+
severity: "MEDIUM",
|
|
329
|
+
requiredActions: [
|
|
330
|
+
"Implement jailbreak detection for high-risk apps: check for Cydia, substrate, /Applications paths, and dyld injection.",
|
|
331
|
+
"Consider using a commercial RASP SDK (e.g., Guardsquare iXGuard) for tamper-resistant detection.",
|
|
332
|
+
"MASVS-RESILIENCE-1: apps without jailbreak detection run in a fully compromised security context without warning.",
|
|
333
|
+
"At minimum, detect and log — do not silently trust jailbroken environments for financial, healthcare, or government apps."
|
|
334
|
+
]
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/** CHECK 10: URLSession without certificate pinning. MASVS-NETWORK-2 / CWE-295 */
|
|
338
|
+
function checkCertPinningMissing(ctx) {
|
|
339
|
+
const URL_SESSION_RE = /URLSession/;
|
|
340
|
+
const PINNING_RE = /didReceive.*challenge|URLSession.*pinning|TrustKit|Alamofire.*ServerTrustManager|ServerTrustPolicy|pinnedCertificates|evaluateTrust/i;
|
|
341
|
+
const urlSessionFiles = filesWithMatch(ctx.allNativeSources, URL_SESSION_RE);
|
|
342
|
+
if (urlSessionFiles.length === 0)
|
|
343
|
+
return null;
|
|
344
|
+
const pinningFiles = filesWithMatch(ctx.allNativeSources, PINNING_RE);
|
|
345
|
+
if (pinningFiles.length > 0)
|
|
346
|
+
return null;
|
|
347
|
+
return {
|
|
348
|
+
id: "IOS_CERTIFICATE_PINNING_MISSING",
|
|
349
|
+
title: "URLSession usage detected but no certificate pinning implementation found (MASVS-NETWORK-2)",
|
|
350
|
+
severity: "HIGH",
|
|
351
|
+
files: urlSessionFiles.slice(0, 10),
|
|
352
|
+
requiredActions: [
|
|
353
|
+
"Implement certificate pinning via URLSessionDelegate didReceive(_:challenge:completionHandler:) or use TrustKit.",
|
|
354
|
+
"Pin the SPKI (SubjectPublicKeyInfo) hash rather than the full leaf certificate to survive cert renewals.",
|
|
355
|
+
"MASVS-NETWORK-2 / CWE-295: without pinning, all HTTPS traffic is vulnerable to interception by trusted-but-malicious CAs."
|
|
356
|
+
]
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
/** CHECK 11: UIPasteboard.general used — sensitive data may leak. MASVS-PLATFORM-4 / CWE-200 */
|
|
360
|
+
function checkPasteboardSensitive(ctx) {
|
|
361
|
+
const PASTEBOARD_RE = /UIPasteboard\.general/;
|
|
362
|
+
const files = filesWithMatch(ctx.allNativeSources, PASTEBOARD_RE);
|
|
363
|
+
if (files.length === 0)
|
|
364
|
+
return null;
|
|
365
|
+
return {
|
|
366
|
+
id: "IOS_PASTEBOARD_SENSITIVE",
|
|
367
|
+
title: "UIPasteboard.general used — sensitive data may leak to other apps via system clipboard",
|
|
368
|
+
severity: "HIGH",
|
|
369
|
+
files: files.slice(0, 10),
|
|
370
|
+
evidence: evidenceLines(ctx.allNativeSources, PASTEBOARD_RE),
|
|
371
|
+
requiredActions: [
|
|
372
|
+
"Audit all UIPasteboard.general writes to ensure no passwords, tokens, or PII are placed on the general pasteboard.",
|
|
373
|
+
"For app-internal copy/paste of sensitive data, use UIPasteboard(name:create:) with a private named pasteboard.",
|
|
374
|
+
"Set expiration: pasteboard.setItems([data], options: [.expirationDate: Date().addingTimeInterval(30)])",
|
|
375
|
+
"MASVS-PLATFORM-4 / CWE-200: the general pasteboard is readable by all installed apps."
|
|
376
|
+
]
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
/** CHECK 12: Sensitive view controllers without screenshot / screen-capture protection. MASVS-RESILIENCE-2 / CWE-359 */
|
|
380
|
+
function checkScreenshotUnprotected(ctx) {
|
|
381
|
+
const SCREEN_CAPTURE_RE = /UIScreen\.main\.isCaptured|isSecureTextEntry\s*=\s*true|userDidTakeScreenshotNotification/;
|
|
382
|
+
if (filesWithMatch(ctx.allNativeSources, SCREEN_CAPTURE_RE).length > 0)
|
|
383
|
+
return null;
|
|
384
|
+
const SENSITIVE_VC_RE = /ViewController.*(?:Password|Payment|Auth|Secret|Credential|Card|Account)|(?:Password|Payment|Auth|Secret|Credential|Card|Account).*ViewController/i;
|
|
385
|
+
const sensitiveFiles = filesWithMatch(ctx.allNativeSources, SENSITIVE_VC_RE);
|
|
386
|
+
if (sensitiveFiles.length === 0)
|
|
387
|
+
return null;
|
|
388
|
+
return {
|
|
389
|
+
id: "IOS_SCREENSHOT_UNPROTECTED",
|
|
390
|
+
title: "Sensitive view controllers detected without screenshot / screen-capture protection",
|
|
391
|
+
severity: "MEDIUM",
|
|
392
|
+
files: sensitiveFiles.slice(0, 10),
|
|
393
|
+
requiredActions: [
|
|
394
|
+
"Check UIScreen.main.isCaptured and hide/blur sensitive fields when the screen is being recorded or mirrored.",
|
|
395
|
+
"Subscribe to UIScreen.capturedDidChangeNotification to react to capture-state changes.",
|
|
396
|
+
"Set isSecureTextEntry = true on all password and sensitive input fields.",
|
|
397
|
+
"MASVS-RESILIENCE-2 / CWE-359: screen recordings and screenshots expose sensitive data to malicious screen-capture apps."
|
|
398
|
+
]
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
/** CHECK 13: WKWebView addScriptMessageHandler exposes native bridge to JS. MASVS-PLATFORM-5 / CWE-749 */
|
|
402
|
+
function checkWebviewJsBridge(ctx) {
|
|
403
|
+
const WEBVIEW_HANDLER_RE = /addScriptMessageHandler/;
|
|
404
|
+
const files = filesWithMatch(ctx.allNativeSources, WEBVIEW_HANDLER_RE);
|
|
405
|
+
if (files.length === 0)
|
|
406
|
+
return null;
|
|
407
|
+
return {
|
|
408
|
+
id: "IOS_WEBVIEW_JS_ENABLED",
|
|
409
|
+
title: "WKWebView addScriptMessageHandler exposes native bridge to JavaScript — XSS can reach native code",
|
|
410
|
+
severity: "CRITICAL",
|
|
411
|
+
files: files.slice(0, 10),
|
|
412
|
+
evidence: evidenceLines(ctx.allNativeSources, WEBVIEW_HANDLER_RE),
|
|
413
|
+
requiredActions: [
|
|
414
|
+
"Audit all WKScriptMessageHandler implementations: validate every message name and payload type strictly.",
|
|
415
|
+
"Never expose sensitive native APIs (file system, keychain, network) through the JS bridge.",
|
|
416
|
+
"Disable JavaScript if it is not required: config.preferences.javaScriptEnabled = false",
|
|
417
|
+
"Load only trusted first-party content in webviews that have a native bridge — never load external URLs.",
|
|
418
|
+
"MASVS-PLATFORM-5 / CWE-749: XSS in a bridged WKWebView is equivalent to remote code execution in the native context."
|
|
419
|
+
]
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/** CHECK 14: CFBundleURLTypes registered without source-level origin validation. MASVS-PLATFORM-3 / CWE-939 */
|
|
423
|
+
function checkUrlSchemeUnvalidated(ctx) {
|
|
424
|
+
const URL_SCHEME_PLIST_RE = /CFBundleURLTypes/;
|
|
425
|
+
const plistsWithSchemes = ctx.infoPlistEntries
|
|
426
|
+
.filter(([, text]) => URL_SCHEME_PLIST_RE.test(text))
|
|
427
|
+
.map(([p]) => p);
|
|
428
|
+
if (plistsWithSchemes.length === 0)
|
|
429
|
+
return null;
|
|
430
|
+
const VALIDATION_RE = /application.*open.*url.*options|openURL.*validat|url\.scheme|url\.host|allowedSchemes/i;
|
|
431
|
+
if (filesWithMatch(ctx.allNativeSources, VALIDATION_RE).length > 0)
|
|
432
|
+
return null;
|
|
433
|
+
return {
|
|
434
|
+
id: "IOS_URL_SCHEME_UNVALIDATED",
|
|
435
|
+
title: "Custom URL scheme registered in Info.plist but no scheme/origin validation found in source",
|
|
436
|
+
severity: "HIGH",
|
|
437
|
+
files: plistsWithSchemes.slice(0, 10),
|
|
438
|
+
requiredActions: [
|
|
439
|
+
"In application(_:open:options:), validate the full URL: scheme, host, and path against a strict allowlist before acting on it.",
|
|
440
|
+
"Check UIApplicationOpenURLOptionsSourceApplicationKey to verify the calling app's bundle ID.",
|
|
441
|
+
"MASVS-PLATFORM-3 / CWE-939: unvalidated URL schemes allow any installed app to trigger deep-link actions with attacker-controlled parameters."
|
|
442
|
+
]
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
/** CHECK 15: LAContext without evaluatedPolicyDomainState enrollment-change detection. MASVS-AUTH-3 / CWE-287 */
|
|
446
|
+
function checkBiometricWeak(ctx) {
|
|
447
|
+
const LACONTEXT_RE = /LAContext/;
|
|
448
|
+
const laContextFiles = filesWithMatch(ctx.allNativeSources, LACONTEXT_RE);
|
|
449
|
+
if (laContextFiles.length === 0)
|
|
450
|
+
return null;
|
|
451
|
+
const ENROLLMENT_RE = /evaluatedPolicyDomainState|domainState|LABiometryType|deviceOwnerAuthenticationWithBiometrics/;
|
|
452
|
+
if (filesWithMatch(ctx.allNativeSources, ENROLLMENT_RE).length > 0)
|
|
453
|
+
return null;
|
|
454
|
+
return {
|
|
455
|
+
id: "IOS_BIOMETRIC_WEAK",
|
|
456
|
+
title: "LAContext used without evaluatedPolicyDomainState enrollment-change detection",
|
|
457
|
+
severity: "HIGH",
|
|
458
|
+
files: laContextFiles.slice(0, 10),
|
|
459
|
+
requiredActions: [
|
|
460
|
+
"After each successful biometric authentication, save context.evaluatedPolicyDomainState and compare on the next auth to detect added/removed fingerprints.",
|
|
461
|
+
"If the domain state changes, invalidate the session and require re-authentication.",
|
|
462
|
+
"MASVS-AUTH-3 / CWE-287: without enrollment detection, an attacker who adds their own fingerprint to the device can bypass biometric authentication."
|
|
463
|
+
]
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
/** CHECK 16: ENABLE_BITCODE = YES in release xcconfig or pbxproj. MASVS-CODE-2 */
|
|
467
|
+
function checkBitcodeEnabled(ctx) {
|
|
468
|
+
const BITCODE_RE = /ENABLE_BITCODE\s*=\s*YES/i;
|
|
469
|
+
const files = [...new Set([...filesWithMatch(ctx.xcconfigs, BITCODE_RE), ...filesWithMatch(ctx.pbxprojs, BITCODE_RE)])];
|
|
470
|
+
if (files.length === 0)
|
|
471
|
+
return null;
|
|
472
|
+
return {
|
|
473
|
+
id: "IOS_BITCODE_ENABLED",
|
|
474
|
+
title: "ENABLE_BITCODE = YES found — Apple recompiles your binary from bitcode, reducing reverse-engineering barrier",
|
|
475
|
+
severity: "LOW",
|
|
476
|
+
files: files.slice(0, 10),
|
|
477
|
+
requiredActions: [
|
|
478
|
+
"Set ENABLE_BITCODE = NO for release builds if your threat model requires controlling the exact compiled binary.",
|
|
479
|
+
"Apple deprecated bitcode for iOS/tvOS in Xcode 14 — leaving it enabled has no benefit on modern toolchains.",
|
|
480
|
+
"MASVS-CODE-2: submitted bitcode can be inspected by Apple and recompiled into a different optimization level than tested."
|
|
481
|
+
]
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
/** CHECK 17: DEBUG preprocessor flag or debug plist entries in production. MASVS-CODE-2 / CWE-11 */
|
|
485
|
+
function checkDebugFlagProduction(ctx) {
|
|
486
|
+
const DEBUG_PLIST_RE = /(?:<key>NSAssertionHandler<\/key>|<key>LSEnvironment<\/key>[^]*?DEBUG\s*=\s*1|<key>DEBUG<\/key>\s*<true\/>)/i;
|
|
487
|
+
const debugPlistFiles = ctx.infoPlistEntries.filter(([, t]) => DEBUG_PLIST_RE.test(t)).map(([p]) => p);
|
|
488
|
+
const DEBUG_SOURCE_RE = /#if\s+DEBUG\s*$|DEBUG\s*=\s*true|isDebugBuild\s*=\s*true/m;
|
|
489
|
+
const debugSourceFiles = filesWithMatch(ctx.allNativeSources, DEBUG_SOURCE_RE);
|
|
490
|
+
const allFiles = [...new Set([...debugPlistFiles, ...debugSourceFiles])];
|
|
491
|
+
if (allFiles.length === 0)
|
|
492
|
+
return null;
|
|
493
|
+
return {
|
|
494
|
+
id: "IOS_DEBUG_FLAG_PRODUCTION",
|
|
495
|
+
title: "DEBUG build configuration or flag detected in production plist or source",
|
|
496
|
+
severity: "CRITICAL",
|
|
497
|
+
files: allFiles.slice(0, 10),
|
|
498
|
+
requiredActions: [
|
|
499
|
+
"Ensure DEBUG preprocessor flags are never compiled into release/production builds.",
|
|
500
|
+
"Remove or guard all #if DEBUG blocks that bypass authentication, disable pinning, or log sensitive data.",
|
|
501
|
+
"Validate that release scheme targets use Release configuration, not Debug.",
|
|
502
|
+
"MASVS-CODE-2 / CWE-11: debug builds often disable security controls and expose internal state."
|
|
503
|
+
]
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
/** CHECK 18: Core Data persistent store without NSPersistentStoreFileProtectionKey. MASVS-STORAGE-1 / CWE-312 */
|
|
507
|
+
function checkCoreDataUnencrypted(ctx) {
|
|
508
|
+
const CORE_DATA_RE = /NSPersistentStoreCoordinator|NSPersistentContainer/;
|
|
509
|
+
const coreDataFiles = filesWithMatch(ctx.allNativeSources, CORE_DATA_RE);
|
|
510
|
+
if (coreDataFiles.length === 0)
|
|
511
|
+
return null;
|
|
512
|
+
const PROTECTION_RE = /NSPersistentStoreFileProtectionKey|NSFileProtectionComplete|NSFileProtectionCompleteUnlessOpen/;
|
|
513
|
+
if (filesWithMatch(ctx.allNativeSources, PROTECTION_RE).length > 0)
|
|
514
|
+
return null;
|
|
515
|
+
return {
|
|
516
|
+
id: "IOS_CORE_DATA_UNENCRYPTED",
|
|
517
|
+
title: "Core Data persistent store used without NSPersistentStoreFileProtectionKey — database unencrypted at rest when device is locked",
|
|
518
|
+
severity: "HIGH",
|
|
519
|
+
files: coreDataFiles.slice(0, 10),
|
|
520
|
+
requiredActions: [
|
|
521
|
+
"Add NSPersistentStoreFileProtectionKey: NSFileProtectionComplete to the persistent store options dictionary.",
|
|
522
|
+
"options[NSPersistentStoreFileProtectionKey] = FileProtectionType.complete",
|
|
523
|
+
"MASVS-STORAGE-1 / CWE-312: without Data Protection, the SQLite file is accessible to attackers with physical access to an unlocked device or via a jailbreak."
|
|
524
|
+
]
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
/** CHECK 19: Network debugging / proxy tool reference in production source. MASVS-CODE-2 / CWE-532 */
|
|
528
|
+
function checkNetworkLoggerProduction(ctx) {
|
|
529
|
+
const NETWORK_LOGGER_RE = /(?:Charles|Proxyman|Paw|Rocketim|Netfox|GDNetwork|NetworkActivityLogger|ResponseSniffer|Wormholy)/i;
|
|
530
|
+
const files = filesWithMatch(ctx.allNativeSources, NETWORK_LOGGER_RE);
|
|
531
|
+
if (files.length === 0)
|
|
532
|
+
return null;
|
|
533
|
+
return {
|
|
534
|
+
id: "IOS_NETWORK_LOGGER_PRODUCTION",
|
|
535
|
+
title: "Network debugging / proxy tool reference found in source — must be stripped from production builds",
|
|
536
|
+
severity: "HIGH",
|
|
537
|
+
files: files.slice(0, 10),
|
|
538
|
+
evidence: evidenceLines(ctx.allNativeSources, NETWORK_LOGGER_RE),
|
|
539
|
+
requiredActions: [
|
|
540
|
+
"Wrap all network debugging integrations (Wormholy, Netfox, Charles proxy config) in #if DEBUG guards.",
|
|
541
|
+
"Ensure no debug proxy certificate or trust-all-certs workaround reaches the release binary.",
|
|
542
|
+
"MASVS-CODE-2 / CWE-532: network loggers in production expose all API traffic and can bypass certificate pinning."
|
|
543
|
+
]
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
/** CHECK 20: Sensitive data written to NSTemporaryDirectory. MASVS-STORAGE-1 */
|
|
547
|
+
function checkNsTempDirSensitive(ctx) {
|
|
548
|
+
const TEMP_DIR_RE = /NSTemporaryDirectory\(\)|FileManager\.default\.temporaryDirectory/;
|
|
549
|
+
const SENSITIVE_RE = /(?:token|password|secret|credential|auth|key)/i;
|
|
550
|
+
const files = filesWithMatch(ctx.allNativeSources, TEMP_DIR_RE).filter((f) => SENSITIVE_RE.test(ctx.allNativeSources.get(f) ?? ""));
|
|
551
|
+
if (files.length === 0)
|
|
552
|
+
return null;
|
|
553
|
+
return {
|
|
554
|
+
id: "IOS_TEMP_DIR_SENSITIVE",
|
|
555
|
+
title: "iOS sensitive data written to NSTemporaryDirectory — not guaranteed cleanup, accessible on jailbroken devices (MASVS-STORAGE-1)",
|
|
556
|
+
severity: "HIGH",
|
|
557
|
+
files: files.slice(0, 10),
|
|
558
|
+
evidence: evidenceLines(ctx.allNativeSources, TEMP_DIR_RE),
|
|
559
|
+
requiredActions: [
|
|
560
|
+
"Do not write credential-class data (tokens, passwords, secrets) to NSTemporaryDirectory — the OS does not guarantee timely cleanup.",
|
|
561
|
+
"Use the iOS Keychain for credentials. If temporary scratch files are needed, write to a sub-directory of the app's Documents folder with NSURLIsExcludedFromBackupKey and NSFileProtectionComplete.",
|
|
562
|
+
"MASVS-STORAGE-1: temp files are accessible on jailbroken devices and can survive across app restarts."
|
|
563
|
+
]
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
/** CHECK 21: NSFileProtectionNone set on file — readable when device is locked. MASVS-STORAGE-1 */
|
|
567
|
+
function checkNsFileProtectionNone(ctx) {
|
|
568
|
+
const PROTECTION_NONE_RE = /NSFileProtectionNone|\.noProtection|FileProtectionType\.none/;
|
|
569
|
+
const files = filesWithMatch(ctx.allNativeSources, PROTECTION_NONE_RE);
|
|
570
|
+
if (files.length === 0)
|
|
571
|
+
return null;
|
|
572
|
+
return {
|
|
573
|
+
id: "IOS_FILE_PROTECTION_NONE",
|
|
574
|
+
title: "NSFileProtectionNone set on file — readable when device is locked or powered off (MASVS-STORAGE-1)",
|
|
575
|
+
severity: "CRITICAL",
|
|
576
|
+
files: files.slice(0, 10),
|
|
577
|
+
evidence: evidenceLines(ctx.allNativeSources, PROTECTION_NONE_RE),
|
|
578
|
+
requiredActions: [
|
|
579
|
+
"Replace NSFileProtectionNone with NSFileProtectionComplete for all sensitive files.",
|
|
580
|
+
"Use NSFileProtectionCompleteUnlessOpen only when the file must be accessible while the device is locked for a specific background task.",
|
|
581
|
+
"MASVS-STORAGE-1 / CWE-311: NSFileProtectionNone means the file is readable in any device state, including when locked or seized."
|
|
582
|
+
]
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/** CHECK 22: @AppStorage used for sensitive data — backed by UserDefaults. MASVS-STORAGE-1 */
|
|
586
|
+
function checkAppStorageSensitive(ctx) {
|
|
587
|
+
const APPSTORAGE_RE = /@AppStorage\s*\([^)]*(?:token|password|secret|credential|auth|key)/i;
|
|
588
|
+
const files = filesWithMatch(ctx.allNativeSources, APPSTORAGE_RE);
|
|
589
|
+
if (files.length === 0)
|
|
590
|
+
return null;
|
|
591
|
+
return {
|
|
592
|
+
id: "IOS_APPSTORAGE_SENSITIVE",
|
|
593
|
+
title: "@AppStorage used for sensitive data — backed by UserDefaults, unencrypted, included in iTunes backups (MASVS-STORAGE-1)",
|
|
594
|
+
severity: "HIGH",
|
|
595
|
+
files: files.slice(0, 10),
|
|
596
|
+
evidence: evidenceLines(ctx.allNativeSources, APPSTORAGE_RE),
|
|
597
|
+
requiredActions: [
|
|
598
|
+
"Replace @AppStorage for credential-class keys with a Keychain wrapper property wrapper.",
|
|
599
|
+
"@AppStorage is syntactic sugar over UserDefaults — it is unencrypted and included in iCloud/iTunes backups by default.",
|
|
600
|
+
"MASVS-STORAGE-1 / CWE-312: tokens and passwords in UserDefaults are trivially readable on jailbroken devices."
|
|
601
|
+
]
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/** CHECK 23: iOS SQLite database without SQLCipher encryption. MASVS-STORAGE-1 */
|
|
605
|
+
function checkSqliteUnencrypted(ctx) {
|
|
606
|
+
const SQLITE_RE = /import FMDB|import SQLite|FMDatabase\s*\(|Connection\s*\([^)]*\.db/;
|
|
607
|
+
const CIPHER_RE = /SQLCipher|sqlite3_key|PRAGMA key/;
|
|
608
|
+
const files = filesWithMatch(ctx.allNativeSources, SQLITE_RE).filter((f) => !CIPHER_RE.test(ctx.allNativeSources.get(f) ?? ""));
|
|
609
|
+
if (files.length === 0)
|
|
610
|
+
return null;
|
|
611
|
+
return {
|
|
612
|
+
id: "IOS_SQLITE_UNENCRYPTED",
|
|
613
|
+
title: "iOS SQLite database without SQLCipher encryption — readable on jailbroken devices (MASVS-STORAGE-1)",
|
|
614
|
+
severity: "HIGH",
|
|
615
|
+
files: files.slice(0, 10),
|
|
616
|
+
evidence: evidenceLines(ctx.allNativeSources, SQLITE_RE),
|
|
617
|
+
requiredActions: [
|
|
618
|
+
"Replace FMDB/SQLite.swift with a SQLCipher-backed variant and set a strong database key via sqlite3_key.",
|
|
619
|
+
"Derive the database key from the user's passphrase + device-bound secret using PBKDF2 or Argon2 — do not hardcode it.",
|
|
620
|
+
"MASVS-STORAGE-1: plaintext SQLite files are trivially copied and opened on jailbroken devices or via physical acquisition."
|
|
621
|
+
]
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
/** CHECK 24: WKWebView loading http:// URLs with JavaScript enabled. MASVS-NETWORK-1 */
|
|
625
|
+
function checkWkWebviewHttpLoad(ctx) {
|
|
626
|
+
const WEBVIEW_RE = /WKWebView/;
|
|
627
|
+
const HTTP_LOAD_RE = /loadRequest.*http:|load.*URLRequest.*http:/i;
|
|
628
|
+
const JS_BRIDGE_RE = /javaScriptEnabled\s*=\s*true/;
|
|
629
|
+
const files = filesWithMatch(ctx.allNativeSources, WEBVIEW_RE).filter((f) => {
|
|
630
|
+
const content = ctx.allNativeSources.get(f) ?? "";
|
|
631
|
+
return (HTTP_LOAD_RE.test(content) || JS_BRIDGE_RE.test(content)) && /http:\/\//.test(content);
|
|
632
|
+
});
|
|
633
|
+
if (files.length === 0)
|
|
634
|
+
return null;
|
|
635
|
+
return {
|
|
636
|
+
id: "IOS_WEBVIEW_HTTP_LOAD",
|
|
637
|
+
title: "WKWebView with JavaScript enabled loading http:// — full MITM enables JS bridge injection (MASVS-NETWORK-1)",
|
|
638
|
+
severity: "CRITICAL",
|
|
639
|
+
files: files.slice(0, 10),
|
|
640
|
+
evidence: evidenceLines(ctx.allNativeSources, HTTP_LOAD_RE),
|
|
641
|
+
requiredActions: [
|
|
642
|
+
"Always load WKWebView content over https:// — enforce this in ATS and in the URL construction logic.",
|
|
643
|
+
"If http:// is required for a legacy endpoint, disable JavaScript (config.preferences.javaScriptEnabled = false) and remove all WKScriptMessageHandler registrations.",
|
|
644
|
+
"MASVS-NETWORK-1 / CWE-319: a MITM attacker on http:// can inject arbitrary JavaScript that communicates with any registered native bridge handler."
|
|
645
|
+
]
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
/** CHECK 25: Universal Links configured — verify AASA served over HTTPS. MASVS-PLATFORM-3 */
|
|
649
|
+
function checkUniversalLinkConfig(ctx) {
|
|
650
|
+
const APPLINKS_RE = /applinks:|webcredentials:|NSUserActivityTypes/;
|
|
651
|
+
const allSources = new Map([
|
|
652
|
+
...ctx.allNativeSources,
|
|
653
|
+
...new Map(ctx.infoPlistEntries)
|
|
654
|
+
]);
|
|
655
|
+
const files = filesWithMatch(allSources, APPLINKS_RE);
|
|
656
|
+
if (files.length === 0)
|
|
657
|
+
return null;
|
|
658
|
+
return {
|
|
659
|
+
id: "IOS_UNIVERSAL_LINK_CONFIG",
|
|
660
|
+
title: "Universal Links configured — verify AASA served over HTTPS with restrictive path patterns (MASVS-PLATFORM-3)",
|
|
661
|
+
severity: "HIGH",
|
|
662
|
+
files: files.slice(0, 10),
|
|
663
|
+
evidence: evidenceLines(allSources, APPLINKS_RE),
|
|
664
|
+
requiredActions: [
|
|
665
|
+
"Ensure the apple-app-site-association (AASA) file is served over HTTPS with no redirects and a valid TLS certificate.",
|
|
666
|
+
"Restrict path patterns in the AASA file — avoid catch-all paths like \"/*\"; use the most specific paths possible.",
|
|
667
|
+
"Validate all incoming NSUserActivity URLs in application(_:continue:restorationHandler:) before acting on parameters.",
|
|
668
|
+
"MASVS-PLATFORM-3 / CWE-939: over-broad AASA paths allow attackers to hijack deep links by serving a malicious AASA from a sibling domain."
|
|
669
|
+
]
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
/** CHECK 26: React Native AsyncStorage used for sensitive data. MASVS-STORAGE-1 */
|
|
673
|
+
async function checkRnAsyncStorageSensitive() {
|
|
674
|
+
const JS_EXTENSIONS = ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"];
|
|
675
|
+
const jsIgnore = ["**/node_modules/**", "**/.git/**"];
|
|
676
|
+
const jsSources = await readFileMap(JS_EXTENSIONS, jsIgnore);
|
|
677
|
+
const ASYNC_STORAGE_RE = /AsyncStorage\.setItem\s*\([^,]*(?:token|password|secret|auth|key)/i;
|
|
678
|
+
const files = filesWithMatch(jsSources, ASYNC_STORAGE_RE);
|
|
679
|
+
if (files.length === 0)
|
|
680
|
+
return null;
|
|
681
|
+
return {
|
|
682
|
+
id: "RN_ASYNC_STORAGE_SENSITIVE",
|
|
683
|
+
title: "React Native AsyncStorage used for sensitive data — unencrypted, readable on rooted devices (MASVS-STORAGE-1)",
|
|
684
|
+
severity: "HIGH",
|
|
685
|
+
files: files.slice(0, 10),
|
|
686
|
+
evidence: evidenceLines(jsSources, ASYNC_STORAGE_RE),
|
|
687
|
+
requiredActions: [
|
|
688
|
+
"Replace AsyncStorage with react-native-keychain or @react-native-community/encrypted-storage for credential-class data.",
|
|
689
|
+
"AsyncStorage is backed by unencrypted SQLite on Android and unencrypted files on iOS — readable on rooted/jailbroken devices.",
|
|
690
|
+
"MASVS-STORAGE-1: tokens, passwords, and secrets must be stored in the platform keystore (iOS Keychain / Android Keystore)."
|
|
691
|
+
]
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
/** CHECK 27: React Native CodePush OTA without bundle signing. MASVS-RESILIENCE-3 */
|
|
695
|
+
async function checkCodePushIntegrity() {
|
|
696
|
+
const JS_EXTENSIONS = ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"];
|
|
697
|
+
const jsIgnore = ["**/node_modules/**", "**/.git/**"];
|
|
698
|
+
const jsSources = await readFileMap(JS_EXTENSIONS, jsIgnore);
|
|
699
|
+
const CODEPUSH_RE = /CodePush\.sync|codePush\.sync|import.*code-push/i;
|
|
700
|
+
const INTEGRITY_RE = /publicKey|mandatory.*true|rollbackRetryOptions/;
|
|
701
|
+
const files = filesWithMatch(jsSources, CODEPUSH_RE).filter((f) => !INTEGRITY_RE.test(jsSources.get(f) ?? ""));
|
|
702
|
+
if (files.length === 0)
|
|
703
|
+
return null;
|
|
704
|
+
return {
|
|
705
|
+
id: "RN_CODEPUSH_NO_INTEGRITY",
|
|
706
|
+
title: "React Native CodePush OTA without bundle signing — compromised CDN deploys malicious JS (MASVS-RESILIENCE-3)",
|
|
707
|
+
severity: "HIGH",
|
|
708
|
+
files: files.slice(0, 10),
|
|
709
|
+
evidence: evidenceLines(jsSources, CODEPUSH_RE),
|
|
710
|
+
requiredActions: [
|
|
711
|
+
"Enable CodePush bundle signing: generate an RSA key pair and pass the public key via CodePushPublicKey in Info.plist.",
|
|
712
|
+
"Set mandatory: true for critical security patches to prevent users from running outdated bundles.",
|
|
713
|
+
"MASVS-RESILIENCE-3: without signing, a compromised or malicious CDN update can replace the entire JS bundle with attacker code."
|
|
714
|
+
]
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
/** CHECK 28: Expo AsyncStorage for credentials instead of SecureStore. MASVS-STORAGE-1 */
|
|
718
|
+
async function checkExpoAsyncStorage() {
|
|
719
|
+
const JS_EXTENSIONS = ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"];
|
|
720
|
+
const jsIgnore = ["**/node_modules/**", "**/.git/**"];
|
|
721
|
+
const pkgText = await readFileSafe("package.json").catch(() => "");
|
|
722
|
+
if (!/"expo"/.test(pkgText))
|
|
723
|
+
return null;
|
|
724
|
+
const jsSources = await readFileMap(JS_EXTENSIONS, jsIgnore);
|
|
725
|
+
const ASYNC_SENSITIVE_RE = /AsyncStorage.*(?:token|secret|password|auth)/i;
|
|
726
|
+
const SECURE_STORE_RE = /SecureStore\.setItemAsync/;
|
|
727
|
+
const files = filesWithMatch(jsSources, ASYNC_SENSITIVE_RE).filter((f) => !SECURE_STORE_RE.test(jsSources.get(f) ?? ""));
|
|
728
|
+
if (files.length === 0)
|
|
729
|
+
return null;
|
|
730
|
+
return {
|
|
731
|
+
id: "EXPO_ASYNC_STORAGE_SENSITIVE",
|
|
732
|
+
title: "Expo AsyncStorage for credentials instead of SecureStore — not backed by iOS Keychain or Android Keystore (MASVS-STORAGE-1)",
|
|
733
|
+
severity: "HIGH",
|
|
734
|
+
files: files.slice(0, 10),
|
|
735
|
+
evidence: evidenceLines(jsSources, ASYNC_SENSITIVE_RE),
|
|
736
|
+
requiredActions: [
|
|
737
|
+
"Replace AsyncStorage with expo-secure-store (SecureStore.setItemAsync) for all credential-class data.",
|
|
738
|
+
"expo-secure-store uses iOS Keychain and Android Keystore under the hood — AsyncStorage uses unencrypted flat files.",
|
|
739
|
+
"MASVS-STORAGE-1: secrets in AsyncStorage are trivially readable on jailbroken iOS or rooted Android devices."
|
|
740
|
+
]
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
/** CHECK 29: Certificate Transparency enforcement not configured. MASVS-NETWORK-2 */
|
|
744
|
+
function checkCertificateTransparency(ctx) {
|
|
745
|
+
const PINNING_RE = /didReceive.*challenge|TrustKit|ServerTrustManager|ServerTrustPolicy|pinnedCertificates|NSPinnedDomains/i;
|
|
746
|
+
const hasPinning = filesWithMatch(ctx.allNativeSources, PINNING_RE).length > 0 ||
|
|
747
|
+
ctx.infoPlistEntries.some(([, t]) => PINNING_RE.test(t));
|
|
748
|
+
if (!hasPinning)
|
|
749
|
+
return null;
|
|
750
|
+
const CT_RE = /NSRequiresCertificateTransparency|certificateTransparencyEnabled|CTPolicy/;
|
|
751
|
+
const hasCT = filesWithMatch(ctx.allNativeSources, CT_RE).length > 0 ||
|
|
752
|
+
ctx.infoPlistEntries.some(([, t]) => CT_RE.test(t));
|
|
753
|
+
if (hasCT)
|
|
754
|
+
return null;
|
|
755
|
+
return {
|
|
756
|
+
id: "MOBILE_NO_CERT_TRANSPARENCY",
|
|
757
|
+
title: "Certificate Transparency enforcement not configured — misissued CA certificates can MITM without appearing in CT logs (MASVS-NETWORK-2)",
|
|
758
|
+
severity: "MEDIUM",
|
|
759
|
+
requiredActions: [
|
|
760
|
+
"Set NSRequiresCertificateTransparency to true in your ATS dictionary in Info.plist.",
|
|
761
|
+
"When using TrustKit, set kTSKRequireCertificateTransparency: true in the TrustKit configuration.",
|
|
762
|
+
"MASVS-NETWORK-2: without CT enforcement, a misissued certificate from any trusted CA can intercept TLS traffic without appearing in public CT logs."
|
|
763
|
+
]
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
// ── orchestrator ──────────────────────────────────────────────────────────────
|
|
767
|
+
export async function checkMobileIos(_) {
|
|
768
|
+
const findings = [];
|
|
769
|
+
try {
|
|
770
|
+
const ctx = await loadContext();
|
|
771
|
+
// Synchronous checks — push all ATS per-plist findings first
|
|
772
|
+
findings.push(...checkAtsWeak(ctx));
|
|
773
|
+
// Remaining checks return Finding | null
|
|
774
|
+
const candidates = [
|
|
775
|
+
checkBackupAllowed(ctx),
|
|
776
|
+
checkKeychainWeakAccess(ctx),
|
|
777
|
+
checkUserDefaultsSensitive(ctx),
|
|
778
|
+
// checkLogSensitive is async — resolved separately below
|
|
779
|
+
checkHardcodedSecret(ctx),
|
|
780
|
+
checkBundleSecrets(ctx),
|
|
781
|
+
checkWeakCrypto(ctx),
|
|
782
|
+
checkArcDisabled(ctx),
|
|
783
|
+
checkJailbreakDetectionMissing(ctx),
|
|
784
|
+
checkCertPinningMissing(ctx),
|
|
785
|
+
checkPasteboardSensitive(ctx),
|
|
786
|
+
checkScreenshotUnprotected(ctx),
|
|
787
|
+
checkWebviewJsBridge(ctx),
|
|
788
|
+
checkUrlSchemeUnvalidated(ctx),
|
|
789
|
+
checkBiometricWeak(ctx),
|
|
790
|
+
checkBitcodeEnabled(ctx),
|
|
791
|
+
checkDebugFlagProduction(ctx),
|
|
792
|
+
checkCoreDataUnencrypted(ctx),
|
|
793
|
+
checkNetworkLoggerProduction(ctx),
|
|
794
|
+
checkNsTempDirSensitive(ctx),
|
|
795
|
+
checkNsFileProtectionNone(ctx),
|
|
796
|
+
checkAppStorageSensitive(ctx),
|
|
797
|
+
checkSqliteUnencrypted(ctx),
|
|
798
|
+
checkWkWebviewHttpLoad(ctx),
|
|
799
|
+
checkUniversalLinkConfig(ctx),
|
|
800
|
+
checkCertificateTransparency(ctx),
|
|
801
|
+
await checkLogSensitive(ctx),
|
|
802
|
+
await checkRnAsyncStorageSensitive(),
|
|
803
|
+
await checkCodePushIntegrity(),
|
|
804
|
+
await checkExpoAsyncStorage()
|
|
805
|
+
];
|
|
806
|
+
for (const c of candidates) {
|
|
807
|
+
if (c !== null)
|
|
808
|
+
findings.push(c);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
catch (err) {
|
|
812
|
+
console.warn("[checkMobileIos] Internal error:", sanitizeErrorMessage(err instanceof Error ? err.message : String(err)));
|
|
813
|
+
}
|
|
22
814
|
return findings;
|
|
23
815
|
}
|