diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e4f2582..2929204 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,26 +1,26 @@ { "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "android-reverse-engineering-skill", - "description": "Claude Code plugins for Android reverse engineering", + "description": "Claude Code plugins for Android reverse engineering, tracker detection, and ad SDK analysis", "owner": { "name": "Simone Avogadro" }, "metadata": { - "description": "Claude Code plugins for Android reverse engineering", - "version": "1.0.0" + "description": "Claude Code plugins for Android reverse engineering, tracker detection, and ad SDK analysis", + "version": "1.1.0" }, "plugins": [ { "name": "android-reverse-engineering", "source": "./plugins/android-reverse-engineering", - "description": "Decompile Android APK/JAR/AAR with jadx, trace call flows through libraries, and document extracted APIs.", - "version": "1.0.0", + "description": "Decompile Android APK/JAR/AAR with jadx, trace call flows, extract APIs, and analyze tracker/ad SDKs for privacy auditing.", + "version": "1.1.0", "author": { "name": "Simone Avogadro" }, "repository": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill", "license": "Apache-2.0", - "keywords": ["android", "reverse-engineering", "apk", "jadx", "decompile", "api-extraction"], + "keywords": ["android", "reverse-engineering", "apk", "jadx", "decompile", "api-extraction", "tracker-analysis", "ad-analysis", "privacy"], "category": "security" } ] diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c6fd214 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +A Claude Code Skill (plugin) for Android reverse engineering, API extraction, and privacy auditing. It provides three skills: +- **android-reverse-engineering**: 5-phase workflow — dependency verification, APK/XAPK/JAR/AAR decompilation (jadx and/or Fernflower), manifest/structure analysis, call flow tracing, and HTTP API endpoint extraction +- **tracker-analysis**: 4-phase workflow — detect analytics/tracker SDKs (Firebase, Adjust, AppsFlyer, Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry), analyze init/events/user ID/consent, report data exfiltration endpoints +- **ad-analysis**: 3-phase workflow — detect ad SDKs (AdMob, Unity, IronSource, AppLovin, Meta AN, Vungle, InMobi, Chartboost, Pangle, Mintegral), map ad formats and mediation setup, report privacy/consent + +## Repository Structure + +- `.claude-plugin/marketplace.json` — Marketplace catalog entry +- `plugins/android-reverse-engineering/.claude-plugin/plugin.json` — Plugin manifest +- `plugins/android-reverse-engineering/commands/decompile.md` — `/decompile` slash command +- `plugins/android-reverse-engineering/commands/find-trackers.md` — `/find-trackers` slash command +- `plugins/android-reverse-engineering/commands/find-ads.md` — `/find-ads` slash command +- `plugins/android-reverse-engineering/skills/android-reverse-engineering/` — Core RE skill (5-phase workflow, references, scripts) +- `plugins/android-reverse-engineering/skills/tracker-analysis/` — Tracker/analytics SDK detection skill (4-phase workflow, references, find-trackers.sh) +- `plugins/android-reverse-engineering/skills/ad-analysis/` — Advertising SDK detection skill (3-phase workflow, references, find-ads.sh) + +## Key Scripts + +Core scripts under `plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/`: + +```bash +# Check installed dependencies +bash scripts/check-deps.sh + +# Install a dependency (auto-detects OS/package manager) +bash scripts/install-dep.sh # e.g., jadx, vineflower, dex2jar + +# Decompile an APK/JAR/AAR/XAPK +bash scripts/decompile.sh [--engine jadx|fernflower|both] [--deobf] [--no-res] [-o outdir] + +# Search decompiled source for API calls +bash scripts/find-api-calls.sh [--retrofit|--okhttp|--volley|--urls|--auth|--all] +``` + +Tracker analysis script under `plugins/android-reverse-engineering/skills/tracker-analysis/scripts/`: + +```bash +# Search for tracker/analytics SDKs +bash find-trackers.sh [--firebase|--adjust|--appsflyer|--mixpanel|--amplitude|--segment|--braze|--clevertap|--flurry|--all] +``` + +Ad analysis script under `plugins/android-reverse-engineering/skills/ad-analysis/scripts/`: + +```bash +# Search for advertising SDKs +bash find-ads.sh [--admob|--unity|--ironsource|--applovin|--facebook|--formats|--mediation|--consent|--entrypoints|--all] +``` + +## Architecture + +**Plugin structure follows Claude Code skill conventions:** +- `skills//SKILL.md` defines the skill's workflow and capabilities +- `commands/.md` defines slash commands with YAML frontmatter +- `scripts/` contains executable bash utilities invoked by the skill +- `references/` contains in-depth technical documentation the skill consults + +**Script conventions:** +- Exit codes: 0 = success, 1 = error, 2 = manual action needed +- `check-deps.sh` outputs machine-readable `INSTALL_REQUIRED:` and `INSTALL_OPTIONAL:` lines +- `decompile.sh` handles XAPK by extracting the archive and decompiling each APK separately +- `${CLAUDE_PLUGIN_ROOT}` references the plugin root directory; `FERNFLOWER_JAR_PATH` for custom JAR location + +**Decompiler strategy:** +- jadx: default for APKs (fast, handles resources natively) +- Fernflower/Vineflower: better Java output for complex code, requires dex2jar for APK input +- `--engine both`: runs both in parallel and compares output quality + +## No Build/Test/Lint + +This is a documentation-and-scripts plugin with no compiled code, no test suite, and no linter configuration. Changes are validated by reading the markdown/bash and testing scripts manually against APK files. + +## Conventions + +- Line endings are LF (enforced via `.gitattributes` for WSL/Windows compatibility) +- Scripts target Bash 4.0+ and support Linux (apt/dnf/pacman) and macOS (Homebrew) +- Scripts fall back to user-local installs (`~/.local/`) when sudo is unavailable diff --git a/README.md b/README.md index 1764dbe..2e78300 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # Android Reverse Engineering & API Extraction — Claude Code skill -A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files and **extracts the HTTP APIs** used by the app — Retrofit endpoints, OkHttp calls, hardcoded URLs, authentication patterns — so you can document and reproduce them without the original source code. +A Claude Code skill that decompiles Android APK/XAPK/JAR/AAR files, **extracts HTTP APIs**, and **audits privacy** by detecting tracker/analytics and advertising SDKs — so you can document endpoints, understand data collection, and assess ad monetization without the original source code. ## What it does - **Decompiles** APK, XAPK, JAR, and AAR files using jadx and Fernflower/Vineflower (single engine or side-by-side comparison) - **Extracts and documents APIs**: Retrofit endpoints, OkHttp calls, hardcoded URLs, auth headers and tokens - **Traces call flows** from Activities/Fragments through ViewModels and repositories down to HTTP calls +- **Detects tracker/analytics SDKs**: Firebase Analytics, Adjust, AppsFlyer, Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry — with deep analysis of init, events, user identification, consent, and data exfiltration endpoints +- **Detects advertising SDKs**: AdMob, Unity Ads, IronSource/LevelPlay, AppLovin/MAX, Meta Audience Network, Vungle, InMobi, Chartboost, Pangle, Mintegral — with ad format mapping, mediation analysis, and consent framework detection - **Analyzes** app structure: manifest, packages, architecture patterns - **Handles obfuscated code**: strategies for navigating ProGuard/R8 output @@ -50,23 +52,36 @@ Then in Claude Code: ## Usage -### Slash command +### Slash commands ``` /decompile path/to/app.apk ``` +Runs the full workflow: dependency check, decompilation, and initial structure analysis. -This runs the full workflow: dependency check, decompilation, and initial structure analysis. +``` +/find-trackers path/to/decompiled/sources/ +``` +Detects analytics/tracker SDKs and produces a privacy report with init patterns, events, user identification, consent handling, and data endpoints. + +``` +/find-ads path/to/decompiled/sources/ +``` +Detects advertising SDKs and produces a report with ad formats, mediation setup, ad unit IDs, and consent framework analysis. ### Natural language -The skill activates on phrases like: +The skills activate on phrases like: - "Decompile this APK" - "Reverse engineer this Android app" - "Extract API endpoints from this app" - "Follow the call flow from LoginActivity" - "Analyze this AAR library" +- "Find trackers in this app" +- "What analytics SDKs does this app use?" +- "Detect ad networks in this app" +- "Show me the ad mediation setup" ### Manual scripts @@ -96,6 +111,16 @@ bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scri bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/ bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/ --retrofit bash plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/find-api-calls.sh output/sources/ --urls + +# Find tracker/analytics SDKs +bash plugins/android-reverse-engineering/skills/tracker-analysis/scripts/find-trackers.sh output/sources/ +bash plugins/android-reverse-engineering/skills/tracker-analysis/scripts/find-trackers.sh output/sources/ --firebase +bash plugins/android-reverse-engineering/skills/tracker-analysis/scripts/find-trackers.sh output/sources/ --adjust + +# Find advertising SDKs +bash plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh output/sources/ +bash plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh output/sources/ --admob +bash plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh output/sources/ --mediation ``` ## Repository Structure @@ -109,21 +134,39 @@ android-reverse-engineering-skill/ │ ├── .claude-plugin/ │ │ └── plugin.json # Plugin manifest │ ├── skills/ -│ │ └── android-reverse-engineering/ -│ │ ├── SKILL.md # Core workflow (5 phases) +│ │ ├── android-reverse-engineering/ # Core RE skill +│ │ │ ├── SKILL.md # 5-phase workflow +│ │ │ ├── references/ +│ │ │ │ ├── setup-guide.md +│ │ │ │ ├── jadx-usage.md +│ │ │ │ ├── fernflower-usage.md +│ │ │ │ ├── api-extraction-patterns.md +│ │ │ │ └── call-flow-analysis.md +│ │ │ └── scripts/ +│ │ │ ├── check-deps.sh +│ │ │ ├── install-dep.sh +│ │ │ ├── decompile.sh +│ │ │ └── find-api-calls.sh +│ │ ├── tracker-analysis/ # Tracker/analytics SDK detection +│ │ │ ├── SKILL.md # 4-phase workflow +│ │ │ ├── references/ +│ │ │ │ ├── tracker-sdk-catalog.md +│ │ │ │ ├── tracker-init-patterns.md +│ │ │ │ └── data-exfiltration-patterns.md +│ │ │ └── scripts/ +│ │ │ └── find-trackers.sh +│ │ └── ad-analysis/ # Advertising SDK detection +│ │ ├── SKILL.md # 3-phase workflow │ │ ├── references/ -│ │ │ ├── setup-guide.md -│ │ │ ├── jadx-usage.md -│ │ │ ├── fernflower-usage.md -│ │ │ ├── api-extraction-patterns.md -│ │ │ └── call-flow-analysis.md +│ │ │ ├── ad-sdk-catalog.md +│ │ │ ├── mediation-patterns.md +│ │ │ └── ad-format-patterns.md │ │ └── scripts/ -│ │ ├── check-deps.sh -│ │ ├── install-dep.sh -│ │ ├── decompile.sh -│ │ └── find-api-calls.sh +│ │ └── find-ads.sh │ └── commands/ -│ └── decompile.md # /decompile slash command +│ ├── decompile.md # /decompile slash command +│ ├── find-trackers.md # /find-trackers slash command +│ └── find-ads.md # /find-ads slash command ├── LICENSE └── README.md ``` diff --git a/plugins/android-reverse-engineering/.claude-plugin/plugin.json b/plugins/android-reverse-engineering/.claude-plugin/plugin.json index be386ee..3e19321 100644 --- a/plugins/android-reverse-engineering/.claude-plugin/plugin.json +++ b/plugins/android-reverse-engineering/.claude-plugin/plugin.json @@ -1,13 +1,13 @@ { "name": "android-reverse-engineering", "version": "1.0.0", - "description": "Decompile Android APK/JAR/AAR with jadx, trace call flows through libraries, and document extracted APIs.", + "description": "Decompile Android APK/JAR/AAR with jadx, trace call flows, extract APIs, and analyze tracker/ad SDKs for privacy auditing.", "author": { "name": "Simone Avogadro" }, "repository": "https://github.com/SimoneAvogadro/android-reverse-engineering-skill", "license": "Apache-2.0", - "keywords": ["android", "reverse-engineering", "apk", "jadx", "decompile", "api-extraction"], + "keywords": ["android", "reverse-engineering", "apk", "jadx", "decompile", "api-extraction", "tracker-analysis", "ad-analysis", "privacy"], "skills": "./skills/", "commands": "./commands/" } diff --git a/plugins/android-reverse-engineering/commands/find-ads.md b/plugins/android-reverse-engineering/commands/find-ads.md new file mode 100644 index 0000000..a44b409 --- /dev/null +++ b/plugins/android-reverse-engineering/commands/find-ads.md @@ -0,0 +1,81 @@ +--- +allowed-tools: Bash, Read, Glob, Grep, Write +description: Detect and analyze advertising SDKs in decompiled Android sources +user-invocable: true +argument-hint: +argument: path to decompiled sources directory (optional) +--- + +# /find-ads + +Detect and analyze advertising SDKs in a decompiled Android app. + +## Instructions + +You are starting the ad analysis workflow. Follow these steps: + +### Step 1: Get the source directory + +If the user provided a path as an argument, use that. Otherwise, ask the user for the path to the decompiled sources directory. + +If no decompiled sources exist yet, tell the user to run `/decompile` first on their APK/XAPK file. + +Verify the directory exists and contains `.java` or `.kt` files: + +```bash +find "$SOURCE_DIR" -name "*.java" -o -name "*.kt" | head -5 +``` + +### Step 2: Run broad detection + +Execute the ad detection script to sweep for all known ad SDKs: + +```bash +bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh "$SOURCE_DIR" --all +``` + +Parse the output — non-empty sections indicate a detected SDK or feature. + +### Step 3: Analyze detected SDKs and identify active entry points + +For each SDK found: + +1. **Identify the role** — is it the primary mediator or a mediated network? +2. **Extract ad unit/placement IDs** — find all ID strings +3. **Map ad formats** — which formats (banner, interstitial, rewarded, native, app-open) are implemented? +4. **Trace the load/show lifecycle** — where is each ad loaded and when is it shown? +5. **Check mediation setup** — if mediation is detected, identify all adapters and the waterfall/bidding strategy + +**Distinguish active vs passive SDKs** by running entry point detection: + +```bash +bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh "$SOURCE_DIR" --entrypoints +``` + +This searches for ad SDK calls **only in app code** (excluding library packages like `com/google`, `com/unity3d`, etc.). Compare these results with the full detection from Step 2: + +- SDKs that appear in `--entrypoints` output are **actively called** by the app +- SDKs detected in Step 2 but absent from `--entrypoints` are **passive dependencies** (mediation adapters only) +- Determine the **ad architecture**: Single mediator, Multiple direct, or Hybrid + +Use the reference documents in `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/` for SDK-specific patterns. + +### Step 4: Produce report + +Generate a structured report with: +- **Summary table**: ad network, role (mediator/mediated/active/passive), formats, ad unit IDs, consent +- **Ad architecture**: type (Single mediator / Multiple direct / Hybrid), active entry points with file:line, passive SDK list, ASCII diagram +- **Mediation setup**: primary mediator, strategy, mediated networks, adapter classes +- **Ad formats by placement**: for each format — SDK, ad unit, load/show location, trigger +- **Ad unit ID table**: all discovered IDs with format, SDK, and source location +- **Privacy & consent**: UMP, TCF, COPPA, AD_ID permission, per-network consent + +Refer to `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/SKILL.md` for the full report format. + +### Step 5: Offer next steps + +Tell the user what they can do next: +- **Deep-dive a specific ad network**: "I can trace the full integration for AdMob" +- **Analyze mediation waterfall**: "I can map the exact network priority order" +- **Check trackers**: "Run `/find-trackers` to analyze analytics/tracker SDKs too" +- **Export report**: "I can save this report as a markdown file" diff --git a/plugins/android-reverse-engineering/commands/find-trackers.md b/plugins/android-reverse-engineering/commands/find-trackers.md new file mode 100644 index 0000000..c774503 --- /dev/null +++ b/plugins/android-reverse-engineering/commands/find-trackers.md @@ -0,0 +1,66 @@ +--- +allowed-tools: Bash, Read, Glob, Grep, Write +description: Detect and analyze tracker/analytics SDKs in decompiled Android sources +user-invocable: true +argument-hint: +argument: path to decompiled sources directory (optional) +--- + +# /find-trackers + +Detect and analyze analytics/tracker SDKs in a decompiled Android app. + +## Instructions + +You are starting the tracker analysis workflow. Follow these steps: + +### Step 1: Get the source directory + +If the user provided a path as an argument, use that. Otherwise, ask the user for the path to the decompiled sources directory. + +If no decompiled sources exist yet, tell the user to run `/decompile` first on their APK/XAPK file. + +Verify the directory exists and contains `.java` or `.kt` files: + +```bash +find "$SOURCE_DIR" -name "*.java" -o -name "*.kt" | head -5 +``` + +### Step 2: Run broad detection + +Execute the tracker detection script to sweep for all known tracker SDKs: + +```bash +bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh "$SOURCE_DIR" --all +``` + +Parse the output — non-empty sections indicate a detected SDK. + +### Step 3: Analyze each detected SDK + +For each SDK found in the sweep, perform deep analysis: + +1. **Read the initialization code** — find where the SDK is set up, extract API keys/app IDs +2. **Catalog tracked events** — list all event names and their parameters +3. **Check user identification** — find setUserId/identify calls, what user data is collected +4. **Verify consent handling** — check if the app gates analytics on user consent +5. **Identify data endpoints** — known domains and any custom relay/proxy + +Use the reference documents in `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/` for SDK-specific patterns. + +### Step 4: Produce report + +Generate a structured report with: +- **Summary table**: SDK name, init location, event count, user ID method, consent status +- **Per-SDK detail**: initialization, events, user data, consent, endpoints +- **Privacy summary**: total SDKs, consent coverage, user data shared, endpoint domains + +Refer to `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/SKILL.md` for the full report format. + +### Step 5: Offer next steps + +Tell the user what they can do next: +- **Deep-dive a specific SDK**: "I can trace the full data flow for Firebase Analytics" +- **Check data exfiltration**: "I can search for proxy/relay patterns and custom endpoints" +- **Analyze ad SDKs**: "Run `/find-ads` to analyze advertising SDKs too" +- **Export report**: "I can save this report as a markdown file" diff --git a/plugins/android-reverse-engineering/skills/ad-analysis/SKILL.md b/plugins/android-reverse-engineering/skills/ad-analysis/SKILL.md new file mode 100644 index 0000000..40d92b0 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/ad-analysis/SKILL.md @@ -0,0 +1,177 @@ +--- +description: Detect and analyze advertising SDKs in decompiled Android apps. Identifies AdMob, Unity Ads, IronSource/LevelPlay, AppLovin/MAX, Meta Audience Network, Vungle, InMobi, Chartboost, Pangle, Mintegral. Extracts ad formats (banner, interstitial, rewarded, native), mediation setup, ad unit IDs, and consent/privacy frameworks. +trigger: find ads|ad analysis|advertising SDK|detect ads|ad network|AdMob|Unity Ads|IronSource|AppLovin|Facebook ads|Meta Audience Network|Vungle|InMobi|Chartboost|Pangle|Mintegral|mediation|ad format|rewarded ad|interstitial ad|banner ad +--- + +# Ad Analysis + +Detect and analyze advertising SDKs embedded in decompiled Android applications. Produces a structured report covering ad network identification, ad formats used, mediation configuration, ad unit/placement IDs, and consent/privacy framework compliance. + +## Prerequisites + +This skill operates on **already decompiled** source code. If the app has not been decompiled yet, use the `android-reverse-engineering` skill first: + +```bash +bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh +``` + +The decompiled sources directory (typically `/sources/`) is the input for this skill. + +## Workflow + +### Phase 1: Verify Decompiled Sources + +Confirm that the decompiled source directory exists and contains Java/Kotlin files. + +**Action**: Check the provided directory path. + +- If the user provides a path, verify it exists and contains `.java` or `.kt` files +- If no decompiled sources are available, instruct the user to run `/decompile` first +- Look for `AndroidManifest.xml` — it contains ad-related Activities, Services, meta-data (APPLICATION_ID, sdk keys), and the AD_ID permission + +### Phase 2: Detect SDKs, Formats, and Mediation + +Run the ad detection script to identify all ad SDKs, formats, and mediation setup. + +**Action**: Execute the detection script. + +```bash +bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh --all +``` + +For targeted searches: +```bash +# Single SDK +bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh --admob +bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh --unity + +# Specific aspects +bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh --formats # cross-SDK format detection +bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh --mediation # mediation adapter detection +bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh --consent # consent/privacy frameworks +``` + +Parse the output to identify: +- Which ad SDKs are present +- Which ad formats are used (banner, interstitial, rewarded, native, app-open) +- Whether mediation is set up and which SDK is the mediator +- Consent framework presence + +For each detected SDK, read the surrounding code to extract: + +1. **Initialization**: Where and how is the SDK initialized? Extract app ID/SDK key +2. **Ad formats**: Which formats are implemented? Extract ad unit/placement IDs +3. **Mediation**: Is this SDK a mediator or a mediated network? Which adapters are present? + - See `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/mediation-patterns.md` +4. **Ad format details**: For each format, understand the load/show lifecycle + - See `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/ad-format-patterns.md` +5. **Consent**: Does the app implement UMP, TCF, or SDK-specific consent? + +### Phase 2b: Identify Active Entry Points + +Distinguish which ad SDKs the app calls directly vs which are only present as passive mediation dependencies. + +**Action**: Run the entry point detection. + +```bash +bash ${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/scripts/find-ads.sh --entrypoints +``` + +This searches for ad SDK init/load/show calls **only in app code**, excluding known library packages (`com/google`, `com/unity3d`, `com/ironsource`, etc.). The results reveal which SDKs the app actively uses. + +**Analysis**: + +1. Compare the entry point results with the full SDK detection from Phase 2 +2. For each detected SDK, classify it as: + - **Active**: the app code calls its init/load/show methods directly + - **Passive**: its code is present only inside library packages (mediation adapters) +3. Determine the ad architecture: + - **Single mediator**: the app calls only 1 ad SDK (e.g., AdMob `MobileAds.initialize()`), all others are passive adapter dependencies + - **Multiple direct**: the app calls 2+ ad SDKs directly (manual ad network brokerage) + - **Hybrid**: the app calls a mediator for some formats and additional SDKs directly for others + +See `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/mediation-patterns.md` — "Active vs Passive SDKs" section for detailed patterns. + +### Phase 3: Produce Report + +Generate a structured ad analysis report. + +**Report format:** + +```markdown +# Ad Analysis Report — + +## Summary + +| Ad Network | Role | Formats | Ad Unit IDs | Consent | +|---|---|---|---|---| +| AdMob | Mediator | Banner, Interstitial, Rewarded | ca-app-pub-XXX/YYY, ... | UMP ✓ | +| Unity Ads | Mediated | Interstitial, Rewarded | (via mediation) | — | +| IronSource | Mediated | Rewarded | (via mediation) | — | + +## Ad Architecture + +- **Type**: Single mediator / Multiple direct / Hybrid +- **Active entry points** (called from app code): + - `MobileAds.initialize()` — `com/example/app/AdManager.java:28` + - `InterstitialAd.load()` — `com/example/app/GameActivity.java:112` +- **Passive SDKs** (present only as mediation adapter dependencies): + - Unity Ads, IronSource, AppLovin, Meta AN — invoked only via `com.google.ads.mediation.*` adapters + +``` +App code ──→ AdMob (mediator) + ├──→ Google Ads (direct) + ├──→ UnityAdapter ──→ Unity Ads SDK [passive] + ├──→ IronSourceAdapter ──→ IS SDK [passive] + └──→ AppLovinAdapter ──→ AL SDK [passive] +``` + +## Mediation Setup + +- **Primary mediator**: AdMob +- **Strategy**: Hybrid (bidding + waterfall) +- **Mediated networks**: Unity Ads, IronSource, AppLovin, Meta AN +- **Adapter classes found**: list + +## Ad Formats by Placement + +### Banner +- **SDK**: AdMob (direct) +- **Size**: BANNER (320×50) +- **Ad unit**: `ca-app-pub-XXXXX/YYYYY` +- **Location**: `MainActivity.java:45` — bottom of screen +- **Auto-refresh**: yes + +### Interstitial +- **SDK**: AdMob (mediated to Unity, IronSource) +- **Ad unit**: `ca-app-pub-XXXXX/ZZZZZ` +- **Load location**: `GameActivity.java:112` +- **Show trigger**: level complete (`onLevelComplete()`) + +### Rewarded +- **SDK**: AdMob (mediated) +- **Ad unit**: `ca-app-pub-XXXXX/WWWWW` +- **Reward**: 50 coins (`onUserEarnedReward`) +- **Show trigger**: "Watch ad for coins" button + +## Ad Unit / Placement IDs + +| ID | Format | SDK | Location | +|---|---|---|---| +| `ca-app-pub-XXXXX/YYYYY` | Banner | AdMob | MainActivity.java:45 | +| `ca-app-pub-XXXXX/ZZZZZ` | Interstitial | AdMob | GameActivity.java:112 | + +## Privacy & Consent + +- **UMP / Funding Choices**: ✓ implemented in `ConsentActivity.java` +- **TCF v2**: ✓ (IABTCF_ keys in SharedPreferences) +- **COPPA**: tagForChildDirectedTreatment = false +- **AD_ID permission**: declared in manifest +- **Per-network consent**: setHasUserConsent called for [list] +``` + +## References + +- `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/ad-sdk-catalog.md` — Package names, classes, manifest markers for detection +- `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/mediation-patterns.md` — Mediation layers, waterfall vs bidding, adapter identification +- `${CLAUDE_PLUGIN_ROOT}/skills/ad-analysis/references/ad-format-patterns.md` — Banner, interstitial, rewarded, native, app-open patterns diff --git a/plugins/android-reverse-engineering/skills/ad-analysis/references/ad-format-patterns.md b/plugins/android-reverse-engineering/skills/ad-analysis/references/ad-format-patterns.md new file mode 100644 index 0000000..36d83aa --- /dev/null +++ b/plugins/android-reverse-engineering/skills/ad-analysis/references/ad-format-patterns.md @@ -0,0 +1,247 @@ +# Ad Format Patterns + +Code patterns for each ad format across major SDKs — how to identify banner, interstitial, rewarded, native, and app-open ads in decompiled code. + +## Banner Ads + +Small ads displayed within the app UI, typically at top or bottom of screen. + +### Lifecycle +1. Create ad view (XML layout or programmatic) +2. Set ad unit ID +3. Load ad request +4. Ad displays inline — auto-refreshes periodically + +### SDK-specific patterns + +**AdMob**: +```java +AdView adView = new AdView(context); +adView.setAdSize(AdSize.BANNER); // or LARGE_BANNER, MEDIUM_RECTANGLE, FULL_BANNER, LEADERBOARD +adView.setAdUnitId("ca-app-pub-XXXXX/YYYYY"); +adView.loadAd(new AdRequest.Builder().build()); +``` + +**Unity Ads**: +```java +BannerView bannerView = new BannerView(activity, "placement_id", new UnityBannerSize(320, 50)); +bannerView.load(); +``` + +**IronSource**: +```java +IronSource.loadBanner(bannerLayout, "placement_name"); +// or LevelPlay: +LevelPlayBannerAdView banner = new LevelPlayBannerAdView(context, "ad_unit_id"); +banner.loadAd(); +``` + +**AppLovin MAX**: +```java +MaxAdView adView = new MaxAdView("ad_unit_id", context); +adView.loadAd(); +adView.startAutoRefresh(); +``` + +**Meta AN**: +```java +AdView adView = new AdView(context, "placement_id", AdSize.BANNER_HEIGHT_50); +adView.loadAd(); +``` + +### Grep for all banners +```bash +grep -rn 'AdView\|BannerView\|AdSize\.\|BANNER\|loadBanner\|IronSourceBannerLayout\|MaxAdView\|MBBannerView\|PAGBannerAd\|InMobiBanner' "$SOURCE_DIR" +``` + +--- + +## Interstitial Ads + +Full-screen ads shown at natural transition points (level complete, between pages). + +### Lifecycle +1. Load ad in advance (preload) +2. Check if ad is ready +3. Show ad at transition point +4. Handle close callback → resume app flow + +### SDK-specific patterns + +**AdMob**: +```java +InterstitialAd.load(context, "ca-app-pub-XXXXX/YYYYY", adRequest, + new InterstitialAdLoadCallback() { + @Override public void onAdLoaded(InterstitialAd ad) { /* cache */ } + @Override public void onAdFailedToLoad(LoadAdError error) { /* retry */ } + }); +// Show: +interstitialAd.setFullScreenContentCallback(new FullScreenContentCallback() { ... }); +interstitialAd.show(activity); +``` + +**Unity Ads**: +```java +UnityAds.load("placement_id", new IUnityAdsLoadListener() { ... }); +UnityAds.show(activity, "placement_id", new IUnityAdsShowListener() { ... }); +``` + +**IronSource**: +```java +IronSource.loadInterstitial(); +if (IronSource.isInterstitialReady()) { + IronSource.showInterstitial("placement_name"); +} +``` + +**AppLovin MAX**: +```java +MaxInterstitialAd interstitialAd = new MaxInterstitialAd("ad_unit_id", activity); +interstitialAd.loadAd(); +interstitialAd.showAd(); +``` + +### Grep for all interstitials +```bash +grep -rn 'InterstitialAd\|showInterstitial\|loadInterstitial\|MaxInterstitialAd\|isInterstitialReady\|FullScreenContentCallback\|TTFullScreenVideoAd\|PAGInterstitialAd' "$SOURCE_DIR" +``` + +--- + +## Rewarded Ads + +Full-screen ads that give the user an in-app reward for watching. + +### Lifecycle +1. Load ad in advance +2. Show ad when user opts in (e.g., "Watch ad for extra life") +3. User watches ad to completion +4. Reward callback fires → grant reward +5. Handle incomplete views (user skipped) + +### SDK-specific patterns + +**AdMob**: +```java +RewardedAd.load(context, "ca-app-pub-XXXXX/YYYYY", adRequest, + new RewardedAdLoadCallback() { + @Override public void onAdLoaded(RewardedAd ad) { /* cache */ } + }); +// Show: +rewardedAd.show(activity, new OnUserEarnedRewardListener() { + @Override public void onUserEarnedReward(RewardItem reward) { + int amount = reward.getAmount(); + String type = reward.getType(); + } +}); +``` + +**Unity Ads**: +```java +// Same load/show as interstitial, but placement configured as rewarded on dashboard +UnityAds.load("rewarded_placement", loadListener); +UnityAds.show(activity, "rewarded_placement", showListener); +// Reward granted in onUnityAdsShowComplete with UnityAds.UnityAdsShowCompletionState.COMPLETED +``` + +**IronSource**: +```java +// Auto-loaded by default +if (IronSource.isRewardedVideoAvailable()) { + IronSource.showRewardedVideo("placement_name"); +} +// Reward in RewardedVideoListener.onRewardedVideoAdRewarded(Placement placement) +``` + +**AppLovin MAX**: +```java +MaxRewardedAd rewardedAd = MaxRewardedAd.getInstance("ad_unit_id", activity); +rewardedAd.loadAd(); +rewardedAd.showAd(); // reward via MaxRewardedAdListener.onUserRewarded(MaxAd, MaxReward) +``` + +### Grep for all rewarded +```bash +grep -rn 'RewardedAd\|RewardedVideo\|showRewardedVideo\|OnUserEarnedRewardListener\|RewardItem\|MaxRewardedAd\|TTRewardVideoAd\|PAGRewardedAd\|MBRewardVideoHandler' "$SOURCE_DIR" +``` + +--- + +## Native Ads + +Ads that match the app's look and feel, rendered with custom templates. + +### Lifecycle +1. Create ad loader with ad unit ID +2. Load ad +3. Receive native ad object with assets (headline, body, image, CTA, icon) +4. Inflate custom layout and bind assets +5. Register ad view for impression/click tracking + +### SDK-specific patterns + +**AdMob**: +```java +AdLoader adLoader = new AdLoader.Builder(context, "ca-app-pub-XXXXX/YYYYY") + .forNativeAd(nativeAd -> { /* bind to NativeAdView */ }) + .withNativeAdOptions(new NativeAdOptions.Builder().build()) + .build(); +adLoader.loadAd(new AdRequest.Builder().build()); +``` + +**Meta AN**: +```java +NativeAd nativeAd = new NativeAd(context, "placement_id"); +nativeAd.loadAd(nativeAd.buildLoadAdConfig() + .withAdListener(new NativeAdListener() { ... }) + .build()); +``` + +**AppLovin MAX**: +```java +MaxNativeAdLoader nativeAdLoader = new MaxNativeAdLoader("ad_unit_id", context); +nativeAdLoader.setNativeAdListener(new MaxNativeAdListener() { ... }); +nativeAdLoader.loadAd(); +``` + +### Grep for all native +```bash +grep -rn 'NativeAd\|NativeAdView\|NativeAdLoader\|AdLoader\.Builder\|NativeBannerAd\|NativeAdLayout\|MaxNativeAdLoader\|InMobiNative\|VungleNativeAd' "$SOURCE_DIR" +``` + +--- + +## App Open Ads + +Full-screen ads shown when the app is foregrounded (cold start or resume from background). + +### Lifecycle +1. Preload ad during app initialization or background +2. On app foreground, check if ad is available and not expired +3. Show ad before the main content appears +4. Handle close callback → show app content + +### Pattern (AdMob-specific) +```java +AppOpenAd.load(context, "ca-app-pub-XXXXX/YYYYY", adRequest, + AppOpenAd.APP_OPEN_AD_ORIENTATION_PORTRAIT, + new AppOpenAd.AppOpenAdLoadCallback() { + @Override public void onAdLoaded(AppOpenAd ad) { /* cache with timestamp */ } + }); +// Show on foreground: +appOpenAd.show(activity); +``` + +Common implementation pattern uses `Application.ActivityLifecycleCallbacks` to detect foreground transitions: +```java +registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { + @Override public void onActivityStarted(Activity activity) { + // Show app open ad if available + } +}); +``` + +### Grep for app open +```bash +grep -rn 'AppOpenAd\|AppOpenAdLoadCallback\|APP_OPEN_AD\|AppOpenAdManager\|appOpenAd' "$SOURCE_DIR" +``` diff --git a/plugins/android-reverse-engineering/skills/ad-analysis/references/ad-sdk-catalog.md b/plugins/android-reverse-engineering/skills/ad-analysis/references/ad-sdk-catalog.md new file mode 100644 index 0000000..5975dd2 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/ad-analysis/references/ad-sdk-catalog.md @@ -0,0 +1,103 @@ +# Ad SDK Catalog + +Quick-reference for detecting advertising SDKs in decompiled Android apps. + +## AdMob / Google Mobile Ads + +| Field | Value | +|---|---| +| **Package** | `com.google.android.gms.ads` | +| **Key classes** | `MobileAds`, `AdView`, `InterstitialAd`, `RewardedAd`, `NativeAd`, `AppOpenAd`, `AdRequest`, `AdLoader` | +| **Manifest markers** | ``, `` | +| **Gradle** | `com.google.android.gms:play-services-ads` | +| **Grep detection** | `MobileAds\.initialize\|com\.google\.android\.gms\.ads\|ca-app-pub-` | + +## Unity Ads + +| Field | Value | +|---|---| +| **Package** | `com.unity3d.ads`, `com.unity3d.services` | +| **Key classes** | `UnityAds`, `BannerView`, `IUnityAdsInitializationListener`, `IUnityAdsLoadListener`, `IUnityAdsShowListener` | +| **Manifest markers** | ``, `` | +| **Gradle** | `com.unity3d.ads:unity-ads` | +| **Grep detection** | `UnityAds\.initialize\|com\.unity3d\.ads\|IUnityAds` | + +## IronSource / LevelPlay + +| Field | Value | +|---|---| +| **Package** | `com.ironsource.mediationsdk`, `com.ironsource.sdk` | +| **Key classes** | `IronSource`, `IronSourceBannerLayout`, `InterstitialListener`, `RewardedVideoListener`, `LevelPlayBannerAdView`, `LevelPlayInterstitialAd`, `LevelPlayRewardedAd` | +| **Manifest markers** | ``, `` | +| **Gradle** | `com.ironsource.sdk:mediationsdk` | +| **Grep detection** | `IronSource\.init\|com\.ironsource\.mediationsdk\|LevelPlay` | + +## AppLovin / MAX + +| Field | Value | +|---|---| +| **Package** | `com.applovin.sdk`, `com.applovin.mediation` | +| **Key classes** | `AppLovinSdk`, `MaxAdView`, `MaxInterstitialAd`, `MaxRewardedAd`, `MaxNativeAdLoader`, `MaxAdListener`, `MaxRewardedAdListener` | +| **Manifest markers** | ``, `` | +| **Gradle** | `com.applovin:applovin-sdk` | +| **Grep detection** | `AppLovinSdk\.getInstance\|MaxAdView\|MaxInterstitialAd\|MaxRewardedAd` | + +## Meta Audience Network (Facebook) + +| Field | Value | +|---|---| +| **Package** | `com.facebook.ads` | +| **Key classes** | `AudienceNetworkAds`, `AdView`, `InterstitialAd`, `RewardedVideoAd`, `NativeAd`, `NativeBannerAd`, `NativeAdLayout` | +| **Manifest markers** | ``, `` | +| **Gradle** | `com.facebook.android:audience-network-sdk` | +| **Grep detection** | `AudienceNetworkAds\.initialize\|com\.facebook\.ads\.\|InterstitialAdListener` | + +## Vungle / Liftoff + +| Field | Value | +|---|---| +| **Package** | `com.vungle.warren` (legacy), `com.vungle.ads` (v7+) | +| **Key classes** | `Vungle`, `VungleBanner`, `VungleInterstitial`, `VungleRewarded`, `VungleNativeAd` | +| **Manifest markers** | `` | +| **Gradle** | `com.vungle:vungle-ads` | +| **Grep detection** | `Vungle\.init\|com\.vungle\.\|VungleBanner\|VungleInterstitial` | + +## InMobi + +| Field | Value | +|---|---| +| **Package** | `com.inmobi.sdk`, `com.inmobi.ads` | +| **Key classes** | `InMobiSdk`, `InMobiBanner`, `InMobiInterstitial`, `InMobiNative` | +| **Manifest markers** | `` | +| **Gradle** | `com.inmobi.monetization:inmobi-ads` | +| **Grep detection** | `InMobiSdk\.init\|InMobiBanner\|InMobiInterstitial` | + +## Chartboost + +| Field | Value | +|---|---| +| **Package** | `com.chartboost.sdk` | +| **Key classes** | `Chartboost`, `ChartboostDelegate` | +| **Manifest markers** | `` | +| **Gradle** | `com.chartboost:chartboost-sdk` | +| **Grep detection** | `Chartboost\.startWithAppId\|Chartboost\.showInterstitial\|com\.chartboost\.sdk` | + +## Pangle / TikTok (Bytedance) + +| Field | Value | +|---|---| +| **Package** | `com.bytedance.sdk.openadsdk` (legacy), `com.pgl.sys` (v5+) | +| **Key classes** | `TTAdSdk`, `TTAdConfig`, `TTAdNative`, `TTFullScreenVideoAd`, `TTRewardVideoAd`, `TTBannerAd`; v5: `PAGSdk`, `PAGBannerAd`, `PAGInterstitialAd`, `PAGRewardedAd` | +| **Manifest markers** | `` | +| **Gradle** | `com.pangle.global:ads-sdk` | +| **Grep detection** | `TTAdSdk\.init\|PAGSdk\.init\|com\.bytedance\.sdk\.openadsdk` | + +## Mintegral + +| Field | Value | +|---|---| +| **Package** | `com.mbridge.msdk` | +| **Key classes** | `MBridgeSDKFactory`, `MBBannerView`, `MBInterstitialVideoHandler`, `MBRewardVideoHandler`, `MBNewInterstitialHandler` | +| **Manifest markers** | `` | +| **Gradle** | `com.mbridge.msdk.oversea:mbbanner`, `com.mbridge.msdk.oversea:interstitialvideo`, etc. | +| **Grep detection** | `MBridgeSDKFactory\|com\.mbridge\.msdk\|MBBannerView\|MBRewardVideoHandler` | diff --git a/plugins/android-reverse-engineering/skills/ad-analysis/references/mediation-patterns.md b/plugins/android-reverse-engineering/skills/ad-analysis/references/mediation-patterns.md new file mode 100644 index 0000000..020649f --- /dev/null +++ b/plugins/android-reverse-engineering/skills/ad-analysis/references/mediation-patterns.md @@ -0,0 +1,231 @@ +# Mediation Patterns + +How ad mediation works in decompiled Android code — identifying the primary mediator, adapter chains, waterfall vs bidding, and configuration extraction. + +## What is Mediation + +Ad mediation allows an app to use multiple ad networks through a single SDK. A **mediator** (e.g., AdMob, IronSource, AppLovin MAX) orchestrates which ad network serves each impression via: + +- **Waterfall**: Networks are tried in priority order; first to fill wins +- **Bidding (header bidding)**: Networks bid simultaneously; highest bid wins +- **Hybrid**: Some networks bid, others fill via waterfall + +## Identifying the Primary Mediator + +The mediator is the SDK that controls ad loading. Look for: + +### Google AdMob Mediation +```java +// AdMob is the mediator when you see mediation adapters: +com.google.ads.mediation.* +// Adapter classes follow the pattern: +com.google.ads.mediation.unity.UnityAdapter +com.google.ads.mediation.ironsource.IronSourceMediationAdapter +com.google.ads.mediation.applovin.AppLovinMediationAdapter +com.google.ads.mediation.facebook.FacebookMediationAdapter +com.google.ads.mediation.vungle.VungleMediationAdapter +``` + +**Grep**: +```bash +grep -rn 'com\.google\.ads\.mediation\.' "$SOURCE_DIR" +``` + +### IronSource / LevelPlay Mediation +```java +// IronSource is the mediator when you see adapter packages: +com.ironsource.adapters.* +// Adapter naming: +com.ironsource.adapters.admob.* +com.ironsource.adapters.unityads.* +com.ironsource.adapters.applovin.* +com.ironsource.adapters.facebook.* +com.ironsource.adapters.vungle.* +``` + +**Grep**: +```bash +grep -rn 'com\.ironsource\.adapters\.' "$SOURCE_DIR" +``` + +### AppLovin MAX Mediation +```java +// MAX is the mediator when you see MAX adapter packages: +com.applovin.mediation.adapters.* +// Adapter classes extend MediationAdapterBase: +com.applovin.mediation.adapters.GoogleMediationAdapter +com.applovin.mediation.adapters.UnityAdsMediationAdapter +com.applovin.mediation.adapters.IronSourceMediationAdapter +com.applovin.mediation.adapters.FacebookMediationAdapter +``` + +**Grep**: +```bash +grep -rn 'com\.applovin\.mediation\.adapters\.' "$SOURCE_DIR" +``` + +## Waterfall vs Bidding + +### Waterfall indicators +```bash +# Priority/floor price configuration +grep -rn 'waterfall\|floorPrice\|ecpm\|eCPM\|setPriority\|adNetworkOrder' "$SOURCE_DIR" + +# Manual network ordering +grep -rn 'setNetworkOrder\|setAdapterOrder\|setWaterfallConfiguration' "$SOURCE_DIR" +``` + +### Bidding indicators +```bash +# Real-time bidding signals +grep -rn 'bidding\|headerBidding\|bidToken\|collectSignal\|getBiddingToken' "$SOURCE_DIR" + +# Bidding adapter classes (often have "Bidding" in the name) +grep -rn 'BiddingAdapter\|BiddingProvider\|RTBAdapter' "$SOURCE_DIR" + +# MAX bidding +grep -rn 'MaxMediatedNetworkInfo\|isBidding\|bidFloor' "$SOURCE_DIR" +``` + +## Adapter Discovery + +### List all mediation adapters present +```bash +# Find all adapter class files +find "$SOURCE_DIR" -path "*/mediation/adapters/*.java" -o -path "*/adapters/*Adapter*.java" | head -50 + +# Find adapter registration/initialization +grep -rn 'registerAdapter\|initializeAdapter\|setAdapterState\|MediationAdapter' "$SOURCE_DIR" +``` + +### Extract adapter configuration +```bash +# Network IDs and app keys passed to adapters +grep -rn 'setAppKey\|setAppId\|setGameId\|setSdkKey\|setNetworkKey' "$SOURCE_DIR" + +# Ad unit/placement mapping (mediator → network) +grep -rn 'placementMap\|adUnitMap\|networkPlacementId\|customEventExtras' "$SOURCE_DIR" +``` + +## Common Mediation Setups + +### Setup 1: AdMob as Mediator +``` +AdMob (primary) → loads via: + ├── Google Ads (direct) + ├── UnityAdapter → Unity Ads SDK + ├── IronSourceMediationAdapter → IronSource SDK + ├── AppLovinMediationAdapter → AppLovin SDK + └── FacebookMediationAdapter → Meta AN SDK +``` + +### Setup 2: IronSource/LevelPlay as Mediator +``` +IronSource (primary) → loads via: + ├── IronSource Network (direct) + ├── AdMob adapter → Google Mobile Ads SDK + ├── Unity adapter → Unity Ads SDK + ├── AppLovin adapter → AppLovin SDK + └── Vungle adapter → Vungle SDK +``` + +### Setup 3: AppLovin MAX as Mediator +``` +AppLovin MAX (primary) → loads via: + ├── AppLovin Network (direct, always bidding) + ├── GoogleMediationAdapter → AdMob SDK + ├── IronSourceMediationAdapter → IronSource SDK + ├── UnityAdsMediationAdapter → Unity Ads SDK + └── FacebookMediationAdapter → Meta AN SDK +``` + +## Active vs Passive SDKs + +When mediation is present, most ad SDKs in the APK are **passive dependencies** — they are only invoked internally by the mediator's adapter classes, never by the app's own code. + +### The Entry Point Graph + +``` +App code (com.example.myapp.*) + │ + ▼ +Mediator SDK (e.g., AdMob) ← ACTIVE: called by app code + │ + ├──→ Adapter (com.google.ads.mediation.unity.*) + │ └──→ Unity Ads SDK ← PASSIVE: called only by adapter + │ + ├──→ Adapter (com.google.ads.mediation.ironsource.*) + │ └──→ IronSource SDK ← PASSIVE: called only by adapter + │ + └──→ Adapter (com.google.ads.mediation.applovin.*) + └──→ AppLovin SDK ← PASSIVE: called only by adapter +``` + +### How to Distinguish + +An SDK is **active** if its init/load/show calls appear in app code. An SDK is **passive** if its code is present only inside library packages. + +**Key principle**: Search for SDK calls only in non-library directories. Known library packages to exclude: + +| Package prefix | SDK | +|---|---| +| `com/google` | Google/AdMob | +| `com/unity3d` | Unity Ads | +| `com/ironsource` | IronSource/LevelPlay | +| `com/applovin` | AppLovin/MAX | +| `com/facebook` | Meta Audience Network | +| `com/vungle`, `io/vungle` | Vungle/Liftoff | +| `com/inmobi` | InMobi | +| `com/chartboost` | Chartboost | +| `com/bytedance`, `com/pgl` | Pangle/TikTok | +| `com/mbridge`, `com/mintegral` | Mintegral | + +### Grep Examples + +```bash +# Search for ad SDK calls ONLY in app code (exclude all library packages) +grep -rn --include="*.java" --include="*.kt" \ + --exclude-dir="com/google" --exclude-dir="com/unity3d" \ + --exclude-dir="com/ironsource" --exclude-dir="com/applovin" \ + --exclude-dir="com/facebook" --exclude-dir="com/vungle" \ + --exclude-dir="com/inmobi" --exclude-dir="com/chartboost" \ + --exclude-dir="com/bytedance" --exclude-dir="com/pgl" \ + --exclude-dir="com/mbridge" \ + -E '(MobileAds\.initialize|UnityAds\.initialize|IronSource\.init|AppLovinSdk\.getInstance)' \ + "$SOURCE_DIR" +``` + +**Interpreting results**: +- If `MobileAds.initialize` appears in `com/example/myapp/AdManager.java` → AdMob is **active** +- If `UnityAds.initialize` appears only in `com/google/ads/mediation/unity/UnityAdapter.java` → Unity Ads is **passive** (but this file is excluded, so it won't show up) +- If `UnityAds.initialize` appears in `com/example/myapp/AdsHelper.java` → Unity Ads is also **active** (direct integration alongside mediation) + +### Architecture Classification + +| Architecture | Meaning | Example | +|---|---|---| +| **Single mediator** | App calls 1 SDK; all others are passive adapter deps | AdMob init + 5 mediated networks | +| **Multiple direct** | App calls 2+ SDKs directly; no mediation | AdMob banners + Unity interstitials | +| **Hybrid** | App calls a mediator + some SDKs directly | AdMob mediation + direct IronSource rewarded | + +### Automated Detection + +Use the `--entrypoints` flag of `find-ads.sh`: + +```bash +bash find-ads.sh --entrypoints +``` + +This runs the exclusion-based grep automatically and shows only app-initiated SDK calls. + +## Extracting Mediation Configuration + +Look for server-side configuration that controls the mediation waterfall: + +```bash +# JSON config responses from mediation servers +grep -rn 'mediationConfig\|auctionResponse\|waterfallConfig\|adNetworkConfig' "$SOURCE_DIR" + +# Remote config / A/B test for ad setup +grep -rn 'RemoteConfig.*ad\|firebase.*ad_config\|ad_waterfall\|ad_network_ids' "$SOURCE_DIR" +``` diff --git a/plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh b/plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh new file mode 100755 index 0000000..75bf113 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/ad-analysis/scripts/find-ads.sh @@ -0,0 +1,296 @@ +#!/usr/bin/env bash +# find-ads.sh — Search decompiled source for advertising SDK usage +set -euo pipefail + +usage() { + cat < [OPTIONS] + +Search decompiled Java/Kotlin source for advertising SDK usage. + +Arguments: + Path to the decompiled sources directory + +Options: + --admob Search only for AdMob/Google Mobile Ads + --unity Search only for Unity Ads + --ironsource Search only for IronSource/LevelPlay + --applovin Search only for AppLovin/MAX + --facebook Search only for Meta Audience Network + --vungle Search only for Vungle/Liftoff + --inmobi Search only for InMobi + --chartboost Search only for Chartboost + --pangle Search only for Pangle/TikTok + --mintegral Search only for Mintegral + --formats Search only for cross-SDK ad format patterns + --mediation Search only for mediation adapter patterns + --consent Search only for consent/privacy framework patterns + --manifest Search only for AndroidManifest.xml ad markers + --entrypoints Search only for ad SDK calls in app code (excludes library packages) + --all Search all patterns (default) + -h, --help Show this help message + +Output: + Results are printed as file:line:match for easy navigation. +EOF + exit 0 +} + +SOURCE_DIR="" +SEARCH_ADMOB=false +SEARCH_UNITY=false +SEARCH_IRONSOURCE=false +SEARCH_APPLOVIN=false +SEARCH_FACEBOOK=false +SEARCH_VUNGLE=false +SEARCH_INMOBI=false +SEARCH_CHARTBOOST=false +SEARCH_PANGLE=false +SEARCH_MINTEGRAL=false +SEARCH_FORMATS=false +SEARCH_MEDIATION=false +SEARCH_CONSENT=false +SEARCH_MANIFEST=false +SEARCH_ENTRYPOINTS=false +SEARCH_ALL=true + +while [[ $# -gt 0 ]]; do + case "$1" in + --admob) SEARCH_ADMOB=true; SEARCH_ALL=false; shift ;; + --unity) SEARCH_UNITY=true; SEARCH_ALL=false; shift ;; + --ironsource) SEARCH_IRONSOURCE=true; SEARCH_ALL=false; shift ;; + --applovin) SEARCH_APPLOVIN=true; SEARCH_ALL=false; shift ;; + --facebook) SEARCH_FACEBOOK=true; SEARCH_ALL=false; shift ;; + --vungle) SEARCH_VUNGLE=true; SEARCH_ALL=false; shift ;; + --inmobi) SEARCH_INMOBI=true; SEARCH_ALL=false; shift ;; + --chartboost) SEARCH_CHARTBOOST=true; SEARCH_ALL=false; shift ;; + --pangle) SEARCH_PANGLE=true; SEARCH_ALL=false; shift ;; + --mintegral) SEARCH_MINTEGRAL=true; SEARCH_ALL=false; shift ;; + --formats) SEARCH_FORMATS=true; SEARCH_ALL=false; shift ;; + --mediation) SEARCH_MEDIATION=true; SEARCH_ALL=false; shift ;; + --consent) SEARCH_CONSENT=true; SEARCH_ALL=false; shift ;; + --manifest) SEARCH_MANIFEST=true; SEARCH_ALL=false; shift ;; + --entrypoints) SEARCH_ENTRYPOINTS=true; SEARCH_ALL=false; shift ;; + --all) SEARCH_ALL=true; shift ;; + -h|--help) usage ;; + -*) echo "Error: Unknown option $1" >&2; usage ;; + *) SOURCE_DIR="$1"; shift ;; + esac +done + +if [[ -z "$SOURCE_DIR" ]]; then + echo "Error: No source directory specified." >&2 + usage +fi + +if [[ ! -d "$SOURCE_DIR" ]]; then + echo "Error: Directory not found: $SOURCE_DIR" >&2 + exit 1 +fi + +GREP_OPTS="-rn --include=*.java --include=*.kt" + +section() { + echo + echo "==== $1 ====" + echo +} + +run_grep() { + local pattern="$1" + # shellcheck disable=SC2086 + grep $GREP_OPTS -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true +} + +run_grep_xml() { + local pattern="$1" + grep -rn --include="*.xml" -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true +} + +# --- AdMob / Google Mobile Ads --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_ADMOB" == true ]]; then + section "AdMob — Initialization" + run_grep '(MobileAds\.initialize\s*\(|MobileAds\.setRequestConfiguration|com\.google\.android\.gms\.ads)' + section "AdMob — Banner" + run_grep '(AdView\s|AdSize\.|\.loadAd\s*\(|AdRequest\.Builder|com\.google\.android\.gms\.ads\.AdView)' + section "AdMob — Interstitial" + run_grep '(InterstitialAd\.load\s*\(|InterstitialAdLoadCallback|FullScreenContentCallback|com\.google\.android\.gms\.ads\.interstitial)' + section "AdMob — Rewarded" + run_grep '(RewardedAd\.load\s*\(|RewardedAdLoadCallback|OnUserEarnedRewardListener|RewardedInterstitialAd\.load)' + section "AdMob — Native" + run_grep '(AdLoader\.Builder|NativeAd\.|NativeAdView|UnifiedNativeAd|NativeAdOptions|com\.google\.android\.gms\.ads\.nativead)' + section "AdMob — App Open" + run_grep '(AppOpenAd\.load\s*\(|AppOpenAd\.AppOpenAdLoadCallback|AppOpenAdManager)' + section "AdMob — Ad Unit IDs" + run_grep '(ca-app-pub-|/[0-9]+/[0-9]+|adUnitId|setAdUnitId\s*\()' +fi + +# --- Unity Ads --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_UNITY" == true ]]; then + section "Unity Ads — Initialization" + run_grep '(UnityAds\.initialize\s*\(|UnityAds\.isInitialized|IUnityAdsInitializationListener|com\.unity3d\.ads)' + section "Unity Ads — Load & Show" + run_grep '(UnityAds\.load\s*\(|UnityAds\.show\s*\(|IUnityAdsLoadListener|IUnityAdsShowListener)' + section "Unity Ads — Banner" + run_grep '(BannerView\s*\(|UnityBannerSize|com\.unity3d\.services\.banners)' +fi + +# --- IronSource / LevelPlay --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_IRONSOURCE" == true ]]; then + section "IronSource — Initialization" + run_grep '(IronSource\.init\s*\(|IronSource\.setUserId|IronSourceConfig|com\.ironsource\.mediationsdk)' + section "IronSource — Interstitial" + run_grep '(IronSource\.loadInterstitial|IronSource\.showInterstitial|IronSource\.isInterstitialReady|InterstitialListener)' + section "IronSource — Rewarded Video" + run_grep '(IronSource\.showRewardedVideo|IronSource\.isRewardedVideoAvailable|RewardedVideoListener)' + section "IronSource — Banner" + run_grep '(IronSource\.loadBanner|IronSource\.destroyBanner|IronSourceBannerLayout|BannerListener)' + section "IronSource — LevelPlay" + run_grep '(LevelPlay|LevelPlayAdError|LevelPlayBannerAdView|LevelPlayInterstitialAd|LevelPlayRewardedAd)' +fi + +# --- AppLovin / MAX --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_APPLOVIN" == true ]]; then + section "AppLovin — Initialization" + run_grep '(AppLovinSdk\.getInstance|AppLovinSdk\.initializeSdk|AppLovinSdkSettings|com\.applovin\.sdk)' + section "AppLovin MAX — Banner" + run_grep '(MaxAdView\s|MaxAdViewAdListener|maxAdView\.loadAd|maxAdView\.startAutoRefresh)' + section "AppLovin MAX — Interstitial" + run_grep '(MaxInterstitialAd\s*\(|MaxAdListener|interstitialAd\.loadAd|interstitialAd\.showAd)' + section "AppLovin MAX — Rewarded" + run_grep '(MaxRewardedAd\.getInstance|MaxRewardedAdListener|rewardedAd\.loadAd|rewardedAd\.showAd)' + section "AppLovin MAX — Native" + run_grep '(MaxNativeAdLoader|MaxNativeAdView|MaxNativeAdListener)' +fi + +# --- Facebook / Meta Audience Network --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_FACEBOOK" == true ]]; then + section "Meta Audience Network — Initialization" + run_grep '(AudienceNetworkAds\.initialize|AudienceNetworkAds\.buildInitSettings|com\.facebook\.ads)' + section "Meta AN — Banner" + run_grep '(com\.facebook\.ads\.AdView|com\.facebook\.ads\.AdSize|\.loadAd\s*\(.*AdView)' + section "Meta AN — Interstitial" + run_grep '(com\.facebook\.ads\.InterstitialAd|InterstitialAdListener|InterstitialAdExtendedListener)' + section "Meta AN — Rewarded" + run_grep '(RewardedVideoAd\s*\(|RewardedVideoAdListener|RewardedVideoAdExtendedListener|com\.facebook\.ads\.RewardedVideoAd)' + section "Meta AN — Native" + run_grep '(NativeAd\s*\(|NativeBannerAd|NativeAdListener|com\.facebook\.ads\.NativeAd|NativeAdLayout)' +fi + +# --- Vungle / Liftoff --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_VUNGLE" == true ]]; then + section "Vungle — Initialization & Ads" + run_grep '(Vungle\.init\s*\(|VungleInitListener|Vungle\.loadAd\s*\(|Vungle\.playAd\s*\(|Vungle\.isInitialized|com\.vungle\.warren)' + run_grep '(VungleBanner|VungleInterstitial|VungleRewarded|VungleNativeAd|com\.vungle\.ads)' +fi + +# --- InMobi --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_INMOBI" == true ]]; then + section "InMobi — Initialization & Ads" + run_grep '(InMobiSdk\.init\s*\(|InMobiBanner|InMobiInterstitial|InMobiNative|com\.inmobi\.sdk|com\.inmobi\.ads)' +fi + +# --- Chartboost --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_CHARTBOOST" == true ]]; then + section "Chartboost — Initialization & Ads" + run_grep '(Chartboost\.startWithAppId|Chartboost\.showInterstitial|Chartboost\.showRewardedVideo|Chartboost\.cacheInterstitial|com\.chartboost\.sdk)' +fi + +# --- Pangle / TikTok --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_PANGLE" == true ]]; then + section "Pangle — Initialization & Ads" + run_grep '(TTAdSdk\.init\s*\(|TTAdConfig\.Builder|TTAdNative|TTFullScreenVideoAd|TTRewardVideoAd|TTBannerAd|com\.bytedance\.sdk\.openadsdk)' + run_grep '(PAGSdk\.init\s*\(|PAGConfig\.Builder|PAGBannerAd|PAGInterstitialAd|PAGRewardedAd|com\.pgl\.sys)' +fi + +# --- Mintegral --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_MINTEGRAL" == true ]]; then + section "Mintegral — Initialization & Ads" + run_grep '(MBridgeSDKFactory\.getMBridgeSDK|MBBannerView|MBInterstitialVideoHandler|MBRewardVideoHandler|MBNewInterstitialHandler|com\.mbridge\.msdk)' +fi + +# --- Cross-SDK Ad Format Patterns --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_FORMATS" == true ]]; then + section "Cross-SDK — All Banner Patterns" + run_grep '(BannerView|AdView|BannerAd|IronSourceBannerLayout|MaxAdView|MBBannerView|PAGBannerAd|TTBannerAd|InMobiBanner|VungleBanner)' + section "Cross-SDK — All Interstitial Patterns" + run_grep '(InterstitialAd|showInterstitial|loadInterstitial|MaxInterstitialAd|MBInterstitialVideoHandler|PAGInterstitialAd|TTFullScreenVideoAd|InMobiInterstitial|VungleInterstitial)' + section "Cross-SDK — All Rewarded Patterns" + run_grep '(RewardedAd|RewardedVideo|showRewardedVideo|MaxRewardedAd|MBRewardVideoHandler|PAGRewardedAd|TTRewardVideoAd|VungleRewarded|OnUserEarnedRewardListener)' + section "Cross-SDK — All Native Patterns" + run_grep '(NativeAd|NativeAdView|NativeAdLoader|NativeBannerAd|MaxNativeAdLoader|InMobiNative|VungleNativeAd|NativeAdLayout)' + section "Cross-SDK — Ad Unit / Placement IDs" + run_grep '(adUnitId|setAdUnitId|placementId|setPlacementId|PLACEMENT_ID|AD_UNIT_ID|ca-app-pub-|BANNER_ID|INTERSTITIAL_ID|REWARDED_ID)' +fi + +# --- Mediation Adapters --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_MEDIATION" == true ]]; then + section "Mediation — Google AdMob Mediation Adapters" + run_grep '(com\.google\.ads\.mediation|MediationAdapter|MediationBannerAdapter|MediationInterstitialAdapter|MediationRewardedAd)' + section "Mediation — IronSource Mediation Adapters" + run_grep '(com\.ironsource\.adapters|IronSourceAdapter|ISBannerAdapter|ISInterstitialAdapter|ISRewardedVideoAdapter)' + section "Mediation — AppLovin MAX Mediation Adapters" + run_grep '(com\.applovin\.mediation\.adapters|MediationAdapterBase|MaxAdapter|MaxMediatedNetworkInfo)' + section "Mediation — Waterfall & Bidding Configuration" + run_grep '(waterfall|bidding|headerBidding|realTimeBidding|setWaterfall|mediationConfig|adNetworkId|networkName)' +fi + +# --- Consent / Privacy Frameworks --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_CONSENT" == true ]]; then + section "Consent — Google UMP / Funding Choices" + run_grep '(ConsentInformation|ConsentRequestParameters|ConsentForm\.load|UserMessagingPlatform|com\.google\.android\.ump)' + section "Consent — IAB TCF v2" + run_grep '(IABTCF_|TCString|GDPR_APPLIES|GDPR_CONSENT|gdprApplies|tcfPolicyVersion|tcString)' + section "Consent — COPPA / Child-Directed" + run_grep '(tagForChildDirectedTreatment|COPPA|TAG_FOR_CHILD_DIRECTED|setTagForUnderAgeOfConsent|maxAdContentRating)' + section "Consent — SDK-specific Opt-out" + run_grep '(setHasUserConsent|setDoNotSell|setCCPADoNotSell|setGDPRConsent|gdprForgetMe|setDataProcessingOptions|setNonPersonalizedOnly)' +fi + +# --- AndroidManifest.xml Markers --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_MANIFEST" == true ]]; then + section "AndroidManifest — Ad Activities & Services" + run_grep_xml '(com\.google\.android\.gms\.ads|AdActivity|com\.unity3d\.ads|com\.ironsource|com\.applovin|com\.facebook\.ads|com\.vungle|com\.inmobi|com\.chartboost|com\.bytedance)' + section "AndroidManifest — Ad Permissions" + run_grep_xml '(AD_ID|com\.google\.android\.gms\.permission\.AD_ID|BIND_GET_INSTALL_REFERRER_SERVICE)' + section "AndroidManifest — Ad Meta-data" + run_grep_xml '(com\.google\.android\.gms\.ads\.APPLICATION_ID|com\.google\.android\.gms\.ads\.AD_MANAGER_APP|applovin\.sdk\.key|com\.facebook\.sdk\.ApplicationId|unity\.ads\.gameId)' +fi + +# --- Entry Points: Ad SDK calls from app code only --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_ENTRYPOINTS" == true ]]; then + section "Entry Points — Ad SDK calls from app code (library packages excluded)" + + # Build --exclude-dir arguments for known library packages + EXCLUDE_DIRS=( + --exclude-dir="com/google" + --exclude-dir="com/unity3d" + --exclude-dir="com/ironsource" + --exclude-dir="com/applovin" + --exclude-dir="com/facebook" + --exclude-dir="com/vungle" + --exclude-dir="com/inmobi" + --exclude-dir="com/chartboost" + --exclude-dir="com/bytedance" + --exclude-dir="com/pgl" + --exclude-dir="com/mbridge" + --exclude-dir="com/mintegral" + --exclude-dir="com/adcolony" + --exclude-dir="com/tapjoy" + --exclude-dir="com/fyber" + --exclude-dir="com/amazon/device/ads" + --exclude-dir="io/vungle" + ) + + ENTRYPOINT_PATTERN='(MobileAds\.initialize|MobileAds\.setRequestConfiguration|InterstitialAd\.load|RewardedAd\.load|RewardedInterstitialAd\.load|AppOpenAd\.load|AdView\.loadAd|AdLoader\.Builder|UnityAds\.initialize|UnityAds\.load|UnityAds\.show|IronSource\.init|IronSource\.loadInterstitial|IronSource\.showInterstitial|IronSource\.showRewardedVideo|IronSource\.loadBanner|AppLovinSdk\.getInstance|AppLovinSdk\.initializeSdk|MaxInterstitialAd|MaxRewardedAd|MaxAdView|AudienceNetworkAds\.initialize|Vungle\.init|Vungle\.loadAd|Vungle\.playAd|InMobiSdk\.init|Chartboost\.startWithAppId|TTAdSdk\.init|PAGSdk\.init|MBridgeSDKFactory\.getMBridgeSDK|LevelPlayInterstitialAd|LevelPlayRewardedAd|LevelPlayBannerAdView)' + + # shellcheck disable=SC2086 + grep -rn --include="*.java" --include="*.kt" "${EXCLUDE_DIRS[@]}" -E "$ENTRYPOINT_PATTERN" "$SOURCE_DIR" 2>/dev/null || true + + echo + echo "NOTE: Only calls from app code are shown above. Library-internal calls are excluded." + echo "If no results appear, ad SDKs may only be invoked internally via mediation adapters." +fi + +echo +echo "=== Search complete ===" diff --git a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh index b25e8e2..b4f37a4 100755 --- a/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh +++ b/plugins/android-reverse-engineering/skills/android-reverse-engineering/scripts/install-dep.sh @@ -59,13 +59,18 @@ elif command -v pacman &>/dev/null; then PKG_MANAGER="pacman" fi -# Check sudo availability +# Check sudo availability — must actually work, not just exist if command -v sudo &>/dev/null; then if sudo -n true 2>/dev/null; then + # Passwordless sudo works (NOPASSWD or cached credentials) + HAS_SUDO=true + elif [[ -t 0 ]]; then + # stdin is a terminal — sudo can prompt for password interactively HAS_SUDO=true else - # sudo exists but may need password — we'll try it and let it prompt - HAS_SUDO=true + # sudo exists but can't authenticate (no TTY, no cached credentials) + # This happens inside Claude Code, CI, cron, pipes, etc. + HAS_SUDO=false fi fi @@ -73,8 +78,13 @@ info() { echo "[INFO] $*"; } ok() { echo "[OK] $*"; } fail() { echo "[FAIL] $*" >&2; } manual() { - echo "[MANUAL] $*" >&2 - echo " Cannot install automatically. Please install manually and retry." >&2 + echo "" >&2 + echo "[MANUAL ACTION REQUIRED]" >&2 + echo " Cannot install automatically (no interactive terminal for sudo)." >&2 + echo " Please run the following command in a terminal, then retry:" >&2 + echo "" >&2 + echo " $*" >&2 + echo "" >&2 exit 2 } @@ -91,7 +101,7 @@ pkg_install() { info "Installing $pkg via apt..." sudo apt-get update -qq && sudo apt-get install -y -qq "$pkg" else - manual "Run: sudo apt-get install $pkg" + manual "sudo apt-get update && sudo apt-get install -y $pkg" fi ;; dnf) @@ -99,7 +109,7 @@ pkg_install() { info "Installing $pkg via dnf..." sudo dnf install -y "$pkg" else - manual "Run: sudo dnf install $pkg" + manual "sudo dnf install -y $pkg" fi ;; pacman) @@ -107,7 +117,7 @@ pkg_install() { info "Installing $pkg via pacman..." sudo pacman -S --noconfirm "$pkg" else - manual "Run: sudo pacman -S $pkg" + manual "sudo pacman -S $pkg" fi ;; *) @@ -116,6 +126,35 @@ pkg_install() { esac } +# --- Helper: check that a tool needed by this script is available --- +require_tool() { + local tool="$1" + if command -v "$tool" &>/dev/null; then + return 0 + fi + + # Build the install command for the user + local install_cmd="" + case "$PKG_MANAGER" in + brew) install_cmd="brew install $tool" ;; + apt) install_cmd="sudo apt-get update && sudo apt-get install -y $tool" ;; + dnf) install_cmd="sudo dnf install -y $tool" ;; + pacman) install_cmd="sudo pacman -S $tool" ;; + esac + + echo "" >&2 + echo "[MISSING PREREQUISITE] '$tool' is required by install-dep.sh but is not installed." >&2 + if [[ -n "$install_cmd" ]]; then + echo " Please run the following command in a terminal first, then retry:" >&2 + echo "" >&2 + echo " $install_cmd" >&2 + else + echo " Please install '$tool' using your system package manager, then retry." >&2 + fi + echo "" >&2 + exit 2 +} + # --- Helper: download a file --- download() { local url="$1" dest="$2" @@ -124,8 +163,7 @@ download() { elif command -v wget &>/dev/null; then wget -q -O "$dest" "$url" else - fail "Neither curl nor wget available." - return 1 + require_tool "curl" # exits with instructions fi } @@ -204,6 +242,9 @@ install_jadx() { return 0 fi + # Check prerequisites for download-based install + require_tool "unzip" + # Try brew first (cleanest) if [[ "$PKG_MANAGER" == "brew" ]]; then info "Installing jadx via Homebrew..." @@ -319,6 +360,9 @@ install_dex2jar() { return 0 fi + # Check prerequisites for download-based install + require_tool "unzip" + # Try brew if [[ "$PKG_MANAGER" == "brew" ]]; then info "Installing dex2jar via Homebrew..." diff --git a/plugins/android-reverse-engineering/skills/tracker-analysis/SKILL.md b/plugins/android-reverse-engineering/skills/tracker-analysis/SKILL.md new file mode 100644 index 0000000..2cb6878 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/tracker-analysis/SKILL.md @@ -0,0 +1,129 @@ +--- +description: Detect and analyze analytics/tracker SDKs in decompiled Android apps. Identifies Firebase Analytics, Adjust, AppsFlyer, Mixpanel, Amplitude, Segment, Braze, CleverTap, Flurry, and other telemetry SDKs. Extracts initialization patterns, event logging, user identification, consent mechanisms, and data exfiltration endpoints. +trigger: find trackers|tracker analysis|analytics SDK|detect trackers|privacy analysis trackers|telemetry SDK|Firebase Analytics|Adjust SDK|AppsFlyer|Mixpanel|Amplitude|Segment|Braze|CleverTap|Flurry|what trackers|which analytics|tracking SDK +--- + +# Tracker Analysis + +Detect and analyze analytics/tracker SDKs embedded in decompiled Android applications. Produces a structured privacy report covering SDK identification, initialization, event logging, user identification, consent handling, and known data exfiltration endpoints. + +## Prerequisites + +This skill operates on **already decompiled** source code. If the app has not been decompiled yet, use the `android-reverse-engineering` skill first: + +```bash +bash ${CLAUDE_PLUGIN_ROOT}/skills/android-reverse-engineering/scripts/decompile.sh +``` + +The decompiled sources directory (typically `/sources/`) is the input for this skill. + +## Workflow + +### Phase 1: Verify Decompiled Sources + +Confirm that the decompiled source directory exists and contains Java/Kotlin files. + +**Action**: Check the provided directory path. + +- If the user provides a path, verify it exists and contains `.java` or `.kt` files +- If no decompiled sources are available, instruct the user to run `/decompile` first +- Look for `AndroidManifest.xml` in the parent or sibling `resources/` directory — it contains tracker-relevant metadata + +### Phase 2: Broad Detection Sweep + +Run the tracker detection script to identify all tracker SDKs present in the codebase. + +**Action**: Execute the detection script. + +```bash +bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh --all +``` + +For targeted searches on a specific SDK: +```bash +bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh --firebase +bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh --adjust +bash ${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/scripts/find-trackers.sh --mixpanel +``` + +Parse the output to identify which SDKs are present. Empty sections mean that SDK is not used. + +### Phase 3: Deep Semantic Analysis + +For each detected SDK, perform a thorough analysis following the patterns in the reference documents. + +**For each SDK found, trace these in order:** + +1. **Initialization**: How is the SDK initialized? Where is the API key/app ID? Is it hardcoded or loaded from config? + - Read the initialization code and extract credentials + - Check `AndroidManifest.xml` for meta-data entries + - See `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/tracker-init-patterns.md` + +2. **Event Logging**: What events are tracked? Are they standard SDK events or custom? + - List all event names (both string literals and constants) + - Note what data is attached to each event (parameters, properties) + - Look for revenue/purchase tracking + +3. **User Identification**: How are users identified across sessions? + - Find `setUserId`, `identify`, `onUserLogin` calls + - Check what user properties are set (email, name, custom attributes) + - Look for cross-device identification + +4. **Consent & Opt-out**: Does the app implement consent management for this SDK? + - Find opt-out/opt-in calls + - Check for GDPR compliance (forget-me, data deletion) + - Check for consent gating (is analytics enabled only after consent?) + +5. **Data Exfiltration**: Where does the data go? + - Identify endpoints from `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/data-exfiltration-patterns.md` + - Check for custom endpoints or proxy/relay configurations + - Look for batch upload patterns + +### Phase 4: Produce Report + +Generate a structured tracker analysis report. + +**Report format:** + +```markdown +# Tracker Analysis Report — + +## Summary + +| SDK | Version hint | Init location | Events tracked | User ID | Consent | +|-----|-------------|---------------|----------------|---------|---------| +| Firebase Analytics | (from gradle/manifest) | Application.onCreate | 12 custom + standard | setUserId | setConsent ✓ | +| Adjust | — | MainActivity | 3 events | — | gdprForgetMe ✓ | +| ... | | | | | | + +## Detailed Analysis + +### Firebase Analytics + +- **Initialization**: `FirebaseAnalytics.getInstance()` in `MyApp.java:34` +- **API Key / App ID**: `google_app_id` = `1:123456:android:abc` (from strings.xml) +- **Events tracked**: + - `select_content` (standard) — item_id, item_name + - `purchase_complete` (custom) — amount, currency, product_id + - ... +- **User identification**: `setUserId("...")` called in `LoginViewModel.java:78` +- **User properties**: `subscription_tier`, `app_version` +- **Consent**: `setAnalyticsCollectionEnabled(false)` in `ConsentManager.java:22`, toggled based on user preference +- **Endpoints**: `app-measurement.com` (standard, no custom relay) + +### [Next SDK...] + +## Privacy Summary + +- **Total SDKs detected**: N +- **SDKs with consent gating**: list +- **SDKs without consent gating**: list (⚠️) +- **User data shared**: email, user ID, device ID, location, ... +- **Known endpoint domains**: list of all domains data is sent to +``` + +## References + +- `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/tracker-sdk-catalog.md` — Package names, classes, manifest markers for detection +- `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/tracker-init-patterns.md` — Init calls, event logging, user ID, consent per SDK +- `${CLAUDE_PLUGIN_ROOT}/skills/tracker-analysis/references/data-exfiltration-patterns.md` — Endpoints, proxy patterns, batch upload diff --git a/plugins/android-reverse-engineering/skills/tracker-analysis/references/data-exfiltration-patterns.md b/plugins/android-reverse-engineering/skills/tracker-analysis/references/data-exfiltration-patterns.md new file mode 100644 index 0000000..1a92e10 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/tracker-analysis/references/data-exfiltration-patterns.md @@ -0,0 +1,150 @@ +# Data Exfiltration Patterns + +How tracker SDKs send data out of the device — known endpoints, custom configurations, proxy patterns, and batch upload mechanisms. + +## Known Endpoints by SDK + +### Firebase Analytics / Google Analytics +| Endpoint | Purpose | +|---|---| +| `app-measurement.com` | Primary event collection | +| `firebase-settings.crashlytics.com` | Crashlytics config | +| `firebaselogging-pa.googleapis.com` | Event logging | +| `google-analytics.com/collect` | Legacy GA hits | +| `www.googletagmanager.com` | GTM container | + +### Adjust +| Endpoint | Purpose | +|---|---| +| `app.adjust.com` | Event tracking, attribution | +| `app.adjust.io` | Alternative domain | +| `app.adjust.world` | Regional alternative | +| `cdn.adjust.com` | SDK config | +| `gdpr.adjust.com` | GDPR forget-me requests | + +### AppsFlyer +| Endpoint | Purpose | +|---|---| +| `launches.appsflyer.com` | App launch events | +| `events.appsflyer.com` | In-app events | +| `register.appsflyer.com` | Install attribution | +| `inapps.appsflyer.com` | In-app purchase validation | +| `gcdsdk.appsflyer.com` | SDK config | + +### Mixpanel +| Endpoint | Purpose | +|---|---| +| `api.mixpanel.com/track` | Event tracking | +| `api.mixpanel.com/engage` | People profiles | +| `api.mixpanel.com/decide` | Feature flags, A/B tests | +| `api-js.mixpanel.com` | Alternative endpoint | + +### Amplitude +| Endpoint | Purpose | +|---|---| +| `api.amplitude.com/2/httpapi` | Event upload (v2) | +| `api2.amplitude.com/2/httpapi` | Batch event upload | +| `cdn.amplitude.com` | SDK config | +| `regionconfig.amplitude.com` | Region routing | + +### Segment +| Endpoint | Purpose | +|---|---| +| `api.segment.io/v1/t` | Track events | +| `api.segment.io/v1/i` | Identify calls | +| `api.segment.io/v1/batch` | Batch upload | +| `cdn-settings.segment.com` | Workspace settings | + +### Braze +| Endpoint | Purpose | +|---|---| +| `sdk.iad-01.braze.com` | US-01 SDK endpoint (varies per cluster) | +| `sdk.iad-02.braze.com` | US-02 SDK endpoint | +| `sdk.fra-01.braze.eu` | EU-01 SDK endpoint | +| `rest.iad-01.braze.com` | REST API | + +Braze uses cluster-specific endpoints: `sdk.iad-NN.braze.com`, `sdk.fra-NN.braze.eu`. Look for `.setCustomEndpoint()` in `BrazeConfig.Builder`. + +### CleverTap +| Endpoint | Purpose | +|---|---| +| `wzrkt.com` | Primary analytics/events | +| `in.clevertap.com` | India region | +| `eu1.clevertap.com` | EU region | +| `sg1.clevertap.com` | Singapore region | +| `clevertap-prod.com` | Legacy | + +### Flurry +| Endpoint | Purpose | +|---|---| +| `data.flurry.com` | Event data upload | +| `adlog.flurry.com` | Ad analytics | +| `cfg.flurry.com` | SDK configuration | + +## Custom Endpoint Configuration + +Some SDKs allow custom endpoint configuration — important for identifying proxy/relay setups. + +### Grep for custom endpoints +```bash +# Braze custom endpoint +grep -rn 'setCustomEndpoint\|setEndpoint\|setApiUrl' "$SOURCE_DIR" + +# Segment custom host +grep -rn 'connectionFactory\|apiHost\|cdnHost' "$SOURCE_DIR" + +# Mixpanel custom server +grep -rn 'setServerURL\|setDecideUrl\|setEventsUrl' "$SOURCE_DIR" + +# Amplitude custom server +grep -rn 'setServerUrl\|setUseDynamicConfig' "$SOURCE_DIR" + +# Generic proxy patterns +grep -rn 'proxyUrl\|proxyHost\|relayEndpoint\|analyticsProxy\|trackingProxy' "$SOURCE_DIR" +``` + +## Proxy & Relay Patterns + +Apps may route analytics through their own servers to avoid ad blockers or aggregate data. + +### Detection patterns +```bash +# Look for OkHttp interceptors that rewrite analytics URLs +grep -rn 'Interceptor.*analytics\|Interceptor.*tracking\|rewriteUrl.*analytics' "$SOURCE_DIR" + +# Server-side relay: app sends events to its own API, which forwards to tracker +grep -rn '/api/analytics\|/api/events\|/api/tracking\|/v1/collect\|/v1/events' "$SOURCE_DIR" + +# Custom transport layer +grep -rn 'AnalyticsTransport\|EventTransport\|TrackingTransport\|EventDispatcher' "$SOURCE_DIR" +``` + +## Batch Upload Patterns + +Most SDKs batch events and upload periodically or on app backgrounding. + +### Common batch patterns +```bash +# Database/file storage for batched events +grep -rn 'EventDatabase\|event_queue\|analytics_db\|pending_events\|EventStore' "$SOURCE_DIR" + +# Flush triggers +grep -rn '\.flush\(\)\|flushEvents\|flushQueue\|uploadEvents\|sendBatch\|dispatchEvents' "$SOURCE_DIR" + +# Batch size / interval config +grep -rn 'flushInterval\|batchSize\|maxQueueSize\|uploadInterval\|flushAt\|setFlushInterval' "$SOURCE_DIR" +``` + +## Finding the Underlying HTTP Calls + +Tracker SDKs ultimately use standard HTTP mechanisms. To see the actual network calls: + +```bash +# OkHttp client creation inside SDK packages +grep -rn --include="*.java" 'OkHttpClient\|HttpURLConnection' "$SOURCE_DIR"/com/google/firebase/ +grep -rn --include="*.java" 'OkHttpClient\|HttpURLConnection' "$SOURCE_DIR"/com/adjust/ +grep -rn --include="*.java" 'OkHttpClient\|HttpURLConnection' "$SOURCE_DIR"/com/appsflyer/ + +# POST bodies — look for JSON construction near known SDK packages +grep -rn 'toJson\|JSONObject\|JsonWriter\|Gson\|Moshi' "$SOURCE_DIR"/com/mixpanel/ +``` diff --git a/plugins/android-reverse-engineering/skills/tracker-analysis/references/tracker-init-patterns.md b/plugins/android-reverse-engineering/skills/tracker-analysis/references/tracker-init-patterns.md new file mode 100644 index 0000000..2bb096b --- /dev/null +++ b/plugins/android-reverse-engineering/skills/tracker-analysis/references/tracker-init-patterns.md @@ -0,0 +1,305 @@ +# Tracker SDK Initialization & Event Patterns + +Detailed code patterns for each tracker SDK — how they initialize, log events, identify users, and handle consent. + +## Firebase Analytics + +### Initialization +```java +// Typically in Application.onCreate() or Activity +FirebaseAnalytics mFirebaseAnalytics = FirebaseAnalytics.getInstance(this); +// Auto-initialized via google-services.json — may have no explicit init call +FirebaseApp.initializeApp(context); +``` + +**Where to find API key**: `google-services.json` → `project_info.firebase_url` and `client[].api_key`. After decompilation, check `res/values/strings.xml` for `google_app_id`, `firebase_database_url`, `gcm_defaultSenderId`. + +### Event Logging +```java +Bundle params = new Bundle(); +params.putString(FirebaseAnalytics.Param.ITEM_ID, id); +params.putString(FirebaseAnalytics.Param.ITEM_NAME, name); +mFirebaseAnalytics.logEvent(FirebaseAnalytics.Event.SELECT_CONTENT, params); +// Custom events +mFirebaseAnalytics.logEvent("custom_event_name", params); +``` + +**Grep**: `\.logEvent\s*\(` — captures both standard and custom events. + +### User Identification +```java +mFirebaseAnalytics.setUserId("user_id_string"); +mFirebaseAnalytics.setUserProperty("favorite_food", "pizza"); +``` + +### Consent / Opt-out +```java +mFirebaseAnalytics.setAnalyticsCollectionEnabled(false); // kill switch +// Granular consent (v21.0.0+) +mFirebaseAnalytics.setConsent(Map.of( + ConsentType.ANALYTICS_STORAGE, ConsentStatus.DENIED, + ConsentType.AD_STORAGE, ConsentStatus.DENIED +)); +``` + +--- + +## Adjust + +### Initialization +```java +AdjustConfig config = new AdjustConfig(this, "APP_TOKEN", AdjustConfig.ENVIRONMENT_PRODUCTION); +config.setLogLevel(LogLevel.WARN); +config.setOnAttributionChangedListener(attribution -> { /* ... */ }); +Adjust.onCreate(config); +``` + +**Where to find API key**: First argument to `AdjustConfig()` constructor — the app token string. + +### Event Tracking +```java +AdjustEvent event = new AdjustEvent("EVENT_TOKEN"); +event.setRevenue(1.0, "USD"); +event.addCallbackParameter("key", "value"); +Adjust.trackEvent(event); +``` + +### Attribution +```java +config.setOnAttributionChangedListener(attribution -> { + attribution.trackerToken; + attribution.trackerName; + attribution.network; + attribution.campaign; +}); +``` + +### Consent +```java +Adjust.gdprForgetMe(context); // GDPR right to be forgotten +Adjust.disableThirdPartySharing(context); +Adjust.trackThirdPartySharing(thirdPartySharing); +``` + +--- + +## AppsFlyer + +### Initialization +```java +AppsFlyerLib.getInstance().init("AF_DEV_KEY", conversionListener, this); +AppsFlyerLib.getInstance().start(this); +``` + +**Where to find API key**: First argument to `.init()` — the dev key string. + +### Event Tracking +```java +Map eventValues = new HashMap<>(); +eventValues.put(AFInAppEventParameterName.REVENUE, 1200); +eventValues.put(AFInAppEventParameterName.CURRENCY, "USD"); +AppsFlyerLib.getInstance().logEvent(context, AFInAppEventType.PURCHASE, eventValues); +``` + +### User Identity +```java +AppsFlyerLib.getInstance().setCustomerUserId("user_id"); +AppsFlyerLib.getInstance().setAdditionalData(customData); +``` + +### Consent +```java +AppsFlyerLib.getInstance().anonymizeUser(true); +AppsFlyerLib.getInstance().stop(true, context); // full opt-out +``` + +--- + +## Mixpanel + +### Initialization +```java +MixpanelAPI mixpanel = MixpanelAPI.getInstance(context, "MIXPANEL_TOKEN"); +``` + +**Where to find token**: First string argument to `getInstance()`. + +### Event Tracking +```java +JSONObject props = new JSONObject(); +props.put("source", "search"); +mixpanel.track("Button Clicked", props); +mixpanel.timeEvent("Time On Page"); // starts timer +mixpanel.track("Time On Page"); // ends timer, records duration +``` + +### User Identity +```java +mixpanel.identify("user_id"); +mixpanel.alias("new_id", "existing_id"); +mixpanel.getPeople().set("$email", "user@example.com"); +mixpanel.getPeople().increment("login_count", 1); +``` + +### Consent +```java +mixpanel.optOutTracking(); // full opt-out +mixpanel.optInTracking(); // re-enable +``` + +--- + +## Amplitude + +### Initialization +```java +// v1 (legacy) +Amplitude.getInstance().initialize(context, "API_KEY").enableForegroundTracking(application); +// v2 (kotlin-first) +val amplitude = Amplitude(Configuration(apiKey = "API_KEY", context = applicationContext)) +``` + +**Where to find API key**: Argument to `initialize()` or `Configuration()`. + +### Event Tracking +```java +Amplitude.getInstance().logEvent("button_clicked"); +Amplitude.getInstance().logEvent("purchase", new JSONObject().put("price", 9.99)); +Amplitude.getInstance().logRevenue(new Revenue().setProductId("product").setPrice(9.99)); +``` + +### User Identity +```java +Amplitude.getInstance().setUserId("user_id"); +Identify identify = new Identify().set("plan", "premium").add("login_count", 1); +Amplitude.getInstance().identify(identify); +``` + +### Consent +```java +Amplitude.getInstance().setOptOut(true); +Amplitude.getInstance().enableCoppaControl(); // COPPA +``` + +--- + +## Segment + +### Initialization +```java +Analytics analytics = new Analytics.Builder(context, "WRITE_KEY") + .trackApplicationLifecycleEvents() + .build(); +Analytics.setSingletonInstance(analytics); +``` + +**Where to find write key**: Second argument to `Analytics.Builder()`. + +### Event Tracking +```java +Analytics.with(context).track("Item Purchased", + new Properties().putValue("item", "sword").putValue("revenue", 9.99)); +Analytics.with(context).screen("Home", new Properties().putValue("tab", "feed")); +``` + +### User Identity +```java +Analytics.with(context).identify("user_id", + new Traits().putName("John").putEmail("john@example.com"), null); +Analytics.with(context).alias("new_id"); +``` + +### Consent +```java +Analytics.with(context).optOut(true); +``` + +--- + +## Braze + +### Initialization +```java +BrazeConfig brazeConfig = new BrazeConfig.Builder() + .setApiKey("API_KEY") + .setCustomEndpoint("sdk.iad-01.braze.com") + .build(); +Braze.configure(context, brazeConfig); +``` + +**Where to find API key**: `.setApiKey()` and `.setCustomEndpoint()` in the config builder. + +### Event & Purchase Logging +```java +Braze.getInstance(context).logCustomEvent("event_name", new BrazeProperties().addProperty("key", "value")); +Braze.getInstance(context).logPurchase("product_id", "USD", new BigDecimal("9.99")); +``` + +### User Identity +```java +Braze.getInstance(context).changeUser("user_id"); +Braze.getInstance(context).getCurrentUser().setEmail("user@example.com"); +Braze.getInstance(context).getCurrentUser().setCustomUserAttribute("attr", "value"); +``` + +--- + +## CleverTap + +### Initialization +```java +CleverTapAPI clevertapDefaultInstance = CleverTapAPI.getDefaultInstance(applicationContext); +// Auto-init via manifest meta-data: CLEVERTAP_ACCOUNT_ID, CLEVERTAP_TOKEN +ActivityLifecycleCallback.register(application); +``` + +**Where to find credentials**: Manifest `` tags: `CLEVERTAP_ACCOUNT_ID`, `CLEVERTAP_TOKEN`. + +### Event Tracking +```java +HashMap props = new HashMap<>(); +props.put("Product Name", "Widget"); +clevertapDefaultInstance.pushEvent("Product Viewed", props); +// Charged event (purchase) +clevertapDefaultInstance.pushChargedEvent(chargeDetails, items); +``` + +### User Identity +```java +HashMap profile = new HashMap<>(); +profile.put("Name", "John"); +profile.put("Email", "john@example.com"); +profile.put("Identity", "user_id"); +clevertapDefaultInstance.onUserLogin(profile); +clevertapDefaultInstance.pushProfile(profile); +``` + +--- + +## Flurry + +### Initialization +```java +new FlurryAgent.Builder() + .withLogEnabled(true) + .withCaptureUncaughtExceptions(true) + .build(context, "FLURRY_API_KEY"); +``` + +**Where to find API key**: Last argument to `.build()`. + +### Event Tracking +```java +FlurryAgent.logEvent("event_name"); +FlurryAgent.logEvent("event_name", params); // params is Map +FlurryAgent.logEvent("timed_event", params, true); // timed event +FlurryAgent.endTimedEvent("timed_event"); +FlurryAgent.logPayment("product", "id", 1, 9.99, "USD", "txn_id", null); +``` + +### User Identity +```java +FlurryAgent.setUserId("user_id"); +FlurryAgent.setAge(25); +FlurryAgent.setGender(Constants.MALE); +``` diff --git a/plugins/android-reverse-engineering/skills/tracker-analysis/references/tracker-sdk-catalog.md b/plugins/android-reverse-engineering/skills/tracker-analysis/references/tracker-sdk-catalog.md new file mode 100644 index 0000000..4292875 --- /dev/null +++ b/plugins/android-reverse-engineering/skills/tracker-analysis/references/tracker-sdk-catalog.md @@ -0,0 +1,93 @@ +# Tracker SDK Catalog + +Quick-reference for detecting analytics/tracker SDKs in decompiled Android apps. + +## Firebase Analytics + +| Field | Value | +|---|---| +| **Package** | `com.google.firebase.analytics` | +| **Key classes** | `FirebaseAnalytics`, `AppMeasurement`, `AppMeasurementService` | +| **Manifest markers** | ``, `` | +| **Gradle** | `com.google.firebase:firebase-analytics` | +| **Grep detection** | `FirebaseAnalytics\.getInstance\|AppMeasurement\|app-measurement\.com` | + +## Adjust + +| Field | Value | +|---|---| +| **Package** | `com.adjust.sdk` | +| **Key classes** | `Adjust`, `AdjustConfig`, `AdjustEvent`, `AdjustAttribution` | +| **Manifest markers** | `` | +| **Gradle** | `com.adjust.sdk:adjust-android` | +| **Grep detection** | `AdjustConfig\|AdjustEvent\|Adjust\.trackEvent\|Adjust\.onCreate` | + +## AppsFlyer + +| Field | Value | +|---|---| +| **Package** | `com.appsflyer` | +| **Key classes** | `AppsFlyerLib`, `AppsFlyerConversionListener`, `AFInAppEventType` | +| **Manifest markers** | ``, `` | +| **Gradle** | `com.appsflyer:af-android-sdk` | +| **Grep detection** | `AppsFlyerLib\.getInstance\|AFInAppEventType\|appsflyer\.com` | + +## Mixpanel + +| Field | Value | +|---|---| +| **Package** | `com.mixpanel.android` | +| **Key classes** | `MixpanelAPI`, `AnalyticsMessages`, `DecideMessages` | +| **Manifest markers** | `` | +| **Gradle** | `com.mixpanel.android:mixpanel-android` | +| **Grep detection** | `MixpanelAPI\.getInstance\|com\.mixpanel\.android\|api\.mixpanel\.com` | + +## Amplitude + +| Field | Value | +|---|---| +| **Package** | `com.amplitude.api` (v2), `com.amplitude.android` (v1) | +| **Key classes** | `Amplitude`, `AmplitudeClient`, `Revenue`, `Identify` | +| **Manifest markers** | (none specific — uses standard Android components) | +| **Gradle** | `com.amplitude:analytics-android` (v2), `com.amplitude:android-sdk` (v1) | +| **Grep detection** | `Amplitude\.getInstance\|AmplitudeClient\|api\.amplitude\.com` | + +## Segment + +| Field | Value | +|---|---| +| **Package** | `com.segment.analytics` | +| **Key classes** | `Analytics`, `TrackPayload`, `IdentifyPayload`, `ScreenPayload`, `Traits` | +| **Manifest markers** | (none specific) | +| **Gradle** | `com.segment.analytics.android:analytics` | +| **Grep detection** | `Analytics\.with\|com\.segment\.analytics\|api\.segment\.io` | + +## Braze (formerly Appboy) + +| Field | Value | +|---|---| +| **Package** | `com.braze` (v4+), `com.appboy` (legacy) | +| **Key classes** | `Braze`, `BrazeConfig`, `BrazeUser`, `BrazeProperties`; legacy: `Appboy`, `AppboyConfig` | +| **Manifest markers** | ``, `com.braze.BrazeActivityLifecycleCallbackListener` | +| **Gradle** | `com.braze:android-sdk-ui` | +| **Grep detection** | `Braze\.configure\|BrazeConfig\|logCustomEvent\|com\.braze\.\|com\.appboy\.` | + +## CleverTap + +| Field | Value | +|---|---| +| **Package** | `com.clevertap.android.sdk` | +| **Key classes** | `CleverTapAPI`, `ActivityLifecycleCallback` | +| **Manifest markers** | ``, `` | +| **Gradle** | `com.clevertap.android:clevertap-android-sdk` | +| **Grep detection** | `CleverTapAPI\.getDefaultInstance\|pushEvent\|onUserLogin\|wzrkt\.com` | + +## Flurry + +| Field | Value | +|---|---| +| **Package** | `com.flurry.android` | +| **Key classes** | `FlurryAgent`, `FlurryConfig`, `FlurryPerformance` | +| **Manifest markers** | `` | +| **Gradle** | `com.flurry.android:analytics` | +| **Grep detection** | `FlurryAgent\.logEvent\|FlurryAgent\.Builder\|data\.flurry\.com` | diff --git a/plugins/android-reverse-engineering/skills/tracker-analysis/scripts/find-trackers.sh b/plugins/android-reverse-engineering/skills/tracker-analysis/scripts/find-trackers.sh new file mode 100755 index 0000000..62cf8fd --- /dev/null +++ b/plugins/android-reverse-engineering/skills/tracker-analysis/scripts/find-trackers.sh @@ -0,0 +1,231 @@ +#!/usr/bin/env bash +# find-trackers.sh — Search decompiled source for tracker/analytics SDK usage +set -euo pipefail + +usage() { + cat < [OPTIONS] + +Search decompiled Java/Kotlin source for analytics and tracker SDK usage. + +Arguments: + Path to the decompiled sources directory + +Options: + --firebase Search only for Firebase Analytics + --adjust Search only for Adjust SDK + --appsflyer Search only for AppsFlyer SDK + --mixpanel Search only for Mixpanel SDK + --amplitude Search only for Amplitude SDK + --segment Search only for Segment SDK + --braze Search only for Braze/Appboy SDK + --clevertap Search only for CleverTap SDK + --flurry Search only for Flurry SDK + --generic Search only for generic cross-SDK patterns + --endpoints Search only for known tracker endpoints + --manifest Search only for AndroidManifest.xml markers + --all Search all patterns (default) + -h, --help Show this help message + +Output: + Results are printed as file:line:match for easy navigation. +EOF + exit 0 +} + +SOURCE_DIR="" +SEARCH_FIREBASE=false +SEARCH_ADJUST=false +SEARCH_APPSFLYER=false +SEARCH_MIXPANEL=false +SEARCH_AMPLITUDE=false +SEARCH_SEGMENT=false +SEARCH_BRAZE=false +SEARCH_CLEVERTAP=false +SEARCH_FLURRY=false +SEARCH_GENERIC=false +SEARCH_ENDPOINTS=false +SEARCH_MANIFEST=false +SEARCH_ALL=true + +while [[ $# -gt 0 ]]; do + case "$1" in + --firebase) SEARCH_FIREBASE=true; SEARCH_ALL=false; shift ;; + --adjust) SEARCH_ADJUST=true; SEARCH_ALL=false; shift ;; + --appsflyer) SEARCH_APPSFLYER=true; SEARCH_ALL=false; shift ;; + --mixpanel) SEARCH_MIXPANEL=true; SEARCH_ALL=false; shift ;; + --amplitude) SEARCH_AMPLITUDE=true; SEARCH_ALL=false; shift ;; + --segment) SEARCH_SEGMENT=true; SEARCH_ALL=false; shift ;; + --braze) SEARCH_BRAZE=true; SEARCH_ALL=false; shift ;; + --clevertap) SEARCH_CLEVERTAP=true; SEARCH_ALL=false; shift ;; + --flurry) SEARCH_FLURRY=true; SEARCH_ALL=false; shift ;; + --generic) SEARCH_GENERIC=true; SEARCH_ALL=false; shift ;; + --endpoints) SEARCH_ENDPOINTS=true; SEARCH_ALL=false; shift ;; + --manifest) SEARCH_MANIFEST=true; SEARCH_ALL=false; shift ;; + --all) SEARCH_ALL=true; shift ;; + -h|--help) usage ;; + -*) echo "Error: Unknown option $1" >&2; usage ;; + *) SOURCE_DIR="$1"; shift ;; + esac +done + +if [[ -z "$SOURCE_DIR" ]]; then + echo "Error: No source directory specified." >&2 + usage +fi + +if [[ ! -d "$SOURCE_DIR" ]]; then + echo "Error: Directory not found: $SOURCE_DIR" >&2 + exit 1 +fi + +GREP_OPTS="-rn --include=*.java --include=*.kt" + +section() { + echo + echo "==== $1 ====" + echo +} + +run_grep() { + local pattern="$1" + # shellcheck disable=SC2086 + grep $GREP_OPTS -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true +} + +run_grep_xml() { + local pattern="$1" + grep -rn --include="*.xml" -E "$pattern" "$SOURCE_DIR" 2>/dev/null || true +} + +# --- Firebase Analytics --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_FIREBASE" == true ]]; then + section "Firebase Analytics — Initialization" + run_grep '(FirebaseAnalytics\.getInstance|FirebaseAnalytics\.newInstance|FirebaseApp\.initializeApp)' + section "Firebase Analytics — Event Logging" + run_grep '(\.logEvent\s*\(|\.setDefaultEventParameters|FirebaseAnalytics\.Event\.|FirebaseAnalytics\.Param\.)' + section "Firebase Analytics — User Identity" + run_grep '(\.setUserId\s*\(|\.setUserProperty\s*\(|\.setAnalyticsCollectionEnabled)' + section "Firebase Analytics — Consent" + run_grep '(\.setConsent\s*\(|ConsentType\.|ConsentStatus\.|setAnalyticsCollectionEnabled\s*\(\s*false)' +fi + +# --- Adjust --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_ADJUST" == true ]]; then + section "Adjust — Initialization" + run_grep '(AdjustConfig\s*\(|Adjust\.onCreate\s*\(|Adjust\.create\s*\(|AdjustConfig\.ENVIRONMENT_)' + section "Adjust — Event Tracking" + run_grep '(AdjustEvent\s*\(|Adjust\.trackEvent\s*\(|\.setRevenue\s*\(|\.setCallbackId\s*\()' + section "Adjust — Attribution & User" + run_grep '(\.setOnAttributionChangedListener|AdjustAttribution|Adjust\.addSessionCallbackParameter|Adjust\.getAdid\s*\()' +fi + +# --- AppsFlyer --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_APPSFLYER" == true ]]; then + section "AppsFlyer — Initialization" + run_grep '(AppsFlyerLib\.getInstance|AppsFlyerConversionListener|\.init\s*\(.*AF_DEV_KEY|\.start\s*\()' + section "AppsFlyer — Event Tracking" + run_grep '(\.logEvent\s*\(.*AppsFlyerLib|AFInAppEventType\.|AFInAppEventParameterName\.|\.trackEvent\s*\()' + section "AppsFlyer — User Identity" + run_grep '(\.setCustomerUserId\s*\(|\.setCustomerIdAndLogSession|\.getAppsFlyerUID\s*\(|\.setAdditionalData\s*\()' +fi + +# --- Mixpanel --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_MIXPANEL" == true ]]; then + section "Mixpanel — Initialization" + run_grep '(MixpanelAPI\.getInstance\s*\(|MixpanelAPI\.init\s*\()' + section "Mixpanel — Event Tracking" + run_grep '(\.track\s*\(|\.trackMap\s*\(|\.timeEvent\s*\(|\.registerSuperProperties\s*\()' + section "Mixpanel — User Identity & Profile" + run_grep '(\.identify\s*\(|\.alias\s*\(|\.getPeople\s*\(|\.getDistinctId\s*\(|\.set\s*\(|\.increment\s*\()' +fi + +# --- Amplitude --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_AMPLITUDE" == true ]]; then + section "Amplitude — Initialization" + run_grep '(Amplitude\.getInstance\s*\(|AmplitudeClient|\.initialize\s*\(.*amplitude|amplitude\.init\s*\()' + section "Amplitude — Event Tracking" + run_grep '(\.logEvent\s*\(|\.logRevenue\s*\(|\.setEventProperties|EventOptions\s*\()' + section "Amplitude — User Identity" + run_grep '(\.setUserId\s*\(|\.setUserProperties\s*\(|\.setDeviceId\s*\(|\.setGroup\s*\(|Identify\s*\(\))' +fi + +# --- Segment --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_SEGMENT" == true ]]; then + section "Segment — Initialization" + run_grep '(Analytics\.with\s*\(|Analytics\.Builder|Analytics\.setSingletonInstance|com\.segment\.analytics)' + section "Segment — Event Tracking" + run_grep '(\.track\s*\(|\.screen\s*\(|\.group\s*\(|TrackPayload|ScreenPayload)' + section "Segment — User Identity" + run_grep '(\.identify\s*\(|\.alias\s*\(|Traits\s*\(\)|IdentifyPayload)' +fi + +# --- Braze (formerly Appboy) --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_BRAZE" == true ]]; then + section "Braze — Initialization" + run_grep '(Braze\.configure\s*\(|BrazeConfig\.Builder|Appboy\.configure\s*\(|AppboyConfig\.Builder)' + section "Braze — Event & Purchase Logging" + run_grep '(\.logCustomEvent\s*\(|\.logPurchase\s*\(|BrazeProperties\s*\(|\.logClick\s*\()' + section "Braze — User Identity" + run_grep '(\.getCurrentUser\s*\(|\.changeUser\s*\(|BrazeUser|\.setEmail\s*\(|\.setCustomUserAttribute\s*\()' +fi + +# --- CleverTap --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_CLEVERTAP" == true ]]; then + section "CleverTap — Initialization" + run_grep '(CleverTapAPI\.getDefaultInstance|CleverTapAPI\.changeCredentials|ActivityLifecycleCallback\.register)' + section "CleverTap — Event Tracking" + run_grep '(\.pushEvent\s*\(|\.recordEvent\s*\(|\.pushChargedEvent\s*\()' + section "CleverTap — User Identity & Profile" + run_grep '(\.pushProfile\s*\(|\.onUserLogin\s*\(|\.profilePush\s*\(|\.getCleverTapID\s*\()' +fi + +# --- Flurry --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_FLURRY" == true ]]; then + section "Flurry — Initialization" + run_grep '(FlurryAgent\.Builder|FlurryAgent\.init\s*\(|FlurryAgent\.onStartSession\s*\(|\.withLogEnabled\s*\()' + section "Flurry — Event Tracking" + run_grep '(FlurryAgent\.logEvent\s*\(|FlurryAgent\.endTimedEvent|FlurryAgent\.logPayment\s*\()' + section "Flurry — User Identity" + run_grep '(FlurryAgent\.setUserId\s*\(|FlurryAgent\.setAge\s*\(|FlurryAgent\.setGender\s*\()' +fi + +# --- Generic Cross-SDK Patterns --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_GENERIC" == true ]]; then + section "Generic — Event Tracking Methods" + run_grep '\.(trackEvent|logEvent|recordEvent|pushEvent|logCustomEvent)\s*\(' + section "Generic — User Identification" + run_grep '\.(setUserId|setCustomerUserId|identify|setDistinctId|onUserLogin|changeUser)\s*\(' + section "Generic — Analytics Collection Toggle" + run_grep '(setAnalyticsCollectionEnabled|setOptOut|setDataCollectionEnabled|setSendingEnabled|optOut|gdprForgetMe)\s*\(' +fi + +# --- Known Tracker Endpoints --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_ENDPOINTS" == true ]]; then + section "Known Tracker Endpoints" + run_grep '"https?://[^"]*(app-measurement\.com|firebase-settings\.crashlytics\.com|firebaselogging|google-analytics\.com/collect)' + run_grep '"https?://[^"]*(app\.adjust\.(com|io|world)|cdn\.adjust\.com)' + run_grep '"https?://[^"]*(appsflyer\.com|appsflyersdk\.com|onelink\.me)' + run_grep '"https?://[^"]*(api\.mixpanel\.com|decide\.mixpanel\.com|api-js\.mixpanel\.com)' + run_grep '"https?://[^"]*(api\.amplitude\.com|api2\.amplitude\.com|cdn\.amplitude\.com)' + run_grep '"https?://[^"]*(api\.segment\.(io|com)|cdn-settings\.segment\.com)' + run_grep '"https?://[^"]*(sdk\.iad-[0-9]+\.braze\.com|rest\.iad-[0-9]+\.braze\.com|appboy\.com)' + run_grep '"https?://[^"]*(wzrkt\.com|clevertap-prod\.com|in\.clevertap\.com)' + run_grep '"https?://[^"]*(data\.flurry\.com|flurry\.com/sdk)' +fi + +# --- AndroidManifest.xml Markers --- +if [[ "$SEARCH_ALL" == true || "$SEARCH_MANIFEST" == true ]]; then + section "AndroidManifest — Tracker Services & Receivers" + run_grep_xml '(com\.google\.firebase\.analytics|com\.google\.android\.gms\.measurement|com\.google\.firebase\.iid)' + run_grep_xml '(com\.adjust\.|com\.appsflyer\.|com\.mixpanel\.|com\.amplitude\.|com\.segment\.)' + run_grep_xml '(com\.braze\.|com\.appboy\.|com\.clevertap\.|com\.flurry\.)' + section "AndroidManifest — Tracker Permissions" + run_grep_xml '(AD_ID|com\.google\.android\.gms\.permission\.AD_ID|ACCESS_FINE_LOCATION|ACCESS_COARSE_LOCATION|GET_ACCOUNTS)' + section "AndroidManifest — Tracker Meta-data" + run_grep_xml '(com\.google\.firebase\.analytics|firebase_analytics_collection_enabled|google_analytics_adid_collection_enabled)' + run_grep_xml '(ADJUST_|APPSFLYER_|MIXPANEL_|AMPLITUDE_|SEGMENT_|BRAZE_|CLEVERTAP_|FLURRY_)' +fi + +echo +echo "=== Search complete ==="