From 9171cb11292bf2a55f03eb2eb8981a683c06565c Mon Sep 17 00:00:00 2001 From: CRT Date: Thu, 15 Oct 2026 22:49:35 +0800 Subject: [PATCH] scan: the email detector skips server / config identities (a service account or OIDC email of a Pub/Sub push, SMTP / sender / admin addresses, values from the environment); a game's rtdn: { audience; email; token } in apps/server/src/config.ts was flagged as App Privacy Email Address --- cli/lib/scan/rules.ts | 10 +++++++++- tests/scan.test.ts | 5 ++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cli/lib/scan/rules.ts b/cli/lib/scan/rules.ts index 9b83b21..9d02a3d 100644 --- a/cli/lib/scan/rules.ts +++ b/cli/lib/scan/rules.ts @@ -100,6 +100,14 @@ export const PLAY_TARGET_SDK_SINCE = '2026-08-31'; export function codeLine(re: RegExp): RegExp { return new RegExp(`^(?!\\s*import[\\s{*'"])(?!.*\\b(?:require|import)\\s*\\(\\s*['"\`])(?!.*\\bfrom\\s*['"\`]).*?(?:${re.source})`, `${re.flags.replace('m', '')}m`); } +/** codeLine, minus server / config identities on the line: a service account or OIDC email of a Pub/Sub push, SMTP / + * sender / admin addresses and values read from the environment are the operator's, never data collected from users + * (a game: `rtdn: { audience: ...; email: ...; token: ... }` in apps/server/src/config.ts was flagged). */ +export function userLine(re: RegExp): RegExp { + const skip = '(?!.*(?:audience|[Oo][Ii][Dd][Cc]|[Pp]ub[Ss]ub|pubsub|[Ss]ervice[_ -]?[Aa]ccount|client_email|rtdn|RTDN|[Ss][Mm][Tt][Pp]|reply[_-]?[Tt]o|[Ss]ender|admin[_A-Za-z]*[Ee]mail|[Ee]mail[_A-Za-z]*[Ff]rom|process\\.env|Bun\\.env|import\\.meta\\.env|Deno\\.env|getenv|ENV\\[))'; + const c = codeLine(re); + return new RegExp(c.source.replace(/^\^/, `^${skip}`), c.flags); +} export const SIGNALS = { appleSignIn: /ASAuthorizationAppleIDProvider|SignInWithAppleButton|AppleAuthenticationButton|AppleAuthentication\.signInAsync|appleAuth\.performRequest|SignInWithApple|OAuthProvider\(\s*['"]apple\.com|AppleAuthProvider|signInWithApple|AppleAuthManager/, thirdPartyLogin: /GIDSignIn|GoogleSignin\.signIn|GoogleSignIn\(|GoogleAuthProvider|signInWithGoogle|FacebookAuthProvider|LoginManager\(\)\.logIn|LoginManager\.logInWithPermissions|signInWithFacebook|FacebookLogin|TwitterAuthProvider|GithubAuthProvider|OAuthProvider\(\s*['"](microsoft\.com|yahoo\.com|twitter\.com|github\.com)/, @@ -111,7 +119,7 @@ export const SIGNALS = { // real evidence that the app collects an email address: an email input or keyboard, an `email` property in a request // body / schema / type (not a quoted constant or label), an email validation, an email endpoint or an email sign-in // call. Accounts alone never mean email (a username with a device token has none). - email: /type\s*[:=]\s*["'{]*email["'}]|inputMode=["']email["']|keyboardType=["']?email-address|autoComplete=["']email|textContentType=["']?(emailAddress|\.emailAddress)|UITextContentType\.emailAddress|\.keyboardType\(\.emailAddress\)|keyboardType\s*=\s*\.emailAddress|TextInputType\.emailAddress|TYPE_TEXT_VARIATION_EMAIL_ADDRESS|KeyboardType\.Email\b|(?]Delete (my |your )?account|\brevokeToken\b|auth\.admin\.deleteUser|Hesab(ı|ımı) sil/i), restore: /restoreCompletedTransactions|AppStore\.sync\(|restorePurchases|restoreTransactions|RestoreTransactions|getAvailablePurchases|\brestorePurchase\b|\.restore\(\)|Restore Purchases|syncPurchases|Purchases\.shared\.restore|store\.restore\b|queryPurchasesAsync/i, subscriptionCode: /subscriptionPeriod|Product\.SubscriptionInfo|\.autoRenewable\b|entitlements\.active|SubscriptionStoreView|ProductType\.Subscription|ProductType\.SUBS|BillingClient\.ProductType\.SUBS/, diff --git a/tests/scan.test.ts b/tests/scan.test.ts index 44f6dbf..22b3e92 100644 --- a/tests/scan.test.ts +++ b/tests/scan.test.ts @@ -389,7 +389,10 @@ describe('monorepo: username accounts without email, plugins in apps/native/node 'const RE = /^[^\\s@]+@[^\\s@]+$/', "fetch('/api/auth/forgot-password')", 'auth.createUserWithEmailAndPassword(a, b)', 'Auth.auth().createUser(withEmail: e, password: p)', 'keyboardType="email-address"', 'request.requestedScopes = [.fullName, .email]']; const no = ["const ROUTES = { register: '/register', usernameCheck: '/username/check' }", "contactEmail: 'support@example.com'", "email: 'privacy@example.com'", - "labels = { email: 'E-mail' }", 'register(username)', "fetch('/api/v1/session')"]; + "labels = { email: 'E-mail' }", 'register(username)', "fetch('/api/v1/session')", + // server / config identities, not user data (a game apps/server/src/config.ts) + ' rtdn: { audience: string | null; email: string | null; token: string | null };', ' email: process.env.RTDN_EMAIL ?? null,', + ' pubsub: { email: cfg.serviceAccount },', ' smtp: { host, email: env.SMTP_USER },']; for (const s of yes) expect({ s, hit: SIGNALS.email.test(s) }).toEqual({ s, hit: true }); for (const s of no) expect({ s, hit: SIGNALS.email.test(s) }).toEqual({ s, hit: false }); });