storebound/cli/lib/scan/sdks.ts

216 lines
18 KiB
TypeScript

// The SDK table `storebound scan` detects: which third-party SDKs a mobile app or game ships, from its dependency files
// (CocoaPods, Swift Package Manager, Carthage, Gradle and version catalogs, npm, pub, Unity packages and External
// Dependency Manager XML, Godot addons, Cordova plugins), and what each one means for the store declarations.
//
// Maintained by hand. Every entry names the SDK's own data disclosure page (`docs`): that page is the source for the
// data types listed in `data`, which are the types the SDK collects by default as its vendor documents them for the
// Play Data safety form and Apple's App Privacy. The app's own configuration can change them (for example
// non-personalized ads only, analytics collection off), which is why scan proposes and the owner decides.
// Checked 2026-10-11. When a vendor changes its disclosure, update the entry and the date.
import type { PlayPurpose } from '../config';
export type SdkCategory = 'ads' | 'analytics' | 'attribution' | 'crash' | 'auth' | 'payments' | 'external-payments' | 'push';
export type Eco = 'cocoapods' | 'spm' | 'carthage' | 'gradle' | 'npm' | 'pub' | 'unity' | 'godot';
export interface SdkData {
/** Play Data safety type id (PSL_...). The App Privacy type follows from it (declarations.ts APPLE_PRIVACY). */
type: string;
collect: PlayPurpose[];
/** Sent to the SDK vendor for its own use (Play "shared"; Apple "third-party advertising" when for ads). */
share?: PlayPurpose[];
}
export interface SdkDef {
id: string;
name: string;
category: SdkCategory;
/** Can track across apps (reads IDFA / the Android advertising id, or links data with other companies' data):
* needs App Tracking Transparency on iOS before it does. */
tracking: boolean;
/** Login provider this SDK adds (4.8 Login Services). */
login?: 'google' | 'facebook' | 'apple' | 'firebase';
/** Matchers per ecosystem. cocoapods: pod name (or its prefix before "/"); spm: a fragment of the repository URL or
* the package identity; gradle: "group:artifact" (or "group:" for any artifact of the group); npm / pub: package
* name; unity: UPM package id or a folder under Assets/; godot: a folder under addons/. */
match: Partial<Record<Eco, string[]>>;
data: SdkData[];
/** Vendor's data disclosure / privacy page: the source for `data`. */
docs: string;
/** iOS: first version that ships Apple's privacy manifest (only where the vendor's release notes say so). */
manifestSince?: string;
/** Ad SDKs: on Google's Families Self-Certified Ads SDK list (support.google.com/googleplay/android-developer/answer/9859768,
* checked 2026-10-11), from `since` (the Android SDK version the list names). false = not on the list. */
families?: { certified: boolean; since?: string; note?: string };
/** Android: the SDK's own manifest declares com.google.android.gms.permission.AD_ID, so it reaches the merged
* manifest without the app declaring it (Play "Advertising ID" page names Google Mobile Ads as an example). */
mergesAdId?: boolean;
}
const ADS_DATA: SdkData[] = [
{ type: 'PSL_DEVICE_ID', collect: ['advertising', 'analytics', 'fraud'], share: ['advertising'] },
{ type: 'PSL_APPROX_LOCATION', collect: ['advertising', 'analytics', 'fraud'], share: ['advertising'] },
{ type: 'PSL_USER_INTERACTION', collect: ['advertising', 'analytics'], share: ['advertising'] },
{ type: 'PSL_PERFORMANCE_DIAGNOSTICS', collect: ['analytics'] },
];
const ANALYTICS_DATA: SdkData[] = [
{ type: 'PSL_DEVICE_ID', collect: ['analytics'] },
{ type: 'PSL_USER_INTERACTION', collect: ['analytics'] },
];
const CRASH_DATA: SdkData[] = [
{ type: 'PSL_CRASH_LOGS', collect: ['analytics'] },
{ type: 'PSL_PERFORMANCE_DIAGNOSTICS', collect: ['analytics'] },
];
const LOGIN_DATA: SdkData[] = [
{ type: 'PSL_NAME', collect: ['account'] },
{ type: 'PSL_EMAIL', collect: ['account'] },
{ type: 'PSL_USER_ACCOUNT', collect: ['account'] },
];
export const SDKS: SdkDef[] = [
// ---- ads (tracking-capable: they read the advertising id when the user allows it) ----
{ id: 'admob', name: 'Google AdMob', category: 'ads', tracking: true, data: ADS_DATA, manifestSince: '11.2.0', mergesAdId: true,
families: { certified: true, since: '19.0.0' },
docs: 'https://developers.google.com/admob/android/privacy/play-data-disclosure',
match: { cocoapods: ['Google-Mobile-Ads-SDK'], spm: ['swift-package-manager-google-mobile-ads'], gradle: ['com.google.android.gms:play-services-ads'],
npm: ['react-native-google-mobile-ads', '@capacitor-community/admob', 'admob-plus-cordova', 'expo-ads-admob'], pub: ['google_mobile_ads'],
unity: ['com.google.ads.mobile', 'GoogleMobileAds'], godot: ['admob'] } },
{ id: 'applovin', name: 'AppLovin MAX', category: 'ads', tracking: true, data: ADS_DATA,
families: { certified: false, note: 'AppLovin left the program; Families apps had to move off it by 2023-05-31' },
docs: 'https://support.axon.ai/en/max/android/overview/data-and-privacy',
match: { cocoapods: ['AppLovinSDK'], spm: ['applovin-ios-sdk'], gradle: ['com.applovin:applovin-sdk'], npm: ['react-native-applovin-max'],
pub: ['applovin_max'], unity: ['com.applovin.mediation.ads', 'MaxSdk'] } },
{ id: 'levelplay', name: 'Unity LevelPlay (ironSource)', category: 'ads', tracking: true, data: ADS_DATA,
families: { certified: true, since: '7.2.1', note: 'listed as ironSource; every mediated network must be on the list too' },
docs: 'https://developers.is.com/ironsource-mobile/general/ironsource-mobile-sdk-data-disclosure/',
match: { cocoapods: ['IronSourceSDK', 'IronSourcePod'], spm: ['LevelPlay-Swift-Package'], gradle: ['com.unity3d.ads-mediation:mediation-sdk', 'com.ironsource.sdk:mediationsdk'],
npm: ['ironsource-mediation', 'react-native-ironsource-mediation'], pub: ['unity_levelplay_mediation', 'ironsource_mediation'],
unity: ['com.unity.services.levelplay', 'IronSource', 'LevelPlay'] } },
{ id: 'unity-ads', name: 'Unity Ads', category: 'ads', tracking: true, data: ADS_DATA,
families: { certified: true, since: '4.0.1' },
docs: 'https://docs.unity.com/en-us/grow/ads/privacy/google-data-safety',
match: { cocoapods: ['UnityAds'], gradle: ['com.unity3d.ads:unity-ads'], pub: ['unity_ads_plugin'], unity: ['com.unity.ads'], npm: ['react-native-unity-ads'] } },
{ id: 'meta-an', name: 'Meta Audience Network', category: 'ads', tracking: true, data: ADS_DATA,
families: { certified: false, note: 'not on the list' },
docs: 'https://developers.facebook.com/docs/audience-network/',
match: { cocoapods: ['FBAudienceNetwork'], gradle: ['com.facebook.android:audience-network-sdk'], npm: ['react-native-fbads'],
pub: ['facebook_audience_network'], unity: ['AudienceNetwork'] } },
// ---- attribution (tracking by design) ----
{ id: 'appsflyer', name: 'AppsFlyer', category: 'attribution', tracking: true,
data: [{ type: 'PSL_DEVICE_ID', collect: ['analytics', 'advertising', 'fraud'], share: ['advertising'] }, { type: 'PSL_USER_INTERACTION', collect: ['analytics', 'advertising'] }],
docs: 'https://www.appsflyer.com/legal/services-privacy-policy/',
match: { cocoapods: ['AppsFlyerFramework'], spm: ['AppsFlyerFramework'], gradle: ['com.appsflyer:af-android-sdk'], npm: ['react-native-appsflyer', 'cordova-plugin-appsflyer-sdk'],
pub: ['appsflyer_sdk'], unity: ['appsflyer-unity-plugin', 'AppsFlyer'] } },
{ id: 'adjust', name: 'Adjust', category: 'attribution', tracking: true,
data: [{ type: 'PSL_DEVICE_ID', collect: ['analytics', 'advertising', 'fraud'], share: ['advertising'] }, { type: 'PSL_USER_INTERACTION', collect: ['analytics', 'advertising'] }],
docs: 'https://help.adjust.com/en/article/google-play-data-safety',
match: { cocoapods: ['Adjust'], spm: ['ios_sdk'], gradle: ['com.adjust.sdk:adjust-android'], npm: ['react-native-adjust'], pub: ['adjust_sdk'], unity: ['com.adjust.sdk', 'Adjust'] } },
// ---- analytics ----
{ id: 'firebase-analytics', name: 'Firebase Analytics', category: 'analytics', tracking: false, manifestSince: '10.22.0',
data: [{ type: 'PSL_DEVICE_ID', collect: ['analytics'] }, { type: 'PSL_USER_INTERACTION', collect: ['analytics'] }, { type: 'PSL_APPROX_LOCATION', collect: ['analytics'] }],
docs: 'https://firebase.google.com/docs/android/play-data-disclosure',
match: { cocoapods: ['FirebaseAnalytics', 'Firebase/Analytics'], spm: ['firebase-ios-sdk#FirebaseAnalytics'], gradle: ['com.google.firebase:firebase-analytics'],
npm: ['@react-native-firebase/analytics', '@capacitor-firebase/analytics', '@capacitor-community/firebase-analytics'], pub: ['firebase_analytics'],
unity: ['com.google.firebase.analytics', 'FirebaseAnalytics'] } },
{ id: 'gameanalytics', name: 'GameAnalytics', category: 'analytics', tracking: false, data: ANALYTICS_DATA,
docs: 'https://gameanalytics.com/privacy-policy',
match: { cocoapods: ['GA-SDK-IOS', 'GameAnalytics'], gradle: ['com.gameanalytics.sdk:gameanalytics-android'], unity: ['com.gameanalytics.sdk', 'GameAnalytics'],
pub: ['gameanalytics_sdk'], npm: ['react-native-gameanalytics'] } },
{ id: 'amplitude', name: 'Amplitude', category: 'analytics', tracking: false, data: ANALYTICS_DATA,
docs: 'https://amplitude.com/security-and-privacy',
match: { cocoapods: ['Amplitude', 'AmplitudeSwift'], spm: ['Amplitude-iOS', 'Amplitude-Swift'], gradle: ['com.amplitude:'], npm: ['@amplitude/analytics-react-native', '@amplitude/react-native'],
pub: ['amplitude_flutter'], unity: ['Amplitude'] } },
{ id: 'mixpanel', name: 'Mixpanel', category: 'analytics', tracking: false, data: ANALYTICS_DATA,
docs: 'https://docs.mixpanel.com/docs/privacy/protecting-user-data',
match: { cocoapods: ['Mixpanel', 'Mixpanel-swift'], spm: ['mixpanel-swift', 'mixpanel-iphone'], gradle: ['com.mixpanel.android:mixpanel-android'], npm: ['mixpanel-react-native'],
pub: ['mixpanel_flutter'], unity: ['com.mixpanel.unity', 'Mixpanel'] } },
// ---- crash reporting ----
{ id: 'crashlytics', name: 'Firebase Crashlytics', category: 'crash', tracking: false, data: CRASH_DATA, manifestSince: '10.22.0',
docs: 'https://firebase.google.com/docs/android/play-data-disclosure',
match: { cocoapods: ['FirebaseCrashlytics', 'Firebase/Crashlytics'], spm: ['firebase-ios-sdk#FirebaseCrashlytics'], gradle: ['com.google.firebase:firebase-crashlytics'],
npm: ['@react-native-firebase/crashlytics', '@capacitor-firebase/crashlytics'], pub: ['firebase_crashlytics'], unity: ['com.google.firebase.crashlytics', 'FirebaseCrashlytics'] } },
{ id: 'sentry', name: 'Sentry', category: 'crash', tracking: false, data: CRASH_DATA, manifestSince: '8.21.0',
docs: 'https://docs.sentry.io/platforms/android/data-management/data-collected/',
match: { cocoapods: ['Sentry'], spm: ['sentry-cocoa'], gradle: ['io.sentry:sentry-android'], npm: ['@sentry/react-native', '@sentry/capacitor'], pub: ['sentry_flutter'], unity: ['io.sentry.unity'] } },
// ---- sign-in ----
{ id: 'google-signin', name: 'Google Sign-In', category: 'auth', tracking: false, login: 'google', data: LOGIN_DATA, manifestSince: '7.1.0',
docs: 'https://developers.google.com/identity/sign-in/ios/start-integrating',
match: { cocoapods: ['GoogleSignIn'], spm: ['GoogleSignIn-iOS'], gradle: ['com.google.android.gms:play-services-auth', 'com.google.android.libraries.identity.googleid:googleid'],
npm: ['@react-native-google-signin/google-signin', '@codetrix-studio/capacitor-google-auth', 'expo-google-sign-in'], pub: ['google_sign_in'], unity: ['com.google.signin', 'GoogleSignIn'] } },
{ id: 'facebook-login', name: 'Facebook Login', category: 'auth', tracking: true, login: 'facebook', data: LOGIN_DATA, manifestSince: '17.0.0',
docs: 'https://developers.facebook.com/docs/facebook-login/',
match: { cocoapods: ['FBSDKLoginKit', 'FacebookLogin'], spm: ['facebook-ios-sdk'], gradle: ['com.facebook.android:facebook-login', 'com.facebook.android:facebook-android-sdk'],
npm: ['react-native-fbsdk-next', '@capacitor-community/facebook-login'], pub: ['flutter_facebook_auth'], unity: ['com.facebook.sdk', 'FacebookSDK'] } },
{ id: 'apple-signin', name: 'Sign in with Apple', category: 'auth', tracking: false, login: 'apple', data: [{ type: 'PSL_NAME', collect: ['account'] }, { type: 'PSL_EMAIL', collect: ['account'] }],
docs: 'https://developer.apple.com/sign-in-with-apple/',
match: { npm: ['expo-apple-authentication', '@invertase/react-native-apple-authentication', '@capacitor-community/apple-sign-in', 'cordova-plugin-sign-in-with-apple'],
pub: ['sign_in_with_apple', 'the_apple_sign_in'], unity: ['com.lupidan.apple-signin-unity', 'AppleAuth'] } },
{ id: 'firebase-auth', name: 'Firebase Authentication', category: 'auth', tracking: false, login: 'firebase', data: LOGIN_DATA, manifestSince: '10.22.0',
docs: 'https://firebase.google.com/docs/android/play-data-disclosure',
match: { cocoapods: ['FirebaseAuth', 'Firebase/Auth'], spm: ['firebase-ios-sdk#FirebaseAuth'], gradle: ['com.google.firebase:firebase-auth'],
npm: ['@react-native-firebase/auth', '@capacitor-firebase/authentication'], pub: ['firebase_auth'], unity: ['com.google.firebase.auth', 'FirebaseAuth'] } },
// ---- payments ----
{ id: 'play-billing', name: 'Google Play Billing', category: 'payments', tracking: false, data: [{ type: 'PSL_PURCHASE_HISTORY', collect: ['functionality'] }],
docs: 'https://developer.android.com/google/play/billing',
match: { gradle: ['com.android.billingclient:billing'] } },
{ id: 'revenuecat', name: 'RevenueCat', category: 'payments', tracking: false, data: [{ type: 'PSL_PURCHASE_HISTORY', collect: ['functionality', 'analytics'] }, { type: 'PSL_USER_ACCOUNT', collect: ['functionality'] }],
docs: 'https://www.revenuecat.com/docs/platform-resources/apple-platform-resources/apple-app-privacy',
match: { cocoapods: ['RevenueCat', 'Purchases'], spm: ['purchases-ios'], gradle: ['com.revenuecat.purchases:purchases'], npm: ['react-native-purchases', '@revenuecat/purchases-capacitor', 'cordova-plugin-purchases'],
pub: ['purchases_flutter'], unity: ['com.revenuecat.purchases-unity', 'RevenueCat'] } },
{ id: 'iap-wrapper', name: 'In-app purchase plugin', category: 'payments', tracking: false, data: [{ type: 'PSL_PURCHASE_HISTORY', collect: ['functionality'] }],
docs: 'https://developer.apple.com/storekit/',
match: { npm: ['react-native-iap', 'expo-iap', 'expo-in-app-purchases', 'cordova-plugin-purchase', '@capgo/native-purchases'], pub: ['in_app_purchase'], unity: ['com.unity.purchasing'] } },
// ---- payments outside the stores (fine for physical goods and services; digital goods need in-app purchase) ----
{ id: 'stripe', name: 'Stripe', category: 'external-payments', tracking: false, data: [{ type: 'PSL_CREDIT_DEBIT_BANK_ACCOUNT_NUMBER', collect: ['functionality', 'fraud'] }, { type: 'PSL_PURCHASE_HISTORY', collect: ['functionality'] }],
docs: 'https://stripe.com/privacy',
match: { cocoapods: ['Stripe', 'StripePaymentSheet'], spm: ['stripe-ios'], gradle: ['com.stripe:stripe-android'], npm: ['@stripe/stripe-react-native', '@capacitor-community/stripe'], pub: ['flutter_stripe'] } },
{ id: 'paypal', name: 'PayPal / Braintree', category: 'external-payments', tracking: false, data: [{ type: 'PSL_PURCHASE_HISTORY', collect: ['functionality'] }],
docs: 'https://www.paypal.com/us/legalhub/privacy-full',
match: { cocoapods: ['Braintree', 'PayPalCheckout', 'PayPal'], spm: ['braintree_ios', 'paypal-ios'], gradle: ['com.braintreepayments.api:', 'com.paypal.checkout:', 'com.paypal.android:'],
npm: ['react-native-paypal', 'react-native-braintree-dropin-ui'], pub: ['flutter_braintree', 'flutter_paypal'] } },
// ---- push ----
{ id: 'fcm', name: 'Firebase Cloud Messaging', category: 'push', tracking: false, data: [{ type: 'PSL_DEVICE_ID', collect: ['functionality'] }], manifestSince: '10.22.0',
docs: 'https://firebase.google.com/docs/android/play-data-disclosure',
match: { cocoapods: ['FirebaseMessaging', 'Firebase/Messaging'], spm: ['firebase-ios-sdk#FirebaseMessaging'], gradle: ['com.google.firebase:firebase-messaging'],
npm: ['@react-native-firebase/messaging', '@capacitor-firebase/messaging'], pub: ['firebase_messaging'], unity: ['com.google.firebase.messaging', 'FirebaseMessaging'] } },
{ id: 'onesignal', name: 'OneSignal', category: 'push', tracking: false, data: [{ type: 'PSL_USER_INTERACTION', collect: ['analytics'] }, { type: 'PSL_PURCHASE_HISTORY', collect: ['analytics'] }],
docs: 'https://documentation.onesignal.com/docs/google-play-data-safety-requirements',
match: { cocoapods: ['OneSignalXCFramework', 'OneSignal'], spm: ['OneSignal-XCFramework'], gradle: ['com.onesignal:OneSignal'], npm: ['react-native-onesignal', 'onesignal-cordova-plugin', 'onesignal-expo-plugin'],
pub: ['onesignal_flutter'], unity: ['com.onesignal.unity', 'OneSignal'] } },
{ id: 'push-plugin', name: 'Push notifications plugin', category: 'push', tracking: false, data: [],
docs: 'https://developer.apple.com/documentation/usernotifications',
match: { npm: ['@capacitor/push-notifications', 'expo-notifications', '@react-native-community/push-notification-ios', 'react-native-push-notification'], pub: ['flutter_local_notifications'], unity: ['com.unity.mobile.notifications'] } },
];
export interface DepRef { eco: Eco; name: string; version: string | null; file: string; line: number }
/** Does a dependency match an SDK? Pods match by name or "Name/Sub"; Gradle by "group:artifact" or a "group:" prefix;
* SPM by URL fragment (or "url#Product" for one product of a multi-product repository, matched on the product). */
export function matchesSdk(d: DepRef, s: SdkDef): boolean {
const list = s.match[d.eco] ?? [];
const n = d.name;
return list.some(m => {
switch (d.eco) {
case 'cocoapods': return n === m || n.startsWith(`${m}/`);
case 'gradle': return m.endsWith(':') ? n.startsWith(m) : n === m;
case 'spm': {
const [repo, product] = m.split('#') as [string, string | undefined];
if (product) return n.toLowerCase() === product.toLowerCase();
return n.toLowerCase().includes(repo.toLowerCase());
}
case 'unity': return n === m || n.toLowerCase() === m.toLowerCase();
default: return n === m;
}
});
}
/** "1.2.3" vs "1.10" numerically; non-numeric parts count as 0. Ranges like "^11.2.0" or "~> 11.0" use their numbers. */
export function versionAtLeast(v: string, min: string): boolean | null {
const nums = (s: string) => (/\d+(\.\d+)*/.exec(s)?.[0] ?? '').split('.').filter(Boolean).map(Number);
const a = nums(v), b = nums(min);
if (!a.length) return null;
for (let i = 0; i < Math.max(a.length, b.length); i++) {
const x = a[i] ?? 0, y = b[i] ?? 0;
if (x !== y) return x > y;
}
return true;
}