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
|
|
@ -18,6 +18,7 @@ import { PLAY_APP_CATEGORIES, PLAY_GAME_CATEGORIES, playCategoryOf, profileOf }
|
|||
import type { PlanFormId, PlanOnlyFormId } from './console-answers';
|
||||
import { NEVER_PRESS, playAppList } from './console-forms';
|
||||
import type { ConsolePlan, PlanAnswer, PlanIds, PlanStatus, PlanStep } from './console-plan';
|
||||
import { AD_ID_PERMISSION, type ProjectSignals } from './project-signals';
|
||||
|
||||
const PLAY = 'https://play.google.com/console';
|
||||
|
||||
|
|
@ -236,13 +237,34 @@ const AD_ID_PURPOSES: Record<string, string> = {
|
|||
fraud: 'Fraud prevention, security, and compliance', personalization: 'Personalization', account: 'Account management',
|
||||
};
|
||||
|
||||
/**
|
||||
* "Does your app use advertising ID?" from real signals: console.appContent.advertisingId, ad / attribution SDKs (scan),
|
||||
* ads.appAdsTxt, the AD_ID permission in the source or merged AndroidManifest (or its tools:node="remove"). A Data
|
||||
* safety Device ID (PSL_DEVICE_ID) alone is not the advertising ID (a random install id is not). No signal: No
|
||||
* (certain) when the project was scanned; signals that disagree: ask.
|
||||
*/
|
||||
export function advertisingIdUse(config: ShipConfig, s: ProjectSignals | undefined): { v: boolean | null; src: string; note?: string } {
|
||||
const flag = config.console?.appContent?.advertisingId;
|
||||
const yes: string[] = [], no: string[] = [];
|
||||
if (flag === true) yes.push('console.appContent.advertisingId'); else if (flag === false) no.push('console.appContent.advertisingId');
|
||||
if (config.ads?.appAdsTxt?.length) yes.push('ads.appAdsTxt (ad SDKs read the advertising ID)');
|
||||
if (s) {
|
||||
if (s.adSdks.length || s.attributionSdks.length) yes.push(`ad / attribution SDKs: ${[...s.adSdks, ...s.attributionSdks].join(', ')}`);
|
||||
if (s.adIdRemoved) no.push(`AD_ID removed (tools:node="remove") at ${s.adIdRemoved}`);
|
||||
else if (s.adIdPermission) yes.push(`${AD_ID_PERMISSION} at ${s.adIdPermission}`);
|
||||
}
|
||||
if (yes.length && no.length) return { v: null, src: [...yes, ...no].join('; '), note: `The signals disagree (yes: ${yes.join('; ')}; no: ${no.join('; ')}). Decide and set console.appContent.advertisingId (an SDK that reads it needs the AD_ID permission in the merged manifest).` };
|
||||
if (yes.length) return { v: true, src: yes.join('; ') };
|
||||
if (no.length) return { v: false, src: no.join('; ') };
|
||||
if (s) return { v: false, src: `no ad / attribution SDK, no ${AD_ID_PERMISSION} in the manifests, no ads.appAdsTxt (a Device ID in dataSafety is not the advertising ID)` };
|
||||
return { v: null, src: 'console.appContent.advertisingId' };
|
||||
}
|
||||
|
||||
function advertisingId(config: ShipConfig, p: PlanIds): Part {
|
||||
const ac = config.console?.appContent, dev = config.dataSafety?.data?.PSL_DEVICE_ID;
|
||||
const uses: { v: boolean; src: string } | null = ac?.advertisingId !== undefined ? { v: ac.advertisingId, src: 'console.appContent.advertisingId' }
|
||||
: config.ads?.appAdsTxt?.length ? { v: true, src: 'ads.appAdsTxt (ad SDKs read the advertising ID)' }
|
||||
: dev ? { v: true, src: 'dataSafety.data.PSL_DEVICE_ID' } : null;
|
||||
const out: PlanAnswer[] = [uses ? ans('uses', 'Does your app use advertising ID?', uses.v, 'certain', uses.src, { control: 'radio', action: 'pick', label: uses.v ? 'Yes' : 'No', where: 'Advertising ID' })
|
||||
: ans('uses', 'Does your app use advertising ID?', null, 'ask-owner', 'console.appContent.advertisingId', { control: 'radio', where: 'Advertising ID', note: 'Yes when the app or any SDK (ads, analytics, attribution) reads it; set console.appContent.advertisingId.' })];
|
||||
const dev = config.dataSafety?.data?.PSL_DEVICE_ID;
|
||||
const uses = advertisingIdUse(config, p.signals);
|
||||
const out: PlanAnswer[] = [uses.v !== null ? ans('uses', 'Does your app use advertising ID?', uses.v, 'certain', uses.src, { control: 'radio', action: 'pick', label: uses.v ? 'Yes' : 'No', where: 'Advertising ID' })
|
||||
: ans('uses', 'Does your app use advertising ID?', null, 'ask-owner', uses.src, { control: 'radio', where: 'Advertising ID', note: uses.note ?? 'Yes when the app or any SDK (ads, analytics, attribution) reads it; set console.appContent.advertisingId.' })];
|
||||
if (uses?.v) {
|
||||
if (dev) {
|
||||
const purposes = new Set([...dev.collect, ...(dev.shared ? dev.share ?? [] : [])]);
|
||||
|
|
@ -255,7 +277,7 @@ function advertisingId(config: ShipConfig, p: PlanIds): Part {
|
|||
steps: steps([
|
||||
{ text: 'Open App content > Advertising ID (/app-content/ad-id-declaration).' },
|
||||
{ text: 'Pick Yes or No (material radio: JS click on the inner [role=radio]).', answers: ['uses'] },
|
||||
uses?.v && { text: 'Tick exactly the purposes of the sheet (material checkboxes: JS click on the inner [role=checkbox]; read aria-checked back).', answers: out.filter(a => a.id.startsWith('purpose')).map(a => a.id) },
|
||||
uses.v === true && { text: 'Tick exactly the purposes of the sheet (material checkboxes: JS click on the inner [role=checkbox]; read aria-checked back).', answers: out.filter(a => a.id.startsWith('purpose')).map(a => a.id) },
|
||||
{ text: DECLARATION_SAVE },
|
||||
{ text: 'Verify: read the page text and pipe it to `storebound console verify play-advertising-id --from-text -`.' },
|
||||
]),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue