Mobile Apps
How Noir extracts Android and iOS deep-link entry points and links them to the code that handles them.
Noir reports the entry points of an Android or iOS app as endpoints. These are the deep links and exported components the app exposes to the outside world. Noir also links each one to the source code that handles it.
Deep links are a classic mobile attack surface. An externally supplied URL or intent flows into the app and can reach a WebView, an SQL query, or another component.
There is no separate flag. Scan the app's source tree as usual, and Noir picks up the files listed below:
noir scan path/to/app
What Noir parses
| Platform | Source file | What it yields |
|---|---|---|
| Android | AndroidManifest.xml |
custom URL-scheme deep links, exported intent components, exported ContentProviders, verified App Links |
| Android | res/values/strings.xml |
resolves @string/ references used in schemes / hosts / paths |
| Android | build.gradle / build.gradle.kts |
resolves ${applicationId} and custom manifestPlaceholders used in package / component names / schemes / hosts; supplies the package when the manifest has no package attribute |
| Android | res/navigation/*.xml |
Jetpack Navigation <deepLink app:uri="..."> deep links; the owning destination becomes the handling component |
| iOS | Info.plist (any *.plist declaring CFBundleURLTypes) |
custom URL schemes; also resolves $(VAR) / ${VAR} placeholders from .xcconfig / project.pbxproj |
| iOS | *.entitlements |
associated-domains applinks: universal links and appclips: App Clip launch URLs |
| Android | /.well-known/assetlinks.json |
server-side App Links association (Digital Asset Links) |
| iOS | apple-app-site-association |
server-side universal-link paths / components patterns |
The first six rows are the client side, declared in the app bundle. The last two are the server side: well-known files a host publishes so the OS opens the app for its URLs. Both sides of this association produce universal-link endpoints with the same output model.
Endpoint model
Every mobile entry point is an endpoint with method = "GET". The protocol field says what kind of entry point it is:
| protocol | meaning | example URL |
|---|---|---|
mobile-scheme |
custom URL-scheme deep link | myapp://complex/:id |
android-intent |
exported intent component reachable by intent: a data-less action filter, or no intent-filter at all (explicit-intent only) | intent://com.example.app/.SyncService |
android-provider |
exported ContentProvider reachable by another app through a content://authority URI |
content://com.example.app.provider |
universal-link |
verified Android App Link / iOS universal link, or a server-side assetlinks.json / apple-app-site-association pattern |
https://app.example.com/complex/:id, /buy/* |
In plain output, each kind gets a prefix: SCHEME, INTENT, PROVIDER or UNIVERSAL.
The handling component, intent action and category, host, and package go in a per-endpoint metadata map. JSON and YAML output include it. Ordinary HTTP endpoints don't have one.
SCHEME myapp://complex/:id
○ path: id
○ via: .DeepLinkActivity
○ action: android.intent.action.VIEW
○ category: android.intent.category.DEFAULT
○ host: complex
○ package: com.example.myapp
Linking to handler code
When the handler source is present in the scan, Noir connects each deep-link endpoint to the code that processes it.
Android
- The manifest component (e.g.
.DeepLinkActivity) is resolved to its.kt/.javafile. - Its intent-handling methods (
onCreate,onNewIntent,onReceive, …) contribute 1-hop callees, that is, the functions they call directly. - The inputs they read become parameters:
uri.getQueryParameter("q")→ aqueryparam (added to the URL), and theget*Extrafamily → anextraparam.
iOS
Deep links are dispatched centrally, so Noir discovers the handlers and attaches them by kind:
.onOpenURL/application(_:open:)/scene(_:openURLContexts:)→ custom schemes.- The
userActivityhandlers → universal links. URLQueryItemreads becomequeryparams.
With --ai-context, the handler body feeds Noir's source / sink / guard inference. A deep link that flows into a WebView load or another sink then shows up with the taint path (the route the input takes to that sink) attached.
Output behavior
| Output | Mobile endpoints |
|---|---|
| Structured inventory (JSON, JSONL, YAML, TOML, SARIF, Markdown table, mermaid, HTML) and the Elasticsearch / webhook exports | Included, because they are part of the app's surface |
| HTTP-shaped output and delivery (cURL, HTTPie, PowerShell, OpenAPI 2.0 / 3.0, Postman, and active probe / proxy delivery) | Left out. An app URL is something you open, not an HTTP request you send |
-f adb (Android) and -f simctl (iOS) |
Emitted as ready-to-run device commands that launch them |
Notes and limitations
General
- Source code must be present for handler linkage. A manifest- or plist-only scan still yields the endpoints, just without callees, params, or AI context.
Android
- gradle placeholders: Noir reads the nearest
build.gradle(.kts)above the manifest. When the same placeholder is declared more than once (e.g. abuildTypesoverride), the first declaration (by conventiondefaultConfig) wins. Variant-specific values are not modeled. A placeholder with no gradle value stays verbatim in the URL, and the endpoint is taggedunresolved. - Jetpack Navigation: scheme-less URIs (which match both http and https at runtime) are emitted once under
https.{arg}segments become:argpath params,?key={arg}query placeholders becomequeryparams, and a trailing.*wildcard keeps the literal prefix. - Components without an intent-filter: an
android:exported="true"component with no intent-filter is reported as an explicit-intent surface (android-intent, withexplicit/exported/component_typein metadata). An explicit intent naming the component still reaches it, even with no action, category, or data URI.- A filter-less component that is not exported, or is
android:enabled="false", is not reported. - A guarding
android:permissionis recorded in metadata but does not suppress the surface:normal/dangerouspermissions remain obtainable by another app.
- A filter-less component that is not exported, or is
- ContentProviders: an exported
<provider>is reported as anandroid-providersurface, with onecontent://authorityendpoint perandroid:authoritiesentry.- The provider class links as the handler (
via), so itsquery/insert/openFilemethods contribute callees. A raw SQL or file sink shows up in AI context. - Permissions are recorded in metadata and never suppress the surface:
android:readPermission/android:writePermission/android:permission,android:grantUriPermissions(or a<grant-uri-permission>child), and the presence of<path-permission>overrides. - Only an explicitly exported provider is reported (a provider defaults to not-exported on modern SDKs).
- The effective per-path permission of a
<path-permission>is flagged but not resolved.
- The provider class links as the handler (
iOS
- Binary (compiled)
Info.plistfiles are skipped. Source-repo XML plists are parsed. - Noir also picks up routes written in Swift code for the app's own scheme: router enums compared with
url.host == SomeEnum.case.rawValue(each case becomes a route), and hardcodedscheme://host/pathstring literals.
Server-side association files
assetlinks.jsononly declares the package association (no paths), so it yields a single/*endpoint per app.apple-app-site-association(AASA) paths (/buy/*,NOT /private/*) andcomponentsare emitted individually. AASA exclusions are taggedexcluded.- Only the plain-JSON form of AASA is parsed. CMS-signed AASA files (older apps) are skipped.