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.1",
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": ["ios", "app-store", "fastlane", "agent-skill", "claude-code", "precheck"]
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.1
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."