appstore-precheck 1.12.1 → 1.12.2
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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ All notable changes to this project are documented here. Versioning follows
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.12.2] - 2026-07-08
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **`iosSourceDir` root scans no longer sweep build checkouts**: the code-level greps
|
|
12
|
+
scoped to `$IOS_DIR` (purpose-string, tracking/ATT, IAP, private-API, and the other
|
|
13
|
+
`${SRC_INC[@]}`-based checks) now apply the same `GREP_PRUNE` exclude-dirs as the
|
|
14
|
+
repo-wide passes. When `iosSourceDir` resolves to the repo root (a common config, e.g.
|
|
15
|
+
`"."`), these greps previously swept gitignored `build/`, `DerivedData/`, and
|
|
16
|
+
`SourcePackages/checkouts/` output, misreading vendored SDK code as the app's own and
|
|
17
|
+
producing false FAILs/WARNs — observed in the wild as an ATT 5.1.2 FAIL citing a
|
|
18
|
+
RevenueCat mock under a `build/SourcePackages` checkout, and a 2.5.1 private-API FAIL
|
|
19
|
+
citing the `sentry-cocoa` checkout under `.build/`.
|
|
20
|
+
|
|
8
21
|
## [1.12.1] - 2026-07-08
|
|
9
22
|
|
|
10
23
|
Quality patch from a full fresh-eyes review (three independent review passes over the
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appstore-precheck",
|
|
3
|
-
"version": "1.12.
|
|
3
|
+
"version": "1.12.2",
|
|
4
4
|
"description": "Read-only iOS App Store pre-submission check, packaged as a portable Agent Skill with native Claude Code, Cursor, and Codex plugins, plus an npx CLI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Berkay Turk (https://github.com/berkayturk)",
|
|
7
7
|
"homepage": "https://github.com/berkayturk/appstore-precheck",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "https://github.com/berkayturk/appstore-precheck.git"
|
|
10
|
+
"url": "git+https://github.com/berkayturk/appstore-precheck.git"
|
|
11
11
|
},
|
|
12
12
|
"bin": {
|
|
13
13
|
"appstore-precheck": "bin/cli.js"
|
|
@@ -27,5 +27,12 @@
|
|
|
27
27
|
"check-versions": "bash scripts/check-versions.sh",
|
|
28
28
|
"lint": "node --check bin/cli.js && bash -n skills/appstore-precheck/scripts/scan.sh && bash -n skills/appstore-precheck/scripts/verdict.sh && bash -n skills/appstore-precheck/scripts/phase2-precheck.sh && bash -n skills/appstore-precheck/scripts/findings.sh && bash -n skills/appstore-precheck/scripts/suppress.sh && bash -n hooks/fastlane-guard.sh && bash -n install.sh && bash -n tests/run.sh && bash -n tests/all.sh && bash -n tests/test-verdict.sh && bash -n tests/test-guard.sh && bash -n tests/test-config.sh && bash -n tests/test-install.sh && bash -n tests/test-phase2.sh && bash -n tests/test-cli.sh && bash -n tests/test-findings.sh && bash -n tests/test-format-json.sh && bash -n tests/test-suppress.sh && bash -n tests/test-scorecard.sh && bash -n scripts/scorecard.sh && bash -n scripts/scorecard-real.sh && bash -n scripts/update-brew-formula.sh && bash -n tests/test-pack.sh"
|
|
29
29
|
},
|
|
30
|
-
"keywords": [
|
|
30
|
+
"keywords": [
|
|
31
|
+
"ios",
|
|
32
|
+
"app-store",
|
|
33
|
+
"fastlane",
|
|
34
|
+
"agent-skill",
|
|
35
|
+
"claude-code",
|
|
36
|
+
"precheck"
|
|
37
|
+
]
|
|
31
38
|
}
|
|
@@ -4,7 +4,7 @@ description: Read-only pre-submission check for an iOS app before App Store revi
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: Berkay Turk
|
|
7
|
-
version: 1.12.
|
|
7
|
+
version: 1.12.2
|
|
8
8
|
allowed-tools: Bash Read Grep Glob WebFetch mcp__maestro__list_devices mcp__maestro__run mcp__maestro__inspect_screen mcp__maestro__take_screenshot mcp__maestro__cheat_sheet
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -237,7 +237,7 @@ echo "PASS: layout — ios='${IOS_DIR:-?}' metadata='${META_DIR:-?}' xcstrings='
|
|
|
237
237
|
set_rule "privacy-manifest-parity"
|
|
238
238
|
check_required_reason_api() {
|
|
239
239
|
local cat="$1" pattern="$2" hits declared
|
|
240
|
-
hits=$(grep -rEl "$pattern" "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -3)
|
|
240
|
+
hits=$(grep -rEl "$pattern" "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -3)
|
|
241
241
|
declared=$(grep -c "NSPrivacyAccessedAPICategory${cat}" "$PRIVACY_FILE" 2>/dev/null)
|
|
242
242
|
if [[ -n "$hits" && "${declared:-0}" -eq 0 ]]; then
|
|
243
243
|
fail "5.1.1 Required Reason API — '$cat' used in code (e.g. $(echo "$hits" | head -1)) but not declared in PrivacyInfo.xcprivacy" "$PRIVACY_FILE"
|
|
@@ -277,7 +277,7 @@ else
|
|
|
277
277
|
"Contacts:NSContactsUsageDescription" \
|
|
278
278
|
"HealthKit:NSHealthShareUsageDescription"; do
|
|
279
279
|
framework="${fw%%:*}"; needed_key="${fw##*:}"
|
|
280
|
-
if grep -rqE "import ($framework)|($framework)\." "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
280
|
+
if grep -rqE "import ($framework)|($framework)\." "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
281
281
|
if ! grep -qE "$needed_key" "$INFO_PLIST" 2>/dev/null; then
|
|
282
282
|
fail "5.1.1 framework '${framework%%|*}' imported but Info.plist is missing '${needed_key%%|*}'" "$INFO_PLIST"
|
|
283
283
|
fi
|
|
@@ -289,7 +289,7 @@ else
|
|
|
289
289
|
# AVAudioPlayer/AVPlayer, and Photos/PhotosUI's PhotosPicker/PHPickerViewController
|
|
290
290
|
# run out-of-process and need no Info.plist key at all).
|
|
291
291
|
# Camera: purpose string required only when a capture API is actually used.
|
|
292
|
-
if grep -rqE 'AVCaptureDevice|AVCaptureSession|UIImagePickerController' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
292
|
+
if grep -rqE 'AVCaptureDevice|AVCaptureSession|UIImagePickerController' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
293
293
|
grep -qE 'NSCameraUsageDescription' "$INFO_PLIST" 2>/dev/null || \
|
|
294
294
|
fail "5.1.1 camera capture API used but Info.plist is missing 'NSCameraUsageDescription'" "$INFO_PLIST"
|
|
295
295
|
fi
|
|
@@ -297,15 +297,15 @@ else
|
|
|
297
297
|
# separately from the camera check above: a bare AVCaptureDevice only
|
|
298
298
|
# implies microphone use when it is audio-typed (`.audio` device/media
|
|
299
299
|
# type), not for a video-only capture device.
|
|
300
|
-
if grep -rqE 'AVAudioRecorder|installTap\(|AVAudioEngine\(|AVAudioSession[^;]*\.(playAndRecord|record)|AVCaptureDevice[^;)]*\.audio|for:[[:space:]]*\.audio' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
300
|
+
if grep -rqE 'AVAudioRecorder|installTap\(|AVAudioEngine\(|AVAudioSession[^;]*\.(playAndRecord|record)|AVCaptureDevice[^;)]*\.audio|for:[[:space:]]*\.audio' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
301
301
|
grep -qE 'NSMicrophoneUsageDescription' "$INFO_PLIST" 2>/dev/null || \
|
|
302
302
|
fail "5.1.1 microphone/recording API used but Info.plist is missing 'NSMicrophoneUsageDescription'" "$INFO_PLIST"
|
|
303
303
|
fi
|
|
304
304
|
# Photo library READ: PhotosPicker/PHPicker need no key; only true read/fetch APIs do.
|
|
305
|
-
if grep -rqE 'PHAsset\b|PHFetchResult|PHImageManager|fetchAssets|PHAssetCollection' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
305
|
+
if grep -rqE 'PHAsset\b|PHFetchResult|PHImageManager|fetchAssets|PHAssetCollection' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
306
306
|
grep -qE 'NSPhotoLibraryUsageDescription' "$INFO_PLIST" 2>/dev/null || \
|
|
307
307
|
fail "5.1.1 Photos read API used but Info.plist is missing 'NSPhotoLibraryUsageDescription'" "$INFO_PLIST"
|
|
308
|
-
elif grep -rqE 'PHAssetCreationRequest|UIImageWriteToSavedPhotosAlbum|performChanges' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
308
|
+
elif grep -rqE 'PHAssetCreationRequest|UIImageWriteToSavedPhotosAlbum|performChanges' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
309
309
|
# Add-only save: covered by the add-only key.
|
|
310
310
|
grep -qE 'NSPhotoLibraryAddUsageDescription' "$INFO_PLIST" 2>/dev/null || \
|
|
311
311
|
fail "5.1.1 Photos add-only API used but Info.plist is missing 'NSPhotoLibraryAddUsageDescription'" "$INFO_PLIST"
|
|
@@ -319,8 +319,8 @@ fi
|
|
|
319
319
|
# when the ATT framework IS imported; §16 fires when a tracking SDK is present but
|
|
320
320
|
# the ATT prompt is NOT. Computed once here so the two checks never contradict.
|
|
321
321
|
set_rule "att-usage"
|
|
322
|
-
tracking_sdk=$(grep -rlE 'advertisingIdentifier|ASIdentifierManager|GADMobileAds|GoogleMobileAds|AppLovinSDK|ALSdk|AppsFlyerLib|import Adjust|Adjust\.|FBAudienceNetwork|BranchSDK|IronSource' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
323
|
-
att_used=$(grep -rlE 'AppTrackingTransparency|ATTrackingManager' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
322
|
+
tracking_sdk=$(grep -rlE 'advertisingIdentifier|ASIdentifierManager|GADMobileAds|GoogleMobileAds|AppLovinSDK|ALSdk|AppsFlyerLib|import Adjust|Adjust\.|FBAudienceNetwork|BranchSDK|IronSource' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
323
|
+
att_used=$(grep -rlE 'AppTrackingTransparency|ATTrackingManager' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
324
324
|
if [[ -n "$att_used" ]]; then
|
|
325
325
|
if grep -q "NSUserTrackingUsageDescription" "$INFO_PLIST" 2>/dev/null; then
|
|
326
326
|
pass "5.1.2 ATT — used + NSUserTrackingUsageDescription present"
|
|
@@ -459,7 +459,7 @@ fi
|
|
|
459
459
|
# In-app purchase gate — only run 3.1.2 checks if IAP signals exist
|
|
460
460
|
# ===================================================================
|
|
461
461
|
iap_detected=""
|
|
462
|
-
grep -rqE 'SKPaymentQueue|SKProduct|SKMutablePayment|Product\.products|Product\(for:|Product\(id:|\.purchase\(|Transaction\.currentEntitlements|Transaction\.updates|RevenueCat|Purchases\.(shared|configure|logIn|getProducts)|Adapty|Glassfy|import Qonversion' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null && iap_detected=1
|
|
462
|
+
grep -rqE 'SKPaymentQueue|SKProduct|SKMutablePayment|Product\.products|Product\(for:|Product\(id:|\.purchase\(|Transaction\.currentEntitlements|Transaction\.updates|RevenueCat|Purchases\.(shared|configure|logIn|getProducts)|Adapty|Glassfy|import Qonversion' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null && iap_detected=1
|
|
463
463
|
[[ -n "$SUB_VIEW" ]] && iap_detected=1
|
|
464
464
|
|
|
465
465
|
if [[ -z "$iap_detected" ]]; then
|
|
@@ -511,7 +511,7 @@ else
|
|
|
511
511
|
# would false-RED a compliant app; downgrade the missing-link findings to
|
|
512
512
|
# verify-in-dashboard WARNs when that signal is present.
|
|
513
513
|
remote_paywall=""
|
|
514
|
-
grep -rqE 'RevenueCatUI|presentPaywall|paywallFooter|AdaptyUI' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null && remote_paywall=1
|
|
514
|
+
grep -rqE 'RevenueCatUI|presentPaywall|paywallFooter|AdaptyUI' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null && remote_paywall=1
|
|
515
515
|
# paywall_req <label> <pattern> [pass-suffix]: PASS when the pattern is in
|
|
516
516
|
# any paywall view, WARN when a remote paywall likely renders it, else FAIL.
|
|
517
517
|
paywall_req() {
|
|
@@ -537,7 +537,7 @@ fi
|
|
|
537
537
|
# ===================================================================
|
|
538
538
|
set_rule "private-api"
|
|
539
539
|
banned_api='UIWebView|setSelectionIndicatorImage|_UIBackdropView|NSURLConnection[^a-zA-Z]|UIAlertView[^Q]|UIActionSheet[^Q]'
|
|
540
|
-
banned_hits=$(grep -rEnI "$banned_api" "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -5)
|
|
540
|
+
banned_hits=$(grep -rEnI "$banned_api" "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -5)
|
|
541
541
|
if [[ -n "$banned_hits" ]]; then
|
|
542
542
|
pa_first="$(printf '%s\n' "$banned_hits" | head -1)" # "path:line:match"
|
|
543
543
|
pa_file="${pa_first%%:*}"
|
|
@@ -576,9 +576,9 @@ fi
|
|
|
576
576
|
# ===================================================================
|
|
577
577
|
set_rule "siwa-parity"
|
|
578
578
|
if [[ -n "$IOS_DIR" ]]; then
|
|
579
|
-
social_login=$(grep -rlE 'GIDSignIn|import GoogleSignIn|FBSDKLoginKit|FBSDKLoginManager|FacebookLogin|import Auth0|LineSDK|VKSdkAuthorization' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
579
|
+
social_login=$(grep -rlE 'GIDSignIn|import GoogleSignIn|FBSDKLoginKit|FBSDKLoginManager|FacebookLogin|import Auth0|LineSDK|VKSdkAuthorization' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
580
580
|
if [[ -n "$social_login" ]]; then
|
|
581
|
-
if grep -rqE 'ASAuthorizationAppleID|SignInWithAppleButton|AppleIDProvider' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
581
|
+
if grep -rqE 'ASAuthorizationAppleID|SignInWithAppleButton|AppleIDProvider' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
582
582
|
pass "4.8 Sign in with Apple — present alongside third-party login"
|
|
583
583
|
else
|
|
584
584
|
warn "4.8 Sign in with Apple — third-party social login detected (e.g. $(basename "$social_login")) but no Sign in with Apple found; Apple requires an equivalent option (4.8). Some account systems are exempt — verify."
|
|
@@ -591,8 +591,8 @@ fi
|
|
|
591
591
|
# ===================================================================
|
|
592
592
|
set_rule "external-purchase-link"
|
|
593
593
|
ext_purchase=""
|
|
594
|
-
grep -rqE 'ExternalPurchase|ExternalPurchaseLink|ExternalPurchaseCustomLink' "${IOS_DIR:-.}" "${SRC_INC[@]}" 2>/dev/null && ext_purchase=1
|
|
595
|
-
grep -rqE 'external-purchase' "${IOS_DIR:-.}" --include='*.entitlements' 2>/dev/null && ext_purchase=1
|
|
594
|
+
grep -rqE 'ExternalPurchase|ExternalPurchaseLink|ExternalPurchaseCustomLink' "${IOS_DIR:-.}" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null && ext_purchase=1
|
|
595
|
+
grep -rqE 'external-purchase' "${IOS_DIR:-.}" "${GREP_PRUNE[@]}" --include='*.entitlements' 2>/dev/null && ext_purchase=1
|
|
596
596
|
if [[ -n "$ext_purchase" ]]; then
|
|
597
597
|
warn "3.1.1(a) External purchase link detected — ensure the External Purchase entitlement, eligible storefronts, the required disclosure sheet, and App Store Connect reporting are in place (3.1.1(a))."
|
|
598
598
|
fi
|
|
@@ -666,7 +666,7 @@ fi
|
|
|
666
666
|
# incomplete. Soft "verify" wording (a crash-only Sentry may genuinely collect
|
|
667
667
|
# nothing), so WARN, never FAIL.
|
|
668
668
|
set_rule "analytics-privacyinfo-mismatch"
|
|
669
|
-
analytics_sdk=$(grep -rlE 'FirebaseAnalytics|import Firebase|import Amplitude|Amplitude\(|import Mixpanel|Mixpanel\.|import Sentry|SentrySDK|import Segment|SEGAnalytics|Analytics\.shared\(|import Bugsnag|Bugsnag\.|AppCenterAnalytics|import Datadog|DatadogCore' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
669
|
+
analytics_sdk=$(grep -rlE 'FirebaseAnalytics|import Firebase|import Amplitude|Amplitude\(|import Mixpanel|Mixpanel\.|import Sentry|SentrySDK|import Segment|SEGAnalytics|Analytics\.shared\(|import Bugsnag|Bugsnag\.|AppCenterAnalytics|import Datadog|DatadogCore' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
670
670
|
if [[ -n "$analytics_sdk" ]]; then
|
|
671
671
|
declared_data=""
|
|
672
672
|
if [[ -n "$PRIVACY_FILE" && -f "$PRIVACY_FILE" ]]; then
|
|
@@ -709,7 +709,7 @@ fi
|
|
|
709
709
|
# content. We can't tell digital from physical statically, so this is advisory.
|
|
710
710
|
set_rule "thirdparty-payment-sdk"
|
|
711
711
|
if [[ -n "$IOS_DIR" ]]; then
|
|
712
|
-
payment_sdk=$(grep -rlE 'import Stripe|StripePaymentSheet|StripeApplePay|import Braintree|BTPaymentFlow|import PayPal|PayPalCheckout|PayPalNativeCheckout|import Square|SquareInAppPayments|import Adyen|AdyenComponents|RazorpaySDK|import Paddle' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
712
|
+
payment_sdk=$(grep -rlE 'import Stripe|StripePaymentSheet|StripeApplePay|import Braintree|BTPaymentFlow|import PayPal|PayPalCheckout|PayPalNativeCheckout|import Square|SquareInAppPayments|import Adyen|AdyenComponents|RazorpaySDK|import Paddle' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
713
713
|
if [[ -n "$payment_sdk" ]]; then
|
|
714
714
|
warn "3.1.1 Third-party payment SDK — '$(basename "$payment_sdk")' detected; selling digital content/functionality must use in-app purchase, not an external processor (3.1.1). Allowed only for physical goods/services — verify your offering."
|
|
715
715
|
fi
|
|
@@ -723,9 +723,9 @@ fi
|
|
|
723
723
|
# warn when no report/block/moderation affordance is found anywhere in the source.
|
|
724
724
|
set_rule "ugc-no-moderation"
|
|
725
725
|
if [[ -n "$IOS_DIR" ]]; then
|
|
726
|
-
ugc_signal=$(grep -rlE 'userGeneratedContent|\bUGC\b|StreamChat|MessageKit|SendbirdSDK|PubNub|postComment|submitComment|createPost|publishPost|uploadUserPhoto|uploadVideo' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
726
|
+
ugc_signal=$(grep -rlE 'userGeneratedContent|\bUGC\b|StreamChat|MessageKit|SendbirdSDK|PubNub|postComment|submitComment|createPost|publishPost|uploadUserPhoto|uploadVideo' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
727
727
|
if [[ -n "$ugc_signal" ]]; then
|
|
728
|
-
if grep -rqiE 'report(Content|User|Abuse|Post|Comment|Message|Reason|ed)|block(ed)?User|unblockUser|moderat(e|ion|or)|flag(Content|Post|User|Comment|Message)|content.?filter' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
728
|
+
if grep -rqiE 'report(Content|User|Abuse|Post|Comment|Message|Reason|ed)|block(ed)?User|unblockUser|moderat(e|ion|or)|flag(Content|Post|User|Comment|Message)|content.?filter' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
729
729
|
pass "1.2 UGC — user-generated content with report/block/moderation affordances present"
|
|
730
730
|
else
|
|
731
731
|
warn "1.2 UGC — user-generated content detected (e.g. $(basename "$ugc_signal")) but no report/block/moderation mechanism found; UGC apps must offer content filtering, a report mechanism, user blocking, and published contact info (1.2)"
|
|
@@ -757,7 +757,7 @@ fi
|
|
|
757
757
|
# auto-renew copy or a one-time PassKit payment with recurring Apple Pay. We don't
|
|
758
758
|
# try to detect the disclosure text (too noisy) — we flag it for manual verify.
|
|
759
759
|
set_rule "applepay-recurring-disclosure"
|
|
760
|
-
if [[ -n "$IOS_DIR" ]] && grep -rqE 'PKRecurringPaymentRequest' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
760
|
+
if [[ -n "$IOS_DIR" ]] && grep -rqE 'PKRecurringPaymentRequest' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
761
761
|
warn "4.9 Apple Pay — recurring Apple Pay (PKRecurringPaymentRequest) detected; verify you disclose the renewal term, what's provided, the charges, and how to cancel before purchase (4.9)"
|
|
762
762
|
fi
|
|
763
763
|
|
|
@@ -768,9 +768,9 @@ fi
|
|
|
768
768
|
# use the system SKStoreReviewController / requestReview API.
|
|
769
769
|
set_rule "custom-review-prompt"
|
|
770
770
|
if [[ -n "$IOS_DIR" ]]; then
|
|
771
|
-
review_link=$(grep -rlE 'write-review|action=write-review|itms-apps[^"]*review' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
771
|
+
review_link=$(grep -rlE 'write-review|action=write-review|itms-apps[^"]*review' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
772
772
|
if [[ -n "$review_link" ]]; then
|
|
773
|
-
if grep -rqE 'requestReview|SKStoreReviewController|\.requestReview' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
773
|
+
if grep -rqE 'requestReview|SKStoreReviewController|\.requestReview' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
774
774
|
pass "5.6.1 App reviews — uses the system requestReview API alongside the review link"
|
|
775
775
|
else
|
|
776
776
|
warn "5.6.1 App reviews — a direct App Store review link/prompt was found (e.g. $(basename "$review_link")) but no system requestReview (SKStoreReviewController) call; Apple disallows custom review prompts (5.6.1)"
|
|
@@ -834,8 +834,8 @@ fi
|
|
|
834
834
|
# Personal health information must not be stored in iCloud, and HealthKit data
|
|
835
835
|
# may not be used for advertising/marketing.
|
|
836
836
|
set_rule "health-icloud-sync"
|
|
837
|
-
if [[ -n "$IOS_DIR" ]] && grep -rqE 'import HealthKit|HKHealthStore' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
838
|
-
if grep -rqE 'import CloudKit|CKRecord|CKContainer|NSUbiquitousKeyValueStore|NSUbiquitousContainer' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
837
|
+
if [[ -n "$IOS_DIR" ]] && grep -rqE 'import HealthKit|HKHealthStore' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
838
|
+
if grep -rqE 'import CloudKit|CKRecord|CKContainer|NSUbiquitousKeyValueStore|NSUbiquitousContainer' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
839
839
|
warn "5.1.3 Health data — HealthKit and iCloud/CloudKit are both used; personal health information must not be stored in iCloud (5.1.3). Verify HealthKit data is not synced to iCloud."
|
|
840
840
|
else
|
|
841
841
|
pass "5.1.3 Health data — HealthKit used without an obvious iCloud sync path"
|
|
@@ -849,7 +849,7 @@ fi
|
|
|
849
849
|
# on-screen before use, and may not sell/share data.
|
|
850
850
|
set_rule "vpn-networkextension"
|
|
851
851
|
if [[ -n "$IOS_DIR" ]]; then
|
|
852
|
-
vpn_use=$(grep -rlE 'NEVPNManager|NETunnelProviderManager|NEPacketTunnelProvider|NEVPNProtocol' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
852
|
+
vpn_use=$(grep -rlE 'NEVPNManager|NETunnelProviderManager|NEPacketTunnelProvider|NEVPNProtocol' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
853
853
|
if [[ -n "$vpn_use" ]]; then
|
|
854
854
|
warn "5.4 VPN — NetworkExtension/NEVPNManager usage detected (e.g. $(basename "$vpn_use")); VPN apps must be offered by an organization account, disclose data collection on-screen before use, and not sell/share data (5.4). Verify compliance."
|
|
855
855
|
fi
|
|
@@ -864,7 +864,7 @@ fi
|
|
|
864
864
|
# the reviewer-prep notes. Social-only logins are not gated here (too noisy).
|
|
865
865
|
set_rule "demo-account"
|
|
866
866
|
if [[ -n "$IOS_DIR" ]]; then
|
|
867
|
-
auth_signal=$(grep -rlE 'SecureField' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
867
|
+
auth_signal=$(grep -rlE 'SecureField' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
868
868
|
[[ -z "$auth_signal" ]] && auth_signal=$(find "$IOS_DIR" "${PRUNE[@]}" \( -name '*Login*View*.swift' -o -name '*SignIn*View*.swift' \) 2>/dev/null | head -1)
|
|
869
869
|
if [[ -n "$auth_signal" ]]; then
|
|
870
870
|
demo_present=""
|
|
@@ -891,7 +891,7 @@ fi
|
|
|
891
891
|
# JS-bundle OTA for React Native (CodePush) is allowed, so we do NOT flag it.
|
|
892
892
|
set_rule "executable-code-download"
|
|
893
893
|
if [[ -n "$IOS_DIR" ]]; then
|
|
894
|
-
hotcode=$(grep -rlE 'import JSPatch|JSPatch\.|[Jj][Ss][Pp]atch|import Rollout|Rollout\.|rollout\.io|DynamicCocoa|import SwiftPatch' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
894
|
+
hotcode=$(grep -rlE 'import JSPatch|JSPatch\.|[Jj][Ss][Pp]atch|import Rollout|Rollout\.|rollout\.io|DynamicCocoa|import SwiftPatch' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
895
895
|
if [[ -n "$hotcode" ]]; then
|
|
896
896
|
warn "2.5.2 Executable code — a hot-patch / remote-code framework was detected (e.g. $(basename "$hotcode")); apps may not download or run code that changes features (JSPatch/Rollout-style native hot-patching). Allowed JS-bundle OTA (e.g. React Native CodePush) is fine — verify this is not native hot-patching (2.5.2)"
|
|
897
897
|
fi
|
|
@@ -911,13 +911,13 @@ if [[ -f "$INFO_PLIST" && -n "$IOS_DIR" ]]; then
|
|
|
911
911
|
while IFS= read -r m; do
|
|
912
912
|
[[ -z "$m" ]] && continue
|
|
913
913
|
case "$m" in
|
|
914
|
-
location) grep -rqE 'CLLocationManager|CoreLocation' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null || unused="$unused location" ;;
|
|
915
|
-
audio) grep -rqE 'AVAudioSession|AVPlayer|AVAudioPlayer|AVQueuePlayer|AVAudioEngine|AVKit|VideoPlayer|AVPlayerViewController|MPMusicPlayerController|MPNowPlayingInfoCenter' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null || unused="$unused audio" ;;
|
|
916
|
-
voip) grep -rqE 'PushKit|PKPushRegistry|CallKit|CXProvider' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null || unused="$unused voip" ;;
|
|
917
|
-
fetch) grep -rqE 'BGAppRefreshTask|BGTaskScheduler|BackgroundTasks|setMinimumBackgroundFetchInterval|performFetchWithCompletionHandler' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null || unused="$unused fetch" ;;
|
|
918
|
-
processing) grep -rqE 'BGProcessingTask|BGTaskScheduler|BackgroundTasks' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null || unused="$unused processing" ;;
|
|
919
|
-
bluetooth-central|bluetooth-peripheral) grep -rqE 'CoreBluetooth|CBCentralManager|CBPeripheralManager' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null || unused="$unused $m" ;;
|
|
920
|
-
remote-notification) grep -rqE 'didReceiveRemoteNotification|UNUserNotificationCenter|registerForRemoteNotifications' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null || unused="$unused remote-notification" ;;
|
|
914
|
+
location) grep -rqE 'CLLocationManager|CoreLocation' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null || unused="$unused location" ;;
|
|
915
|
+
audio) grep -rqE 'AVAudioSession|AVPlayer|AVAudioPlayer|AVQueuePlayer|AVAudioEngine|AVKit|VideoPlayer|AVPlayerViewController|MPMusicPlayerController|MPNowPlayingInfoCenter' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null || unused="$unused audio" ;;
|
|
916
|
+
voip) grep -rqE 'PushKit|PKPushRegistry|CallKit|CXProvider' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null || unused="$unused voip" ;;
|
|
917
|
+
fetch) grep -rqE 'BGAppRefreshTask|BGTaskScheduler|BackgroundTasks|setMinimumBackgroundFetchInterval|performFetchWithCompletionHandler' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null || unused="$unused fetch" ;;
|
|
918
|
+
processing) grep -rqE 'BGProcessingTask|BGTaskScheduler|BackgroundTasks' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null || unused="$unused processing" ;;
|
|
919
|
+
bluetooth-central|bluetooth-peripheral) grep -rqE 'CoreBluetooth|CBCentralManager|CBPeripheralManager' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null || unused="$unused $m" ;;
|
|
920
|
+
remote-notification) grep -rqE 'didReceiveRemoteNotification|UNUserNotificationCenter|registerForRemoteNotifications' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null || unused="$unused remote-notification" ;;
|
|
921
921
|
esac
|
|
922
922
|
done <<< "$modes"
|
|
923
923
|
if [[ -n "$unused" ]]; then
|
|
@@ -933,7 +933,7 @@ fi
|
|
|
933
933
|
# ===================================================================
|
|
934
934
|
set_rule "crypto-wallet-mining"
|
|
935
935
|
if [[ -n "$IOS_DIR" ]]; then
|
|
936
|
-
crypto_sdk=$(grep -rlE 'import Web3|web3swift|Web3Swift|WalletConnect|TrustWalletCore|CoinbaseWalletSDK|SolanaSwift|CryptoMining|coinhive|MoneroMiner' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
936
|
+
crypto_sdk=$(grep -rlE 'import Web3|web3swift|Web3Swift|WalletConnect|TrustWalletCore|CoinbaseWalletSDK|SolanaSwift|CryptoMining|coinhive|MoneroMiner' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
937
937
|
if [[ -n "$crypto_sdk" ]]; then
|
|
938
938
|
warn "3.1.5(a) Cryptocurrency — a crypto wallet/exchange/mining signal was detected (e.g. $(basename "$crypto_sdk")); wallets & exchanges have entity and licensing requirements, and on-device mining is not permitted (3.1.5(a)). Verify eligibility."
|
|
939
939
|
fi
|
|
@@ -947,7 +947,7 @@ fi
|
|
|
947
947
|
# files. WARN (verify) — this is the most false-positive-prone of the batch.
|
|
948
948
|
set_rule "webview-wrapper"
|
|
949
949
|
if [[ -n "$IOS_DIR" ]]; then
|
|
950
|
-
if grep -rqE 'WKWebView' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
950
|
+
if grep -rqE 'WKWebView' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
951
951
|
swift_n=$(find "$IOS_DIR" "${PRUNE[@]}" -name '*.swift' 2>/dev/null | wc -l | tr -d ' ')
|
|
952
952
|
if (( swift_n > 0 && swift_n <= 4 )); then
|
|
953
953
|
warn "4.2.3 Minimum functionality — the app appears to be a WKWebView wrapper with only $swift_n Swift file(s); a thin wrapper around a website is rejected under 4.2.3. Add native value, or verify this is a real app rather than a repackaged site."
|
|
@@ -960,7 +960,7 @@ fi
|
|
|
960
960
|
# ===================================================================
|
|
961
961
|
set_rule "remote-desktop"
|
|
962
962
|
if [[ -n "$IOS_DIR" ]]; then
|
|
963
|
-
remote_desktop=$(grep -rlE 'import libvncclient|LibVNC|VNCClient|RDPSession|RDPKit|RemoteDesktopClient|import FreeRDP|JumpDesktop' "$IOS_DIR" "${SRC_INC[@]}" --include="*.m" 2>/dev/null | head -1)
|
|
963
|
+
remote_desktop=$(grep -rlE 'import libvncclient|LibVNC|VNCClient|RDPSession|RDPKit|RemoteDesktopClient|import FreeRDP|JumpDesktop' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" --include="*.m" 2>/dev/null | head -1)
|
|
964
964
|
if [[ -n "$remote_desktop" ]]; then
|
|
965
965
|
warn "4.2.7 Remote desktop — a remote-desktop/mirroring signal was detected (e.g. $(basename "$remote_desktop")); host-mirroring apps must only show/control the owner's host, display host content (not App Store content), and be free or use IAP (4.2.7). Verify."
|
|
966
966
|
fi
|
|
@@ -983,9 +983,9 @@ fi
|
|
|
983
983
|
# for an in-app deletion path. Deletion via a web page is missed → WARN, not FAIL.
|
|
984
984
|
set_rule "account-no-delete"
|
|
985
985
|
if [[ -n "$IOS_DIR" ]]; then
|
|
986
|
-
signup=$(grep -rlE 'createUser|signUp|signup|createAccount|registerUser|registerNewUser|Auth\.auth\(\)\.createUser' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
986
|
+
signup=$(grep -rlE 'createUser|signUp|signup|createAccount|registerUser|registerNewUser|Auth\.auth\(\)\.createUser' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
987
987
|
if [[ -n "$signup" ]]; then
|
|
988
|
-
if grep -rqiE 'delete.?account|account.?deletion|closeAccount|deleteUser|removeAccount|deleteMyAccount' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null; then
|
|
988
|
+
if grep -rqiE 'delete.?account|account.?deletion|closeAccount|deleteUser|removeAccount|deleteMyAccount' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null; then
|
|
989
989
|
pass "5.1.1(v) Account deletion — account creation present and an in-app account-deletion path was found"
|
|
990
990
|
else
|
|
991
991
|
warn "5.1.1(v) Account deletion — account creation was detected (e.g. $(basename "$signup")) but no in-app account-deletion path found; apps that support account creation must let users delete their account from within the app (5.1.1(v))"
|
|
@@ -1030,7 +1030,7 @@ fi
|
|
|
1030
1030
|
# ===================================================================
|
|
1031
1031
|
set_rule "mdm"
|
|
1032
1032
|
if [[ -n "$IOS_DIR" ]]; then
|
|
1033
|
-
mdm_sig=$(grep -rlE 'import DeviceManagement|MDMConfiguration|ManagedAppConfiguration|com\.apple\.mdm' "$IOS_DIR" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
1033
|
+
mdm_sig=$(grep -rlE 'import DeviceManagement|MDMConfiguration|ManagedAppConfiguration|com\.apple\.mdm' "$IOS_DIR" "${GREP_PRUNE[@]}" "${SRC_INC[@]}" 2>/dev/null | head -1)
|
|
1034
1034
|
[[ -z "$mdm_sig" ]] && mdm_sig=$(grep -rlE 'com\.apple\.configuration\.managed' --include='*.plist' "${GREP_PRUNE[@]}" . 2>/dev/null | pick_shallowest)
|
|
1035
1035
|
if [[ -n "$mdm_sig" ]]; then
|
|
1036
1036
|
warn "5.5 MDM — a Mobile Device Management signal was detected (e.g. $(basename "$mdm_sig")); MDM apps require a commercial enterprise/education entity, may request the MDM capability only for that purpose, and must not sell or use the data for other ends (5.5). Verify eligibility."
|