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
cli/check.ts
18
cli/check.ts
|
|
@ -6,7 +6,8 @@
|
|||
// --json structured result ({ ok, items, warnings, hints, problems } or { findings, errors }), exit 1 on problems
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
||||
import { dirname, join, relative } from 'node:path';
|
||||
import { appPrivacyMismatches, type PrivacyMismatch, trackingDeclaredIssues, type TrackingSources } from './lib/app-privacy';
|
||||
import { appPrivacyMismatches, type PrivacyMismatch, trackingDeclaredIssues, type TrackingSources, typeIdOf } from './lib/app-privacy';
|
||||
import { privacyTypes } from './lib/console-answers';
|
||||
import { type BundleManifest, bundleTrackingIssues, ipaManifests, projectSdkManifests, sdkOfProjectPath } from './lib/privacy-files';
|
||||
import { trackingIssue } from './lib/privacy-manifest';
|
||||
import { type AsoFinding, auditAso } from './lib/aso';
|
||||
|
|
@ -17,7 +18,7 @@ import { playConsoleReminders } from './lib/console-plan';
|
|||
import { APPLE_IAP_LIMITS, appleNameIssues, len, parseAppStoreListing, parsePlayListing } from './lib/copy';
|
||||
import type { Ctx } from './lib/ctx';
|
||||
import { dataSafetyIssues, dataSafetyRows } from './lib/data-safety';
|
||||
import { reviewDemoAccount } from './lib/credentials';
|
||||
import { credentialStatus, reviewDemoAccount } from './lib/credentials';
|
||||
import { log, printJson, warn } from './lib/log';
|
||||
import { profileIssues } from './lib/profile';
|
||||
import { detectProject } from './lib/scan/project';
|
||||
|
|
@ -99,6 +100,11 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const apTypes = config.appPrivacy?.types;
|
||||
const hasTypes = !!apTypes && Object.keys(apTypes).length > 0;
|
||||
const facts = config.apple ? detectProject(ctx.loaded.root) : null;
|
||||
// appPrivacy.types is authoritative (an unlisted type is answered "not collected"): name dataSafety types it leaves out
|
||||
if (hasTypes && config.apple && config.dataSafety?.collectsData) {
|
||||
const left = [...privacyTypes(config)].filter(([name]) => { const id = typeIdOf(name); return id && id !== 'OTHER_DATA_TYPES' && !apTypes![id]; });
|
||||
if (left.length) note(`appPrivacy.types does not list ${left.map(([name, g]) => `${typeIdOf(name)} (dataSafety ${g.from.join(', ')})`).join(', ')}: App Privacy answers ${left.length === 1 ? 'it' : 'them'} "not collected". Add ${left.length === 1 ? 'it' : 'them'} to appPrivacy.types if the app or an SDK collects ${left.length === 1 ? 'it' : 'them'}`);
|
||||
}
|
||||
if (hasTypes && facts && !facts.ios) hints.push('appPrivacy.types: no iOS project found here, so it is not compared with Info.plist / PrivacyInfo.xcprivacy');
|
||||
if (facts?.ios) {
|
||||
const own = facts.privacyManifests.find(m => !/(^|\/)(Pods|node_modules|SourcePackages|Carthage|\.build)\//.test(m.path)) ?? null;
|
||||
|
|
@ -127,6 +133,14 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const typesTrack = hasTypes && Object.values(apTypes!).some(a => a?.tracking === true);
|
||||
if (config.appPrivacy?.tracking === true && !typesTrack && own) trackingDeclaredIssues('appPrivacy.tracking is true', own, tracking).forEach(say);
|
||||
}
|
||||
// template placeholders in the credentials ("YOUR_KEY_ID", "/absolute/path/to/...", "<...>"): missing, never a credential
|
||||
const ph = credentialStatus(ctx.loaded.root).filter(c => c.status === 'placeholder').map(c => c.key);
|
||||
if (ph.length) note(`credentials: ${ph.join(', ')} still hold template placeholders (env or .storebound/credentials.json): treated as missing; put the real value in or leave it empty`);
|
||||
// scan.ignore entries that accept no finding (a typo, a title that changed, a fixed issue) are never silent
|
||||
if (config.scan?.ignore?.length) {
|
||||
const { runScan } = await import('./lib/scan');
|
||||
try { for (const n of (await runScan(ctx.loaded.root, { config: ctx.loaded.path })).report.notes) if (n.startsWith('scan.ignore[')) note(n); } catch (e) { note(`scan.ignore not checked: ${(e as Error).message}`); }
|
||||
}
|
||||
if (config.dataSafety) tryDo('data safety', () => {
|
||||
const rows = dataSafetyRows(config.dataSafety!, config.accounts);
|
||||
// conditional required rows (e.g. the delete-account URL once accounts exist): Play answers 400 without them
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue