From 9d61ff0ec9b61dfee4a5c1f293aaace3342dd5d6 Mon Sep 17 00:00:00 2001 From: Maher Date: Sun, 20 Sep 2026 00:03:51 +0200 Subject: [PATCH] fix(privacy): stop matching `ads` inside ordinary identifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `(?i)(google.*ads|GADMobileAds|admob)` is unanchored, so `ads` matched inside `loadSdk`, `downloads`, `uploads` and `threads`. Any app that used a non-ad Google SDK and had one of those words on the same line got a CRITICAL §5.1.2 finding, which `--exit-code` turns into a failed build, and the suggested fix was to add an ATT prompt the app does not need. Require an ad-specific token instead. `\badmob` is anchored only at the start so it still covers `AdMobBanner` and `expo-ads-admob` while rejecting words that merely end in those letters. Fixes #30 --- internal/privacy/scanner.go | 2 +- internal/privacy/scanner_test.go | 78 ++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/internal/privacy/scanner.go b/internal/privacy/scanner.go index 0fc7744..96e1bc7 100644 --- a/internal/privacy/scanner.go +++ b/internal/privacy/scanner.go @@ -107,7 +107,7 @@ var trackingSDKPatterns = []struct { {regexp.MustCompile(`(?i)(mixpanel)`), "Mixpanel"}, {regexp.MustCompile(`(?i)(@segment/|analytics-react-native)`), "Segment"}, {regexp.MustCompile(`(?i)(branch\.io|react-native-branch)`), "Branch"}, - {regexp.MustCompile(`(?i)(google.*ads|GADMobileAds|admob)`), "Google Ads/AdMob"}, + {regexp.MustCompile(`(?i)(googlemobileads|google-mobile-ads|GADMobileAds|GADApplicationIdentifier|\badmob)`), "Google Ads/AdMob"}, {regexp.MustCompile(`(?i)(unity.*ads|UnityAds)`), "Unity Ads"}, {regexp.MustCompile(`(?i)(applovin|AppLovinSDK)`), "AppLovin"}, {regexp.MustCompile(`(?i)(ironSource|IronSource)`), "ironSource"}, diff --git a/internal/privacy/scanner_test.go b/internal/privacy/scanner_test.go index 1335846..478c204 100644 --- a/internal/privacy/scanner_test.go +++ b/internal/privacy/scanner_test.go @@ -39,3 +39,81 @@ func TestRequiredReasonDetectsRealCallSites(t *testing.T) { } } } + +// The Google Ads/AdMob pattern used to be `google.*ads`, which is unanchored and +// case-insensitive, so `ads` matched inside ordinary identifiers — `loadSdk`, +// `downloads`, `uploads`, `threads`. Any app that touched a non-ad Google SDK and +// happened to have one of those words on the same line got a CRITICAL §5.1.2 +// finding telling it to add an ATT prompt it does not need, and `--exit-code` +// failed the build. https://github.com/RevylAI/greenlight/issues/30 +func TestGoogleAdsPatternIgnoresOrdinaryIdentifiers(t *testing.T) { + clean := []struct { + name string + file string + content string + }{ + {"google sign-in with loadSdk", "googleSignIn.ts", + "GoogleSignin: Awaited>[\"GoogleSignin\"],\n"}, + {"google with downloads", "Downloads.swift", + "let googleDriveDownloads = try await drive.downloads()\n"}, + {"google with threads", "Threads.ts", + "const googleClient = build(); // threads are reused here\n"}, + {"google with uploads", "Uploads.ts", + "import { GoogleAuthProvider } from \"firebase/auth\"; const uploads = [];\n"}, + } + + for _, tc := range clean { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, tc.file, tc.content) + + res, err := Scan(dir) + if err != nil { + t.Fatalf("Scan: %v", err) + } + for _, sdk := range res.TrackingSDKs { + if sdk == "Google Ads/AdMob" { + t.Errorf("reported Google Ads/AdMob for an app with no ad SDK; content=%q", tc.content) + } + } + }) + } +} + +// Anchoring the pattern must not cost us any real AdMob integration. These are the +// forms that appear in the file types detectLang actually scans — Swift, Objective-C +// and JS/TS. Manifest files like Info.plist, Podfile and package.json are not scanned +// at all today, so GADApplicationIdentifier is kept in the pattern for the Swift and +// Objective-C call sites rather than for the plist key. +func TestGoogleAdsPatternStillDetectsRealIntegrations(t *testing.T) { + real := []struct { + name string + file string + content string + }{ + {"swift import", "Ads.swift", "import GoogleMobileAds\n"}, + {"swift api", "Ads.swift", "GADMobileAds.sharedInstance().start(completionHandler: nil)\n"}, + {"objc identifier", "Ads.m", "NSString *key = @\"GADApplicationIdentifier\";\n"}, + {"react native import", "ads.ts", "import mobileAds from 'react-native-google-mobile-ads';\n"}, + {"expo admob component", "Banner.tsx", "import { AdMobBanner } from 'expo-ads-admob';\n"}, + {"bare admob token", "ads.js", "const admob = require('admob');\n"}, + } + + for _, tc := range real { + t.Run(tc.name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, dir, tc.file, tc.content) + + res, err := Scan(dir) + if err != nil { + t.Fatalf("Scan: %v", err) + } + for _, sdk := range res.TrackingSDKs { + if sdk == "Google Ads/AdMob" { + return + } + } + t.Errorf("missed a real AdMob integration; content=%q TrackingSDKs=%v", tc.content, res.TrackingSDKs) + }) + } +}