storebound/cli/lib/scan/permissions.ts
CRT ae58948abd storebound scan: offline rejection-risk scan of the project itself
Detects the stack (native iOS / Android, React Native, Expo, Capacitor, Cordova,
Flutter, Unity, Godot) and the SDKs from a maintained table with versions,
file:line and each vendor's disclosure page, then reports App Store and Google
Play rejection risks with guideline, evidence, fix, severity and skill: 2.1(a)
demo account and placeholder text, 5.1.1(v) account deletion, 4.8 login
services, 3.1.1 restore and outside payments, 3.1.2 subscription info, 5.1.2(i)
ATT, purpose strings, privacy link, privacy manifest, 2.3.10, 2.3.7, 2.3.8, 4.2
web wrappers, 2.5.1, 2.5.5, ATS, export compliance, 1.3 kids ads, review
prompts, privacy labels vs SDKs; Play target SDK 36, delete-account URL, App
access, foreground services, exact alarms, QUERY_ALL_PACKAGES, SMS / call log,
com.example package name.

Proposes Data safety / App Privacy / ATT / privacy manifest answers (detected
file:line or ask owner) as a diff against the config; --write adds only the
detected ones. scan.ignore records owner-accepted findings. MCP tool scan
(read-only), validate includes open findings, desktop Checks tab shows them.
Fixtures for iOS, Android, Capacitor, Unity, Flutter, clean and teaser.
2026-10-11 23:20:55 +08:00

55 lines
5.4 KiB
TypeScript

