appstore-precheck 1.1.1 → 1.5.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 +128 -0
- package/README.md +216 -52
- package/package.json +2 -2
- package/skills/appstore-precheck/SKILL.md +200 -81
- package/skills/appstore-precheck/evals/README.md +3 -2
- package/skills/appstore-precheck/evals/evals.json +6 -3
- package/skills/appstore-precheck/guidelines-baseline.json +19 -7
- package/skills/appstore-precheck/references/methodology.md +79 -7
- package/skills/appstore-precheck/references/pierre-deep-review.md +273 -0
- package/skills/appstore-precheck/scripts/scan.sh +325 -10
- package/skills/appstore-precheck/scripts/verdict.sh +3 -3
|
@@ -153,24 +153,28 @@ CHECK_FAMILY="$(cfg_bool '.optionalChecks.familyControls')"
|
|
|
153
153
|
echo "PASS: layout — ios='${IOS_DIR:-?}' metadata='${META_DIR:-?}' xcstrings='${XCSTRINGS:-?}' locales=${#LOCALES[@]}"
|
|
154
154
|
|
|
155
155
|
# ===================================================================
|
|
156
|
-
# §1 — 5.1.1
|
|
156
|
+
# §1 — 5.1.1 Privacy Manifest / Required Reason API parity
|
|
157
|
+
# Apple documents the Required Reason API rules under 5.1.1 (Data Collection and
|
|
158
|
+
# Storage) + the privacy-manifest developer docs; it is NOT roman sub-item (v).
|
|
159
|
+
# (v) "Account Sign-In" is a different rule — its account-deletion requirement is
|
|
160
|
+
# checked separately in §38.
|
|
157
161
|
# ===================================================================
|
|
158
162
|
check_required_reason_api() {
|
|
159
163
|
local cat="$1" pattern="$2" hits declared
|
|
160
164
|
hits=$(grep -rEl "$pattern" "$IOS_DIR" --include="*.swift" 2>/dev/null | head -3)
|
|
161
165
|
declared=$(grep -c "NSPrivacyAccessedAPICategory${cat}" "$PRIVACY_FILE" 2>/dev/null)
|
|
162
166
|
if [[ -n "$hits" && "${declared:-0}" -eq 0 ]]; then
|
|
163
|
-
fail "5.1.1
|
|
167
|
+
fail "5.1.1 Required Reason API — '$cat' used in code (e.g. $(echo "$hits" | head -1)) but not declared in PrivacyInfo.xcprivacy"
|
|
164
168
|
elif [[ -z "$hits" && "${declared:-0}" -gt 0 ]]; then
|
|
165
|
-
warn "5.1.1
|
|
169
|
+
warn "5.1.1 PrivacyInfo — '$cat' declared but no code usage grepped (may be a false positive, verify manually)"
|
|
166
170
|
elif [[ -n "$hits" && "${declared:-0}" -gt 0 ]]; then
|
|
167
|
-
pass "5.1.1
|
|
171
|
+
pass "5.1.1 Required Reason API — '$cat' parity OK"
|
|
168
172
|
fi
|
|
169
173
|
}
|
|
170
174
|
if [[ -z "$IOS_DIR" ]]; then
|
|
171
175
|
warn "layout — could not auto-detect iOS source dir; set .iosSourceDir in $CONFIG"
|
|
172
176
|
elif [[ -z "$PRIVACY_FILE" ]]; then
|
|
173
|
-
fail "5.1.1
|
|
177
|
+
fail "5.1.1 Required Reason API — PrivacyInfo.xcprivacy not found (required since May 2024 for apps using Required Reason APIs)"
|
|
174
178
|
else
|
|
175
179
|
check_required_reason_api "UserDefaults" 'UserDefaults|@AppStorage'
|
|
176
180
|
check_required_reason_api "FileTimestamp" 'attributesOfItem|creationDate|modificationDate|\.fileCreationDate|\.fileModificationDate'
|
|
@@ -383,13 +387,13 @@ else
|
|
|
383
387
|
fi
|
|
384
388
|
|
|
385
389
|
# ===================================================================
|
|
386
|
-
# §12 — 4.
|
|
390
|
+
# §12 — 4.2 Minimum functionality — navigation hubs
|
|
387
391
|
# ===================================================================
|
|
388
392
|
tab_count=$(grep -rcE 'TabView|NavigationStack|NavigationSplitView' "$IOS_DIR" --include="*.swift" 2>/dev/null | awk -F: '{sum+=$2} END {print sum+0}')
|
|
389
393
|
if (( tab_count < 1 )); then
|
|
390
|
-
warn "4.
|
|
394
|
+
warn "4.2 Minimum functionality — no TabView/NavigationStack found (heuristic, may be a false positive)"
|
|
391
395
|
else
|
|
392
|
-
pass "4.
|
|
396
|
+
pass "4.2 Minimum functionality — $tab_count navigation hub(s) found"
|
|
393
397
|
fi
|
|
394
398
|
|
|
395
399
|
# ===================================================================
|
|
@@ -473,8 +477,8 @@ if [[ -d "$META_DIR" ]]; then
|
|
|
473
477
|
[[ -s "$META_DIR/$loc/privacy_url.txt" ]] && privacy_found=1
|
|
474
478
|
done
|
|
475
479
|
if (( ${#LOCALES[@]} > 0 )); then
|
|
476
|
-
[[ -z "$support_found" ]] && warn "2.3 Support URL — no non-empty support_url.txt in any locale under fastlane metadata; Apple requires a working support URL"
|
|
477
|
-
[[ -z "$privacy_found" ]] && warn "2.3 Privacy URL — no non-empty privacy_url.txt in any locale under fastlane metadata; required for apps with accounts or in-app purchases"
|
|
480
|
+
[[ -z "$support_found" ]] && warn "2.3 Support URL — no non-empty support_url.txt in any locale under fastlane metadata; Apple requires a working support URL with developer contact info (1.5 / 2.3)"
|
|
481
|
+
[[ -z "$privacy_found" ]] && warn "2.3 Privacy URL — no non-empty privacy_url.txt in any locale under fastlane metadata; a privacy policy link is required for every app, and especially apps with accounts or in-app purchases (5.1.1(i))"
|
|
478
482
|
fi
|
|
479
483
|
url_ph=$(grep -rEnI 'example\.com|localhost|\bTODO\b|\bchangeme\b' "$META_DIR" --include='*_url.txt' 2>/dev/null | grep -v "^Binary file" | head -10)
|
|
480
484
|
if [[ -n "$url_ph" ]]; then
|
|
@@ -523,4 +527,315 @@ if [[ -d "$META_DIR" ]]; then
|
|
|
523
527
|
fi
|
|
524
528
|
fi
|
|
525
529
|
|
|
530
|
+
# ===================================================================
|
|
531
|
+
# §21 — 3.1.1 Third-party payment SDK for digital goods
|
|
532
|
+
# ===================================================================
|
|
533
|
+
# Apple requires in-app purchase to unlock digital content/functionality. A
|
|
534
|
+
# third-party payment SDK (Stripe, Braintree, PayPal, …) is legitimate for
|
|
535
|
+
# PHYSICAL goods/services but a frequent rejection when used to sell digital
|
|
536
|
+
# content. We can't tell digital from physical statically, so this is advisory.
|
|
537
|
+
if [[ -n "$IOS_DIR" ]]; then
|
|
538
|
+
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" --include="*.swift" 2>/dev/null | head -1)
|
|
539
|
+
if [[ -n "$payment_sdk" ]]; then
|
|
540
|
+
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."
|
|
541
|
+
fi
|
|
542
|
+
fi
|
|
543
|
+
|
|
544
|
+
# ===================================================================
|
|
545
|
+
# §22 — 1.2 User-generated content without moderation affordances
|
|
546
|
+
# ===================================================================
|
|
547
|
+
# Apps with UGC must provide: a content filter, a report mechanism, the ability
|
|
548
|
+
# to block abusive users, and published contact info. We detect a UGC signal and
|
|
549
|
+
# warn when no report/block/moderation affordance is found anywhere in the source.
|
|
550
|
+
if [[ -n "$IOS_DIR" ]]; then
|
|
551
|
+
ugc_signal=$(grep -rlE 'userGeneratedContent|\bUGC\b|StreamChat|MessageKit|SendbirdSDK|PubNub|postComment|submitComment|createPost|publishPost|uploadUserPhoto|uploadVideo' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
|
|
552
|
+
if [[ -n "$ugc_signal" ]]; then
|
|
553
|
+
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" --include="*.swift" 2>/dev/null; then
|
|
554
|
+
pass "1.2 UGC — user-generated content with report/block/moderation affordances present"
|
|
555
|
+
else
|
|
556
|
+
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)"
|
|
557
|
+
fi
|
|
558
|
+
fi
|
|
559
|
+
fi
|
|
560
|
+
|
|
561
|
+
# ===================================================================
|
|
562
|
+
# §23 — 1.6 App Transport Security disabled globally
|
|
563
|
+
# ===================================================================
|
|
564
|
+
# NSAllowsArbitraryLoads=true turns off ATS for the whole app, weakening
|
|
565
|
+
# data-in-transit security and inviting a 1.6 / data-security question at review.
|
|
566
|
+
if [[ -f "$INFO_PLIST" ]]; then
|
|
567
|
+
# Anchor to the exact key: `NSAllowsArbitraryLoads</key>` (followed by `<`), so the
|
|
568
|
+
# narrower scoped exceptions NSAllowsArbitraryLoadsInWebContent / ...ForMedia — which
|
|
569
|
+
# do NOT disable ATS app-wide and are the recommended alternative — don't false-fire.
|
|
570
|
+
if awk '/NSAllowsArbitraryLoads</{getline; if ($0 ~ /<true/) print "ON"}' "$INFO_PLIST" 2>/dev/null | grep -q ON; then
|
|
571
|
+
warn "1.6 App Transport Security — NSAllowsArbitraryLoads=true in Info.plist disables ATS app-wide; prefer per-domain exceptions, and expect a data-security justification request at review (1.6)"
|
|
572
|
+
fi
|
|
573
|
+
fi
|
|
574
|
+
|
|
575
|
+
# ===================================================================
|
|
576
|
+
# §24 — 4.9 Apple Pay recurring-payment disclosure
|
|
577
|
+
# ===================================================================
|
|
578
|
+
# Apps using Apple Pay for recurring payments must disclose the renewal term,
|
|
579
|
+
# what's provided, the charges, and how to cancel. Gated strictly on the Apple
|
|
580
|
+
# Pay recurring API (PKRecurringPaymentRequest) so it does NOT conflate StoreKit
|
|
581
|
+
# auto-renew copy or a one-time PassKit payment with recurring Apple Pay. We don't
|
|
582
|
+
# try to detect the disclosure text (too noisy) — we flag it for manual verify.
|
|
583
|
+
if [[ -n "$IOS_DIR" ]] && grep -rqE 'PKRecurringPaymentRequest' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
|
|
584
|
+
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)"
|
|
585
|
+
fi
|
|
586
|
+
|
|
587
|
+
# ===================================================================
|
|
588
|
+
# §25 — 5.6.1 Custom App Store review prompt
|
|
589
|
+
# ===================================================================
|
|
590
|
+
# Apple disallows custom review prompts and direct write-review links — apps must
|
|
591
|
+
# use the system SKStoreReviewController / requestReview API.
|
|
592
|
+
if [[ -n "$IOS_DIR" ]]; then
|
|
593
|
+
review_link=$(grep -rlE 'write-review|action=write-review|itms-apps[^"]*review' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
|
|
594
|
+
if [[ -n "$review_link" ]]; then
|
|
595
|
+
if grep -rqE 'requestReview|SKStoreReviewController|\.requestReview' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
|
|
596
|
+
pass "5.6.1 App reviews — uses the system requestReview API alongside the review link"
|
|
597
|
+
else
|
|
598
|
+
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)"
|
|
599
|
+
fi
|
|
600
|
+
fi
|
|
601
|
+
fi
|
|
602
|
+
|
|
603
|
+
# ===================================================================
|
|
604
|
+
# §26 — 2.3.1 Misleading marketing claims in metadata
|
|
605
|
+
# ===================================================================
|
|
606
|
+
# Marketing the app for things iOS apps can't actually do (virus/malware
|
|
607
|
+
# scanners, fake speed boosters) is a 2.3.1 removal vector.
|
|
608
|
+
if [[ -d "$META_DIR" ]]; then
|
|
609
|
+
mislead_re='virus scan|virus scanner|antivirus|anti-virus|malware (scan|remov|clean)|spyware remov|clean your (iphone|device)|speed booster|boost.*(speed|ram)|free money|guaranteed.*(win|prize)'
|
|
610
|
+
mislead_hits=$(grep -rEniI "$mislead_re" "$META_DIR" 2>/dev/null | grep -v "^Binary file" | head -10)
|
|
611
|
+
if [[ -n "$mislead_hits" ]]; then
|
|
612
|
+
warn "2.3.1 Misleading marketing — claims that often violate 2.3.1 (e.g. iOS virus/malware scanners, fake speed boosters) in metadata; verify the app truly delivers them or remove the claim:"
|
|
613
|
+
echo "$mislead_hits" | sed 's/^/ /'
|
|
614
|
+
fi
|
|
615
|
+
fi
|
|
616
|
+
|
|
617
|
+
# ===================================================================
|
|
618
|
+
# §27 — 2.3.8 "For Kids/Children" wording outside the Kids Category
|
|
619
|
+
# ===================================================================
|
|
620
|
+
# Terms implying a child audience in name/subtitle/keywords/description are
|
|
621
|
+
# reserved for the Kids Category (2.3.8 / 5.1.4).
|
|
622
|
+
if [[ -d "$META_DIR" ]]; then
|
|
623
|
+
kids_re='for kids|for children|for your (kid|child)|kids[[:space:]]?app|für kinder|para niños|pour enfants'
|
|
624
|
+
kids_hits=$(grep -rEniI "$kids_re" "$META_DIR" 2>/dev/null | grep -v "^Binary file" | head -10)
|
|
625
|
+
# Cross-gate with §39 (5.1.4): when an ads/analytics SDK is also linked, §39 fires the
|
|
626
|
+
# more specific Kids finding for the same wording signal — don't double-count it here
|
|
627
|
+
# (one root signal must not cost two WARNs against the 5-WARN YELLOW threshold).
|
|
628
|
+
if [[ -n "$kids_hits" && -z "${tracking_sdk:-}" && -z "${analytics_sdk:-}" ]]; then
|
|
629
|
+
warn "2.3.8 'For Kids/Children' wording — terms implying a child audience are reserved for the Kids Category (2.3.8); if not enrolled, remove them from name/subtitle/keywords/description:"
|
|
630
|
+
echo "$kids_hits" | sed 's/^/ /'
|
|
631
|
+
fi
|
|
632
|
+
fi
|
|
633
|
+
|
|
634
|
+
# ===================================================================
|
|
635
|
+
# §28 — 4.4.1 Keyboard extension requiring full access
|
|
636
|
+
# ===================================================================
|
|
637
|
+
# Keyboards must stay functional without full network access / "full access",
|
|
638
|
+
# and may only collect data to enhance the keyboard.
|
|
639
|
+
kb_plist=$(grep -rlE 'com\.apple\.keyboard-service' --include='Info.plist' "${GREP_PRUNE[@]}" . 2>/dev/null | pick_shallowest)
|
|
640
|
+
if [[ -n "$kb_plist" ]]; then
|
|
641
|
+
if grep -A1 'RequestsOpenAccess' "$kb_plist" 2>/dev/null | grep -q '<true'; then
|
|
642
|
+
warn "4.4.1 Keyboard extension — RequestsOpenAccess=true in $(basename "$kb_plist"); keyboards must remain functional without full access and may only collect data to enhance the keyboard (4.4.1). Justify full access in your review notes."
|
|
643
|
+
else
|
|
644
|
+
pass "4.4.1 Keyboard extension — present without requiring full access"
|
|
645
|
+
fi
|
|
646
|
+
fi
|
|
647
|
+
|
|
648
|
+
# ===================================================================
|
|
649
|
+
# §29 — 5.1.3 Health data with an iCloud sync path
|
|
650
|
+
# ===================================================================
|
|
651
|
+
# Personal health information must not be stored in iCloud, and HealthKit data
|
|
652
|
+
# may not be used for advertising/marketing.
|
|
653
|
+
if [[ -n "$IOS_DIR" ]] && grep -rqE 'import HealthKit|HKHealthStore' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
|
|
654
|
+
if grep -rqE 'import CloudKit|CKRecord|CKContainer|NSUbiquitousKeyValueStore|NSUbiquitousContainer' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
|
|
655
|
+
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."
|
|
656
|
+
else
|
|
657
|
+
pass "5.1.3 Health data — HealthKit used without an obvious iCloud sync path"
|
|
658
|
+
fi
|
|
659
|
+
fi
|
|
660
|
+
|
|
661
|
+
# ===================================================================
|
|
662
|
+
# §30 — 5.4 VPN apps (NetworkExtension)
|
|
663
|
+
# ===================================================================
|
|
664
|
+
# VPN apps must be offered by an organization account, declare data collection
|
|
665
|
+
# on-screen before use, and may not sell/share data.
|
|
666
|
+
if [[ -n "$IOS_DIR" ]]; then
|
|
667
|
+
vpn_use=$(grep -rlE 'NEVPNManager|NETunnelProviderManager|NEPacketTunnelProvider|NEVPNProtocol' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
|
|
668
|
+
if [[ -n "$vpn_use" ]]; then
|
|
669
|
+
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."
|
|
670
|
+
fi
|
|
671
|
+
fi
|
|
672
|
+
|
|
673
|
+
# ===================================================================
|
|
674
|
+
# §31 — 2.1 Demo account for a credential login
|
|
675
|
+
# ===================================================================
|
|
676
|
+
# Apps behind a login must give App Review working credentials (a demo account
|
|
677
|
+
# or notes). We fire only on a credential-login signal (a password field or a
|
|
678
|
+
# Login/SignIn view), then look for demo creds in fastlane review_information or
|
|
679
|
+
# the reviewer-prep notes. Social-only logins are not gated here (too noisy).
|
|
680
|
+
if [[ -n "$IOS_DIR" ]]; then
|
|
681
|
+
auth_signal=$(grep -rlE 'SecureField' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
|
|
682
|
+
[[ -z "$auth_signal" ]] && auth_signal=$(find "$IOS_DIR" "${PRUNE[@]}" \( -name '*Login*View*.swift' -o -name '*SignIn*View*.swift' \) 2>/dev/null | head -1)
|
|
683
|
+
if [[ -n "$auth_signal" ]]; then
|
|
684
|
+
demo_present=""
|
|
685
|
+
ri="$META_DIR/review_information"
|
|
686
|
+
if [[ -d "$ri" ]]; then
|
|
687
|
+
for f in demo_user.txt demo_password.txt notes.txt; do [[ -s "$ri/$f" ]] && demo_present=1; done
|
|
688
|
+
fi
|
|
689
|
+
if [[ -z "$demo_present" && -n "$REVIEW_PREP" && -f "$REVIEW_PREP" ]]; then
|
|
690
|
+
grep -qiE 'demo|review.*(account|credential)|test.*account' "$REVIEW_PREP" 2>/dev/null && demo_present=1
|
|
691
|
+
fi
|
|
692
|
+
if [[ -n "$demo_present" ]]; then
|
|
693
|
+
pass "2.1 Demo account — credential login present and reviewer demo credentials/notes found"
|
|
694
|
+
else
|
|
695
|
+
warn "2.1 Demo account — a credential login was detected (e.g. $(basename "$auth_signal")) but no demo account/credentials for App Review found (fastlane review_information or .reviewPrepNotes); apps behind a login must give reviewers working credentials (2.1)"
|
|
696
|
+
fi
|
|
697
|
+
fi
|
|
698
|
+
fi
|
|
699
|
+
|
|
700
|
+
# ===================================================================
|
|
701
|
+
# §32 — 2.5.2 Executable code download / hot-patch
|
|
702
|
+
# ===================================================================
|
|
703
|
+
# Apps must be self-contained; native hot-patching frameworks (JSPatch, Rollout,
|
|
704
|
+
# DynamicCocoa) download code that changes features and are a removal vector.
|
|
705
|
+
# JS-bundle OTA for React Native (CodePush) is allowed, so we do NOT flag it.
|
|
706
|
+
if [[ -n "$IOS_DIR" ]]; then
|
|
707
|
+
hotcode=$(grep -rlE 'import JSPatch|JSPatch\.|[Jj][Ss][Pp]atch|import Rollout|Rollout\.|rollout\.io|DynamicCocoa|import SwiftPatch' "$IOS_DIR" --include="*.swift" --include="*.m" --include="*.h" 2>/dev/null | head -1)
|
|
708
|
+
if [[ -n "$hotcode" ]]; then
|
|
709
|
+
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)"
|
|
710
|
+
fi
|
|
711
|
+
fi
|
|
712
|
+
|
|
713
|
+
# ===================================================================
|
|
714
|
+
# §33 — 2.5.4 Background modes declared but unused
|
|
715
|
+
# ===================================================================
|
|
716
|
+
# Declare only the UIBackgroundModes the app actually uses; a mode declared with
|
|
717
|
+
# no matching API is a frequent rejection. We parse the array and check each
|
|
718
|
+
# declared mode against its framework/API in Swift.
|
|
719
|
+
if [[ -f "$INFO_PLIST" && -n "$IOS_DIR" ]]; then
|
|
720
|
+
modes=$(awk '/<key>UIBackgroundModes<\/key>/{f=1;next} f&&/<\/array>/{f=0} f&&/<string>/{gsub(/.*<string>|<\/string>.*/,""); print}' "$INFO_PLIST" 2>/dev/null)
|
|
721
|
+
if [[ -n "$modes" ]]; then
|
|
722
|
+
unused=""
|
|
723
|
+
while IFS= read -r m; do
|
|
724
|
+
[[ -z "$m" ]] && continue
|
|
725
|
+
case "$m" in
|
|
726
|
+
location) grep -rqE 'CLLocationManager|CoreLocation' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused location" ;;
|
|
727
|
+
audio) grep -rqE 'AVAudioSession|AVPlayer|AVAudioPlayer|AVQueuePlayer|AVAudioEngine|AVKit|VideoPlayer|AVPlayerViewController|MPMusicPlayerController|MPNowPlayingInfoCenter' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused audio" ;;
|
|
728
|
+
voip) grep -rqE 'PushKit|PKPushRegistry|CallKit|CXProvider' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused voip" ;;
|
|
729
|
+
fetch) grep -rqE 'BGAppRefreshTask|BGTaskScheduler|BackgroundTasks|setMinimumBackgroundFetchInterval|performFetchWithCompletionHandler' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused fetch" ;;
|
|
730
|
+
processing) grep -rqE 'BGProcessingTask|BGTaskScheduler|BackgroundTasks' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused processing" ;;
|
|
731
|
+
bluetooth-central|bluetooth-peripheral) grep -rqE 'CoreBluetooth|CBCentralManager|CBPeripheralManager' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused $m" ;;
|
|
732
|
+
remote-notification) grep -rqE 'didReceiveRemoteNotification|UNUserNotificationCenter|registerForRemoteNotifications' "$IOS_DIR" --include="*.swift" 2>/dev/null || unused="$unused remote-notification" ;;
|
|
733
|
+
esac
|
|
734
|
+
done <<< "$modes"
|
|
735
|
+
if [[ -n "$unused" ]]; then
|
|
736
|
+
warn "2.5.4 Background modes —$unused declared in UIBackgroundModes but no matching API usage found in Swift; declare only the background modes the app actually uses (2.5.4)"
|
|
737
|
+
else
|
|
738
|
+
pass "2.5.4 Background modes — declared modes have matching API usage"
|
|
739
|
+
fi
|
|
740
|
+
fi
|
|
741
|
+
fi
|
|
742
|
+
|
|
743
|
+
# ===================================================================
|
|
744
|
+
# §34 — 3.1.5(a) Cryptocurrency wallet / exchange / mining
|
|
745
|
+
# ===================================================================
|
|
746
|
+
if [[ -n "$IOS_DIR" ]]; then
|
|
747
|
+
crypto_sdk=$(grep -rlE 'import Web3|web3swift|Web3Swift|WalletConnect|TrustWalletCore|CoinbaseWalletSDK|SolanaSwift|CryptoMining|coinhive|MoneroMiner' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
|
|
748
|
+
if [[ -n "$crypto_sdk" ]]; then
|
|
749
|
+
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."
|
|
750
|
+
fi
|
|
751
|
+
fi
|
|
752
|
+
|
|
753
|
+
# ===================================================================
|
|
754
|
+
# §35 — 4.2.3 Web-wrapper / thin app
|
|
755
|
+
# ===================================================================
|
|
756
|
+
# A thin WKWebView wrapper around a website is rejected under minimum
|
|
757
|
+
# functionality. Heuristic: WKWebView present in a project with very few Swift
|
|
758
|
+
# files. WARN (verify) — this is the most false-positive-prone of the batch.
|
|
759
|
+
if [[ -n "$IOS_DIR" ]]; then
|
|
760
|
+
if grep -rqE 'WKWebView' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
|
|
761
|
+
swift_n=$(find "$IOS_DIR" "${PRUNE[@]}" -name '*.swift' 2>/dev/null | wc -l | tr -d ' ')
|
|
762
|
+
if (( swift_n > 0 && swift_n <= 4 )); then
|
|
763
|
+
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."
|
|
764
|
+
fi
|
|
765
|
+
fi
|
|
766
|
+
fi
|
|
767
|
+
|
|
768
|
+
# ===================================================================
|
|
769
|
+
# §36 — 4.2.7 Remote desktop / host-mirroring
|
|
770
|
+
# ===================================================================
|
|
771
|
+
if [[ -n "$IOS_DIR" ]]; then
|
|
772
|
+
remote_desktop=$(grep -rlE 'import libvncclient|LibVNC|VNCClient|RDPSession|RDPKit|RemoteDesktopClient|import FreeRDP|JumpDesktop' "$IOS_DIR" --include="*.swift" --include="*.m" 2>/dev/null | head -1)
|
|
773
|
+
if [[ -n "$remote_desktop" ]]; then
|
|
774
|
+
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."
|
|
775
|
+
fi
|
|
776
|
+
fi
|
|
777
|
+
|
|
778
|
+
# ===================================================================
|
|
779
|
+
# §37 — 4.4.2 Safari extension / content blocker
|
|
780
|
+
# ===================================================================
|
|
781
|
+
safari_ext=$(grep -rlE 'com\.apple\.Safari\.(content-blocker|web-extension|extension)' --include='Info.plist' "${GREP_PRUNE[@]}" . 2>/dev/null | pick_shallowest)
|
|
782
|
+
if [[ -n "$safari_ext" ]]; then
|
|
783
|
+
warn "4.4.2 Safari extension — a Safari content-blocker / web extension was detected ($(basename "$safari_ext")); it must use the extension APIs as intended, do only what it declares, and not include hidden analytics/ads or track without consent (4.4.2). Verify."
|
|
784
|
+
fi
|
|
785
|
+
|
|
786
|
+
# ===================================================================
|
|
787
|
+
# §38 — 5.1.1(v) Account Sign-In: account creation without in-app deletion
|
|
788
|
+
# ===================================================================
|
|
789
|
+
# Apple 5.1.1(v): apps that support account creation must also let users delete
|
|
790
|
+
# their account from within the app. Detect account-creation signals, then look
|
|
791
|
+
# for an in-app deletion path. Deletion via a web page is missed → WARN, not FAIL.
|
|
792
|
+
if [[ -n "$IOS_DIR" ]]; then
|
|
793
|
+
signup=$(grep -rlE 'createUser|signUp|signup|createAccount|registerUser|registerNewUser|Auth\.auth\(\)\.createUser' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
|
|
794
|
+
if [[ -n "$signup" ]]; then
|
|
795
|
+
if grep -rqiE 'delete.?account|account.?deletion|closeAccount|deleteUser|removeAccount|deleteMyAccount' "$IOS_DIR" --include="*.swift" 2>/dev/null; then
|
|
796
|
+
pass "5.1.1(v) Account deletion — account creation present and an in-app account-deletion path was found"
|
|
797
|
+
else
|
|
798
|
+
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))"
|
|
799
|
+
fi
|
|
800
|
+
fi
|
|
801
|
+
fi
|
|
802
|
+
|
|
803
|
+
# ===================================================================
|
|
804
|
+
# §39 — 5.1.4 Kids audience with third-party ads / analytics
|
|
805
|
+
# ===================================================================
|
|
806
|
+
# Kids Category apps may not include third-party advertising or analytics and
|
|
807
|
+
# must include a parental gate. We fire when the metadata targets a child
|
|
808
|
+
# audience AND an ad/analytics SDK is linked. tracking_sdk/analytics_sdk come
|
|
809
|
+
# from §3/§19.
|
|
810
|
+
if [[ -d "$META_DIR" && -n "$IOS_DIR" ]]; then
|
|
811
|
+
if grep -rqEiI 'for kids|for children|kids[[:space:]]?app|für kinder|para niños|pour enfants' "$META_DIR" 2>/dev/null; then
|
|
812
|
+
if [[ -n "$tracking_sdk" || -n "$analytics_sdk" ]]; then
|
|
813
|
+
warn "5.1.4 Kids — the metadata targets a child audience and a third-party ads/analytics SDK is linked (e.g. $(basename "${tracking_sdk:-$analytics_sdk}")); Kids Category apps may not include third-party advertising or analytics and must offer a parental gate (5.1.4)"
|
|
814
|
+
fi
|
|
815
|
+
fi
|
|
816
|
+
fi
|
|
817
|
+
|
|
818
|
+
# ===================================================================
|
|
819
|
+
# §40 — 5.3.4 Real-money gambling
|
|
820
|
+
# ===================================================================
|
|
821
|
+
if [[ -d "$META_DIR" ]]; then
|
|
822
|
+
gamble_re='real[ -]?money|gambling|casino|sportsbook|sports[ -]?betting|place[ -].*bets|wager(ing)?|roulette.*real'
|
|
823
|
+
gamble_hits=$(grep -rEniI "$gamble_re" "$META_DIR" 2>/dev/null | grep -v "^Binary file" | head -10)
|
|
824
|
+
if [[ -n "$gamble_hits" ]]; then
|
|
825
|
+
warn "5.3.4 Gambling — real-money gaming language in metadata; real-money gambling/lotteries need the proper licenses, must be geo-restricted to permitted regions, and must be free on the App Store (5.3.4):"
|
|
826
|
+
echo "$gamble_hits" | sed 's/^/ /'
|
|
827
|
+
fi
|
|
828
|
+
fi
|
|
829
|
+
|
|
830
|
+
# ===================================================================
|
|
831
|
+
# §41 — 5.5 Mobile Device Management
|
|
832
|
+
# ===================================================================
|
|
833
|
+
if [[ -n "$IOS_DIR" ]]; then
|
|
834
|
+
mdm_sig=$(grep -rlE 'import DeviceManagement|MDMConfiguration|ManagedAppConfiguration|com\.apple\.mdm' "$IOS_DIR" --include="*.swift" 2>/dev/null | head -1)
|
|
835
|
+
[[ -z "$mdm_sig" ]] && mdm_sig=$(grep -rlE 'com\.apple\.configuration\.managed' --include='*.plist' "${GREP_PRUNE[@]}" . 2>/dev/null | pick_shallowest)
|
|
836
|
+
if [[ -n "$mdm_sig" ]]; then
|
|
837
|
+
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."
|
|
838
|
+
fi
|
|
839
|
+
fi
|
|
840
|
+
|
|
526
841
|
echo "---END-OF-SCAN---"
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
# the .precheck-pass token action — so the verdict is machine-testable, not just an
|
|
6
6
|
# agent judgement.
|
|
7
7
|
#
|
|
8
|
-
# GREEN 0 FAIL and <=
|
|
9
|
-
# YELLOW 0 FAIL and >=
|
|
8
|
+
# GREEN 0 FAIL and <=4 WARN -> token: write
|
|
9
|
+
# YELLOW 0 FAIL and >=5 WARN -> token: hold (needs explicit human confirmation)
|
|
10
10
|
# RED >=1 FAIL -> token: remove
|
|
11
11
|
#
|
|
12
12
|
# Usage:
|
|
@@ -49,7 +49,7 @@ passes=$(count '^PASS:')
|
|
|
49
49
|
|
|
50
50
|
if (( fails >= 1 )); then
|
|
51
51
|
verdict="RED"; token="remove"; code=1
|
|
52
|
-
elif (( warns >=
|
|
52
|
+
elif (( warns >= 5 )); then
|
|
53
53
|
verdict="YELLOW"; token="hold"; code=2
|
|
54
54
|
else
|
|
55
55
|
verdict="GREEN"; token="write"; code=0
|