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:
CRTkafa 2026-10-13 05:39:09 +08:00
parent 86cd79dd45
commit 1366e7e231
34 changed files with 881 additions and 71 deletions

View file

@ -81,7 +81,7 @@ export function proposeDeclarations(p: ProjectFacts, c: ShipConfig | null, confi
detected.set(t, e);
};
if (accounts) {
markAccount('PSL_USER_ACCOUNT', signUp.length ? signUp : [cfgAt(/accounts\s*:/, 'accounts.enabled')], 'accounts');
markAccount('PSL_USER_ACCOUNT', signUp.length ? signUp : [cfgAt(/accounts\s*:/, 'from config (accounts.enabled)')], 'accounts');
for (const s of signUpCfg) for (const t of ACCOUNT_SIGN_UP[s]?.data ?? []) if (t !== 'PSL_USER_ACCOUNT') markAccount(t, [cfgAt(/signUp\s*:/, `accounts.signUp: ${s}`)], `accounts.signUp ${s}`);
const em = grepOwn(p, SIGNALS.email, 2);
if (em.hits.length && !em.built) markAccount('PSL_EMAIL', em.hits, 'email in the sources');
@ -147,8 +147,12 @@ export function proposeDeclarations(p: ProjectFacts, c: ShipConfig | null, confi
// appPrivacy.types: propose collected + purposes for detected types the config does not answer (Apple apps only)
const haveTypes = c?.appPrivacy?.types ?? {};
const typesAdded: Partial<Record<AppleDataTypeId, AppleTypeAnswer>> = {};
// appPrivacy.types is authoritative once it exists (a type it does not list is "not collected"), so a block this
// proposal writes also carries the types dataSafety already declares (kept) when the config has no types yet
const detectedNew = diff.some(d => d.change !== 'kept' && typeIdOf(d.apple.split(' / ')[1]!) && !haveTypes[typeIdOf(d.apple.split(' / ')[1]!)!]);
const withKept = detectedNew && !Object.keys(haveTypes).length;
if (c?.apple) for (const d of diff) {
if (d.change === 'kept') continue;
if (d.change === 'kept' && !withKept) continue;
const id = typeIdOf(d.apple.split(' / ')[1]!);
if (!id || id === 'OTHER_DATA_TYPES' || haveTypes[id]) continue;
const shared = d.after.shared ? d.after.share ?? [] : [];