// iOS privacy-sensitive APIs and the Info.plist purpose string each one needs (Apple: "Requesting access to protected
// resources"; a missing string makes the app crash on the first request and App Store Connect rejects the upload
// with ITMS-90683). `code` finds the API in app sources, `packages` the npm / pub / Unity packages that call it.
// Checked 2026-10-11 against developer.apple.com/documentation/bundleresources/information-property-list.
export interface PermissionDef {
/** Info.plist keys; any one of them satisfies the check (e.g. calendars before / since iOS 17). */
keys: string[];
what: string;
code: RegExp;
packages: string[];
}
export const IOS_PERMISSIONS: PermissionDef[] = [
{ keys: ['NSCameraUsageDescription'], what: 'camera',
code: /\bAVCaptureDevice\b|sourceType\s*=\s*\.camera|UIImagePickerController\.SourceType\.camera|\bWebCamTexture\b|\bVNDocumentCameraViewController\b/,
packages: ['expo-camera', 'react-native-vision-camera', 'react-native-camera', '@capacitor/camera', 'image_picker', 'camera', 'mobile_scanner', 'expo-image-picker', 'react-native-image-picker', 'cordova-plugin-camera', '@capacitor-mlkit/barcode-scanning'] },
{ keys: ['NSMicrophoneUsageDescription'], what: 'microphone',
code: /\bAVAudioRecorder\b|requestRecordPermission|\.record\b.*AVAudioSession|AVAudioApplication\.requestRecordPermission|\bMicrophone\.Start\b|\bAudioEffectRecord\b/,
packages: ['expo-av', 'react-native-audio-recorder-player', 'record', 'flutter_sound', 'cordova-plugin-media-capture', '@capacitor-community/voice-recorder'] },
{ keys: ['NSPhotoLibraryUsageDescription'], what: 'photo library',
code: /\bPHPhotoLibrary\.(requestAuthorization|shared)|\bPHAsset\.fetch|\bALAssetsLibrary\b/,
packages: ['expo-media-library', 'photo_manager', '@react-native-camera-roll/camera-roll', 'image_picker', '@capacitor/camera'] },
{ keys: ['NSPhotoLibraryAddUsageDescription', 'NSPhotoLibraryUsageDescription'], what: 'saving to the photo library',
code: /UIImageWriteToSavedPhotosAlbum|UISaveVideoAtPathToSavedPhotosAlbum|PHAssetChangeRequest|PHAssetCreationRequest/,
packages: ['gallery_saver', 'image_gallery_saver'] },
{ keys: ['NSLocationWhenInUseUsageDescription', 'NSLocationAlwaysAndWhenInUseUsageDescription'], what: 'location',
code: /\bCLLocationManager\b|requestWhenInUseAuthorization|requestAlwaysAuthorization|\bInput\.location\.Start\b|\bCLLocationUpdate\b/,
packages: ['expo-location', '@capacitor/geolocation', 'geolocator', 'location', '@react-native-community/geolocation', 'react-native-geolocation-service', 'cordova-plugin-geolocation'] },
{ keys: ['NSContactsUsageDescription'], what: 'contacts',
code: /\bCNContactStore\b|\bABAddressBook/,
packages: ['expo-contacts', 'react-native-contacts', 'flutter_contacts', 'contacts_service', '@capacitor-community/contacts', 'cordova-plugin-contacts'] },
{ keys: ['NSCalendarsFullAccessUsageDescription', 'NSCalendarsWriteOnlyAccessUsageDescription', 'NSCalendarsUsageDescription'], what: 'calendars',
code: /\bEKEventStore\b/, packages: ['expo-calendar', 'device_calendar', 'react-native-calendar-events'] },
{ keys: ['NSRemindersFullAccessUsageDescription', 'NSRemindersUsageDescription'], what: 'reminders', code: /requestFullAccessToReminders|entityType:\s*\.reminder|EKEntityType\.reminder/, packages: [] },
{ keys: ['NSBluetoothAlwaysUsageDescription'], what: 'Bluetooth',
code: /\bCBCentralManager\b|\bCBPeripheralManager\b/, packages: ['react-native-ble-plx', 'flutter_blue_plus', 'flutter_blue', '@capacitor-community/bluetooth-le', 'cordova-plugin-ble-central'] },
{ keys: ['NSFaceIDUsageDescription'], what: 'Face ID',
code: /deviceOwnerAuthenticationWithBiometrics|evaluatePolicy\(/, packages: ['expo-local-authentication', 'local_auth', 'react-native-biometrics', '@capgo/capacitor-native-biometric'] },
{ keys: ['NSMotionUsageDescription'], what: 'motion and fitness', code: /\bCMMotionActivityManager\b|\bCMPedometer\b|\bCMAltimeter\b/, packages: ['expo-sensors'] },
{ keys: ['NSSpeechRecognitionUsageDescription'], what: 'speech recognition', code: /\bSFSpeechRecognizer\b/, packages: ['@react-native-voice/voice', 'speech_to_text', 'expo-speech-recognition'] },
{ keys: ['NSHealthShareUsageDescription'], what: 'Health data', code: /\bHKHealthStore\b/, packages: ['react-native-health', 'health', '@kingstinct/react-native-healthkit'] },
{ keys: ['NSUserTrackingUsageDescription'], what: 'App Tracking Transparency',
code: /requestTrackingAuthorization|ATTrackingStatusBinding\.RequestAuthorizationTracking/,
packages: ['expo-tracking-transparency', 'react-native-tracking-transparency', 'app_tracking_transparency', '@capacitor-community/app-tracking-transparency', 'com.unity.ads.ios-support'] },
];
/** A purpose string that says nothing (empty, a placeholder, or a generic template Apple rejects under 5.1.1(ii)). */
export function vaguePurpose(s: string): 'empty' | 'vague' | null {
const t = s.trim();
if (!t) return 'empty';
if (/^(todo|tbd|fixme|placeholder|lorem|test|xxx+|\.+|-+|description|usage description|\$\([A-Z_]+\))$/i.test(t)) return 'empty';
// Expo / plugin defaults like "Allow $(PRODUCT_NAME) to access your camera" name no reason
if (t.length < 20 || /^(allow|let) \$\(PRODUCT_NAME\) to access your [a-z ]+\.?$/i.test(t) || /^(this app|we) (needs?|requires?|uses?) (access to )?(your )?[a-z ]+\.?$/i.test(t)) return 'vague';
return null;
}