scan, privacy answers and credentials from a second game's first release (lessons 35-37): review-prompt needs rating context, not a bare "rate"; account signals skip import lines and module paths, config-only accounts say "from config"; accounts.inApp without deletion code stays a medium 5.1.1(v) finding; privacy-manifest --propose builds NSPrivacyCollectedDataTypes from appPrivacy.types and check warns on an empty list; appPrivacy.types is authoritative in the App Privacy sheet (unlisted = not collected), ad / support / AR types default to not collected without tracking or ad SDKs; Play advertising ID from SDKs, the AD_ID permission, app-ads.txt or the flag, never from a Device ID alone; scan.ignore[].evidence and warnings for entries that match nothing; empty credentials template, placeholder values count as missing (check, doctor)
This commit is contained in:
parent
86cd79dd45
commit
1366e7e231
34 changed files with 881 additions and 71 deletions
|
|
@ -9,7 +9,8 @@
|
|||
// ITMS-91064: NSPrivacyTracking and NSPrivacyTrackingDomains must agree in the app's manifest and in every SDK's (true
|
||||
// needs a non-empty list, a non-empty list needs true); a mismatch is a blocker.
|
||||
// --propose prints the app's PrivacyInfo.xcprivacy as storebound proposes it (declared + found API categories, the
|
||||
// collected types it has): NSPrivacyTracking true only with app-level tracking domains (--tracking-domains, else
|
||||
// collected types from appPrivacy.types of storebound.config.ts with linked, tracking and purposes, else the ones the
|
||||
// manifest has): NSPrivacyTracking true only with app-level tracking domains (--tracking-domains, else
|
||||
// appPrivacy.trackingDomains of storebound.config.ts when there is one), else false; tracking done by an SDK is
|
||||
// declared in the SDK's own manifest.
|
||||
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
||||
|
|
@ -19,7 +20,8 @@ import { log } from './lib/log';
|
|||
import { nativeIosPlugins } from './lib/node-packages';
|
||||
import { parsePlist, toXmlPlist, type PlistValue } from './lib/plist';
|
||||
import { manifestFromBytes } from './lib/privacy-files';
|
||||
import { evaluatePrivacyManifest, type Manifest, parseManifest, proposeAppManifest, scanSources, type SdkInfo } from './lib/privacy-manifest';
|
||||
import type { AppleDataTypeId, AppleTypeAnswer } from './lib/app-privacy';
|
||||
import { collectedFromAppPrivacy, evaluatePrivacyManifest, type Manifest, parseManifest, proposeAppManifest, scanSources, type SdkInfo } from './lib/privacy-manifest';
|
||||
|
||||
const SKIP = new Set(['.git', 'build', 'DerivedData', 'node_modules', '.build', 'xcuserdata']);
|
||||
const SRC = /\.(swift|m|mm|c|cc|cpp|h|hpp)$/;
|
||||
|
|
@ -54,11 +56,12 @@ function existingCollected(path: string | undefined): PlistValue[] {
|
|||
try { const v = parsePlist(readFileSync(path)) as Record<string, PlistValue>; return Array.isArray(v.NSPrivacyCollectedDataTypes) ? v.NSPrivacyCollectedDataTypes : []; } catch { return []; }
|
||||
}
|
||||
|
||||
async function configTrackingDomains(): Promise<string[]> {
|
||||
async function configAppPrivacy(): Promise<{ trackingDomains: string[]; types: Partial<Record<AppleDataTypeId, AppleTypeAnswer>> | undefined }> {
|
||||
try {
|
||||
const { loadConfig } = await import('./lib/config');
|
||||
return (await loadConfig()).config.appPrivacy?.trackingDomains ?? [];
|
||||
} catch { return []; }
|
||||
const ap = (await loadConfig()).config.appPrivacy;
|
||||
return { trackingDomains: ap?.trackingDomains ?? [], types: ap?.types };
|
||||
} catch { return { trackingDomains: [], types: undefined }; }
|
||||
}
|
||||
|
||||
export async function run(args: Args): Promise<void> {
|
||||
|
|
@ -110,8 +113,16 @@ export async function run(args: Args): Promise<void> {
|
|||
log('privacy', `${sources.length} source files, ${sdks.length} SDKs, ${manifests.length} app manifest(s)`);
|
||||
const sourceHits = scanSources(sources);
|
||||
if (args.has('propose')) {
|
||||
const domains = args.list('tracking-domains').length ? args.list('tracking-domains') : await configTrackingDomains();
|
||||
console.log(toXmlPlist(proposeAppManifest({ trackingDomains: domains, apis: appManifest?.apis, used: sourceHits.keys(), collected: existingCollected(manifests[0]) }) as PlistValue).trimEnd());
|
||||
const ap = await configAppPrivacy();
|
||||
const domains = args.list('tracking-domains').length ? args.list('tracking-domains') : ap.trackingDomains;
|
||||
const existing = existingCollected(manifests[0]);
|
||||
const types = ap.types && Object.keys(ap.types).length ? ap.types : undefined;
|
||||
console.log(toXmlPlist(proposeAppManifest({ trackingDomains: domains, apis: appManifest?.apis, used: sourceHits.keys(), collected: existing, types }) as PlistValue).trimEnd());
|
||||
if (types) {
|
||||
const c = collectedFromAppPrivacy(types, existing);
|
||||
log('privacy', `NSPrivacyCollectedDataTypes: ${c.entries.length} type(s) from appPrivacy.types (the App Privacy answers)`);
|
||||
for (const n of c.notes) log('privacy', n);
|
||||
} else if (!existing.length) log('privacy', 'NSPrivacyCollectedDataTypes empty: no appPrivacy.types in storebound.config.ts and none in the manifest (answer appPrivacy.types first: storebound console plan apple-app-privacy)');
|
||||
log('privacy', domains.length ? `NSPrivacyTracking true with ${domains.length} app-level tracking domain(s)` : 'NSPrivacyTracking false: no app-level tracking domains (tracking SDKs declare theirs in their own manifests; true with an empty list is ITMS-91064)');
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue