Skip to main content

asc_engine/
manifest.rs

1//! Structured `AndroidManifest.xml` summary: the attack surface an audit
2//! starts from (exported components, permissions, providers, deep links,
3//! application flags, network security config).
4//!
5//! Every attribute that is a resource reference is resolved through
6//! `resources.arsc` as a device on the newest SDK with no locale would see it
7//! ([`asc_arsc::Config::any_device`]); when a referenced boolean differs
8//! between configurations (e.g. `exported="@bool/x"` with a `values-v31`
9//! override) every variant is reported next to the value.
10
11use crate::resources::{plain_value, resolve_simple};
12use crate::text::value;
13use crate::{Engine, Result};
14use asc_arsc::{EntryValue, ResTable, TYPE_INT_BOOLEAN, TYPE_STRING};
15use asc_axml::{Attribute, Element};
16use serde_json::{Value as Json, json};
17
18/// Android 12 (API 31) requires an explicit `android:exported` on every
19/// component with an intent filter; before it such components were exported.
20const SDK_EXPLICIT_EXPORTED: i64 = 31;
21/// Before API 17 providers were exported unless they said otherwise.
22const SDK_PROVIDER_PRIVATE: i64 = 17;
23/// From API 28 cleartext traffic is off unless enabled.
24const SDK_CLEARTEXT_OFF: i64 = 28;
25const FILE_PROVIDER_PATHS: &str = "android.support.FILE_PROVIDER_PATHS";
26
27#[derive(Debug, Clone, Default, PartialEq)]
28pub struct ManifestSummary {
29    pub package: String,
30    pub version_code: Option<String>,
31    pub version_name: Option<String>,
32    pub min_sdk: Option<i64>,
33    pub target_sdk: Option<i64>,
34    pub shared_user_id: Option<String>,
35    pub application: Application,
36    pub uses_permissions: Vec<String>,
37    pub permissions: Vec<Permission>,
38    pub components: Vec<Component>,
39    /// Decoded `networkSecurityConfig`, one record per line.
40    pub network_security: Vec<String>,
41    /// Attributes that could not be resolved and similar notes.
42    pub warnings: Vec<String>,
43}
44
45#[derive(Debug, Clone, Default, PartialEq)]
46pub struct Application {
47    pub class: Option<String>,
48    pub debuggable: bool,
49    pub allow_backup: bool,
50    pub test_only: bool,
51    pub uses_cleartext_traffic: Option<bool>,
52    /// Whether cleartext HTTP is allowed by default, and what decided it.
53    pub cleartext_effective: (bool, &'static str),
54    /// Path of the network security config XML (`res/xml/...`).
55    pub network_security_config: Option<String>,
56    pub permission: Option<String>,
57    pub full_backup_content: Option<String>,
58    pub data_extraction_rules: Option<String>,
59}
60
61#[derive(Debug, Clone, Default, PartialEq)]
62pub struct Permission {
63    pub name: String,
64    pub protection_level: String,
65}
66
67#[derive(Debug, Clone, Default, PartialEq)]
68pub struct Component {
69    /// `activity`, `activity-alias`, `service`, `receiver` or `provider`.
70    pub kind: String,
71    /// Fully qualified class name (for an alias: the alias name).
72    pub class: String,
73    /// `targetActivity` of an alias, fully qualified.
74    pub target: Option<String>,
75    pub exported: bool,
76    /// `explicit`, `intent-filter` (implicit, targetSdk < 31), `provider-default`
77    /// (targetSdk < 17) or `default`, plus `@ref` variants when the value differs
78    /// between configurations.
79    pub exported_reason: String,
80    pub enabled: bool,
81    /// Own permission, or the application's when the component sets none.
82    pub permission: Option<String>,
83    pub permission_from_app: bool,
84    pub process: Option<String>,
85    pub authorities: Vec<String>,
86    pub grant_uri_permissions: bool,
87    pub read_permission: Option<String>,
88    pub write_permission: Option<String>,
89    pub intent_filters: Vec<IntentFilter>,
90    pub meta_data: Vec<(String, String)>,
91    /// FileProvider path entries (`files-path name=x path=y`).
92    pub provider_paths: Vec<String>,
93}
94
95#[derive(Debug, Clone, Default, PartialEq)]
96pub struct IntentFilter {
97    pub actions: Vec<String>,
98    pub categories: Vec<String>,
99    pub data: Vec<DataSpec>,
100}
101
102#[derive(Debug, Clone, Default, PartialEq)]
103pub struct DataSpec {
104    pub scheme: Option<String>,
105    pub host: Option<String>,
106    pub port: Option<String>,
107    /// `path`, `pathPrefix`, `pathPattern`, `pathAdvancedPattern` or
108    /// `pathSuffix`, with its value.
109    pub path: Option<(String, String)>,
110    pub mime_type: Option<String>,
111}
112
113impl IntentFilter {
114    /// Deep link URL patterns of a `VIEW` filter: every scheme combined with
115    /// every host and path, as Android matches them (`*` for absent parts).
116    pub fn deep_links(&self) -> Vec<String> {
117        if !self.actions.iter().any(|a| a == "android.intent.action.VIEW") {
118            return Vec::new();
119        }
120        let pick = |f: fn(&DataSpec) -> Option<String>| -> Vec<String> {
121            let mut v: Vec<String> = self.data.iter().filter_map(f).collect();
122            v.dedup();
123            v
124        };
125        let schemes = pick(|d| d.scheme.clone());
126        let hosts = pick(|d| {
127            d.host.clone().map(|h| match &d.port {
128                Some(p) => format!("{h}:{p}"),
129                None => h,
130            })
131        });
132        let paths = pick(|d| d.path.as_ref().map(|(k, v)| if k == "pathPrefix" { format!("{v}*") } else { v.clone() }));
133        let mut out = Vec::new();
134        for s in &schemes {
135            if !matches!(s.as_str(), "http" | "https") && hosts.is_empty() {
136                out.push(format!("{s}://*"));
137                continue;
138            }
139            for h in if hosts.is_empty() { vec!["*".to_string()] } else { hosts.clone() } {
140                if paths.is_empty() {
141                    out.push(format!("{s}://{h}"));
142                }
143                for p in &paths {
144                    let sep = if p.starts_with('/') { "" } else { "/" };
145                    out.push(format!("{s}://{h}{sep}{p}"));
146                }
147            }
148        }
149        out
150    }
151
152    fn line(&self) -> String {
153        let list = |vs: &[String]| vs.iter().map(|v| value(v)).collect::<Vec<_>>().join(",");
154        let mut parts = Vec::new();
155        if !self.actions.is_empty() {
156            parts.push(format!("action={}", list(&self.actions)));
157        }
158        if !self.categories.is_empty() {
159            parts.push(format!("category={}", list(&self.categories)));
160        }
161        for d in &self.data {
162            let mut s = String::new();
163            if let Some(v) = &d.scheme {
164                s.push_str(&format!("scheme={} ", value(v)));
165            }
166            if let Some(v) = &d.host {
167                s.push_str(&format!("host={} ", value(v)));
168            }
169            if let Some(v) = &d.port {
170                s.push_str(&format!("port={} ", value(v)));
171            }
172            if let Some((k, v)) = &d.path {
173                s.push_str(&format!("{k}={} ", value(v)));
174            }
175            if let Some(v) = &d.mime_type {
176                s.push_str(&format!("mimeType={} ", value(v)));
177            }
178            parts.push(format!("data({})", s.trim_end()));
179        }
180        parts.join(" ")
181    }
182
183    fn json(&self) -> Json {
184        json!({
185            "actions": self.actions,
186            "categories": self.categories,
187            "data": self.data.iter().map(|d| json!({
188                "scheme": d.scheme, "host": d.host, "port": d.port,
189                "path_kind": d.path.as_ref().map(|p| &p.0), "path": d.path.as_ref().map(|p| &p.1),
190                "mime_type": d.mime_type,
191            })).collect::<Vec<_>>(),
192        })
193    }
194}
195
196/// `android:protectionLevel` flags as text (`signature|privileged`).
197pub fn protection_level_name(level: u32) -> String {
198    let base = match level & 0xf {
199        0 => "normal".to_string(),
200        1 => "dangerous".to_string(),
201        2 => "signature".to_string(),
202        3 => "signatureOrSystem".to_string(),
203        4 => "internal".to_string(),
204        b => format!("base{b}"),
205    };
206    const FLAGS: [(u32, &str); 18] = [
207        (0x10, "privileged"),
208        (0x20, "development"),
209        (0x40, "appop"),
210        (0x80, "pre23"),
211        (0x100, "installer"),
212        (0x200, "verifier"),
213        (0x400, "preinstalled"),
214        (0x800, "setup"),
215        (0x1000, "instant"),
216        (0x2000, "runtime"),
217        (0x4000, "oem"),
218        (0x8000, "vendorPrivileged"),
219        (0x10000, "textClassifier"),
220        (0x20000, "configurator"),
221        (0x40000, "incidentReportApprover"),
222        (0x80000, "appPredictor"),
223        (0x100000, "module"),
224        (0x200000, "companion"),
225    ];
226    let mut out = base;
227    let mut rest = level & !0xf;
228    for (bit, name) in FLAGS {
229        if rest & bit != 0 {
230            out.push('|');
231            out.push_str(name);
232            rest &= !bit;
233        }
234    }
235    if rest != 0 {
236        out.push_str(&format!("|0x{rest:x}"));
237    }
238    out
239}
240
241/// Attribute reading with references resolved through the resource table.
242struct Reader<'a> {
243    table: Option<&'a ResTable>,
244    warnings: Vec<String>,
245}
246
247impl Reader<'_> {
248    /// String value of `a`: literal strings as-is, references resolved to
249    /// their (default-config) value.
250    fn string(&mut self, a: &Attribute) -> String {
251        if let (Some(id), Some(t)) = (a.reference(), self.table) {
252            match resolve_simple(t, id) {
253                Some(v) => return plain_value(t, &v),
254                None => self.warnings.push(format!("{}={} does not resolve to a value", a.name, a.value)),
255            }
256        }
257        a.value.clone()
258    }
259
260    fn str_attr(&mut self, el: &Element, name: &str) -> Option<String> {
261        el.android(name).map(|a| self.string(a))
262    }
263
264    /// Boolean attribute, plus a note listing per-config values when it is a
265    /// reference whose value differs between configurations.
266    fn bool_attr(&mut self, el: &Element, name: &str) -> Option<(bool, Option<String>)> {
267        let a = el.android(name)?;
268        if let Some(b) = a.as_bool() {
269            return Some((b, None));
270        }
271        let (Some(id), Some(t)) = (a.reference(), self.table) else {
272            self.warnings.push(format!("{name}={} is not a boolean", a.value));
273            return None;
274        };
275        let variants: Vec<(String, bool)> = t
276            .values(id)
277            .into_iter()
278            .filter_map(|v| match v.value {
279                EntryValue::Simple(x) if x.kind == TYPE_INT_BOOLEAN => Some((v.config.to_string(), x.data != 0)),
280                EntryValue::Simple(x) if x.kind == TYPE_STRING => {
281                    Some((v.config.to_string(), t.string(x.data).is_some_and(|s| s == "true")))
282                }
283                _ => None,
284            })
285            .collect();
286        let resolved = resolve_simple(t, id).and_then(|v| match v.kind {
287            TYPE_INT_BOOLEAN => Some(v.data != 0),
288            TYPE_STRING => t.string(v.data).map(|s| s == "true"),
289            _ => None,
290        });
291        let Some(b) = resolved else {
292            self.warnings.push(format!("{name}={} does not resolve to a boolean", a.value));
293            return None;
294        };
295        let differs = variants.iter().any(|(_, v)| *v != variants[0].1);
296        let note = differs.then(|| {
297            let list: Vec<String> = variants.iter().map(|(c, v)| format!("{}={v}", if c.is_empty() { "default" } else { c })).collect();
298            format!("{} varies: {}", a.value, list.join(","))
299        });
300        Some((b, note))
301    }
302
303    fn int_attr(&mut self, el: &Element, name: &str) -> Option<i64> {
304        let a = el.android(name)?;
305        if let Some(i) = a.as_int() {
306            return Some(i);
307        }
308        if let (Some(id), Some(t)) = (a.reference(), self.table)
309            && let Some(v) = resolve_simple(t, id)
310        {
311            return match v.kind {
312                TYPE_STRING => t.string(v.data).and_then(|s| s.trim().parse().ok()),
313                _ => Some(v.data as i32 as i64),
314            };
315        }
316        // A codename such as "Tiramisu" in a preview build.
317        a.value.parse().ok()
318    }
319
320    /// Path in the APK of the XML resource an attribute points at.
321    fn xml_path(&mut self, a: &Attribute) -> Option<String> {
322        let (id, t) = (a.reference()?, self.table?);
323        match resolve_simple(t, id) {
324            Some(v) if v.kind == TYPE_STRING => t.string(v.data).map(|s| s.into_owned()),
325            _ => {
326                self.warnings.push(format!("{}={} is not a file resource", a.name, a.value));
327                None
328            }
329        }
330    }
331}
332
333/// `.Foo` / `Foo` / `com.x.Foo` → fully qualified against `package`.
334fn class_name(package: &str, name: &str) -> String {
335    if let Some(rest) = name.strip_prefix('.') {
336        format!("{package}.{rest}")
337    } else if !name.contains('.') && !package.is_empty() {
338        format!("{package}.{name}")
339    } else {
340        name.to_string()
341    }
342}
343
344impl Engine {
345    /// Parse `AndroidManifest.xml` (and the XML resources it points at) into
346    /// a [`ManifestSummary`].
347    pub fn manifest_summary(&self) -> Result<ManifestSummary> {
348        let apk = self.open()?;
349        let data = apk.read("AndroidManifest.xml")?;
350        let table = self.resources()?;
351        let resolver = table.as_deref().map(|t| t as &dyn asc_axml::Resolver);
352        let root = asc_axml::parse_tree(&data, resolver)?;
353        let mut r = Reader { table: table.as_deref(), warnings: Vec::new() };
354        let mut s = ManifestSummary {
355            package: root.plain("package").map(|a| a.value.clone()).unwrap_or_default(),
356            version_code: r.str_attr(&root, "versionCode"),
357            version_name: r.str_attr(&root, "versionName"),
358            shared_user_id: r.str_attr(&root, "sharedUserId"),
359            ..ManifestSummary::default()
360        };
361        if table.is_none() {
362            s.warnings.push("no resources.arsc: references are not resolved".into());
363        }
364        for sdk in root.children_named("uses-sdk") {
365            s.min_sdk = r.int_attr(sdk, "minSdkVersion").or(s.min_sdk);
366            s.target_sdk = r.int_attr(sdk, "targetSdkVersion").or(s.target_sdk);
367        }
368        // Without a targetSdkVersion Android uses minSdkVersion (default 1).
369        let target = s.target_sdk.or(s.min_sdk).unwrap_or(1);
370        for el in &root.children {
371            match el.name.as_str() {
372                "uses-permission" | "uses-permission-sdk-23" | "uses-permission-sdk-m" => {
373                    if let Some(n) = r.str_attr(el, "name") {
374                        s.uses_permissions.push(n);
375                    }
376                }
377                "permission" => {
378                    let level = el.android("protectionLevel").map_or(0, |a| a.data);
379                    s.permissions.push(Permission {
380                        name: r.str_attr(el, "name").unwrap_or_default(),
381                        protection_level: protection_level_name(level),
382                    });
383                }
384                _ => {}
385            }
386        }
387        if let Some(app) = root.children_named("application").next() {
388            let a = &mut s.application;
389            a.class = r.str_attr(app, "name").map(|n| class_name(&s.package, &n));
390            a.debuggable = r.bool_attr(app, "debuggable").is_some_and(|b| b.0);
391            a.allow_backup = r.bool_attr(app, "allowBackup").is_none_or(|b| b.0);
392            a.test_only = r.bool_attr(app, "testOnly").is_some_and(|b| b.0);
393            a.uses_cleartext_traffic = r.bool_attr(app, "usesCleartextTraffic").map(|b| b.0);
394            a.permission = r.str_attr(app, "permission");
395            a.full_backup_content = app.android("fullBackupContent").map(|x| r.xml_path(x).unwrap_or_else(|| x.value.clone()));
396            a.data_extraction_rules = app.android("dataExtractionRules").map(|x| r.xml_path(x).unwrap_or_else(|| x.value.clone()));
397            a.network_security_config = app.android("networkSecurityConfig").and_then(|x| r.xml_path(x));
398            let mut nsc_cleartext = None;
399            if let Some(path) = a.network_security_config.clone() {
400                match apk.read(&path).map_err(crate::EngineError::from).and_then(|d| Ok(asc_axml::parse_tree(&d, resolver)?)) {
401                    Ok(doc) => nsc_cleartext = network_security(&doc, table.as_deref(), &mut s.network_security),
402                    Err(e) => r.warnings.push(format!("networkSecurityConfig {path}: {e}")),
403                }
404            }
405            a.cleartext_effective = match (nsc_cleartext, a.uses_cleartext_traffic) {
406                (Some(b), _) => (b, "networkSecurityConfig"),
407                (None, Some(b)) => (b, "usesCleartextTraffic"),
408                (None, None) => (target < SDK_CLEARTEXT_OFF, "targetSdk default"),
409            };
410            let app_permission = a.permission.clone();
411            for el in &app.children {
412                let kind = el.name.as_str();
413                if !matches!(kind, "activity" | "activity-alias" | "service" | "receiver" | "provider") {
414                    continue;
415                }
416                let mut c = Component {
417                    kind: kind.to_string(),
418                    class: class_name(&s.package, &r.str_attr(el, "name").unwrap_or_default()),
419                    target: r.str_attr(el, "targetActivity").map(|n| class_name(&s.package, &n)),
420                    enabled: r.bool_attr(el, "enabled").is_none_or(|b| b.0),
421                    process: r.str_attr(el, "process"),
422                    ..Component::default()
423                };
424                c.permission = r.str_attr(el, "permission");
425                if c.permission.is_none() && app_permission.is_some() {
426                    c.permission = app_permission.clone();
427                    c.permission_from_app = true;
428                }
429                for f in el.children_named("intent-filter") {
430                    let mut filter = IntentFilter::default();
431                    for x in &f.children {
432                        match x.name.as_str() {
433                            "action" => filter.actions.extend(r.str_attr(x, "name")),
434                            "category" => filter.categories.extend(r.str_attr(x, "name")),
435                            "data" => {
436                                let mut d = DataSpec {
437                                    scheme: r.str_attr(x, "scheme"),
438                                    host: r.str_attr(x, "host"),
439                                    port: r.str_attr(x, "port"),
440                                    mime_type: r.str_attr(x, "mimeType"),
441                                    path: None,
442                                };
443                                for k in ["path", "pathPrefix", "pathPattern", "pathAdvancedPattern", "pathSuffix"] {
444                                    if let Some(v) = r.str_attr(x, k) {
445                                        d.path = Some((k.to_string(), v));
446                                    }
447                                }
448                                filter.data.push(d);
449                            }
450                            _ => {}
451                        }
452                    }
453                    c.intent_filters.push(filter);
454                }
455                let (exported, reason) = match r.bool_attr(el, "exported") {
456                    Some((b, note)) => (b, note.map_or("explicit".to_string(), |n| format!("explicit, {n}"))),
457                    None if !c.intent_filters.is_empty() && target < SDK_EXPLICIT_EXPORTED => (true, "intent-filter".to_string()),
458                    None if !c.intent_filters.is_empty() => (false, "missing (install fails on targetSdk>=31)".to_string()),
459                    None if kind == "provider" && target < SDK_PROVIDER_PRIVATE => (true, "provider-default".to_string()),
460                    None => (false, "default".to_string()),
461                };
462                c.exported = exported;
463                c.exported_reason = reason;
464                if kind == "provider" {
465                    c.authorities = r.str_attr(el, "authorities").map(|a| a.split(';').map(str::to_string).collect()).unwrap_or_default();
466                    c.grant_uri_permissions = r.bool_attr(el, "grantUriPermissions").is_some_and(|b| b.0);
467                    c.read_permission = r.str_attr(el, "readPermission");
468                    c.write_permission = r.str_attr(el, "writePermission");
469                }
470                for m in el.children_named("meta-data") {
471                    let name = r.str_attr(m, "name").unwrap_or_default();
472                    let value = m.android("value").or(m.android("resource")).map(|a| r.string(a));
473                    if name == FILE_PROVIDER_PATHS
474                        && let Some(path) = m.android("resource").and_then(|a| r.xml_path(a))
475                    {
476                        match apk.read(&path).map_err(crate::EngineError::from).and_then(|d| Ok(asc_axml::parse_tree(&d, resolver)?)) {
477                            Ok(doc) => c.provider_paths = provider_paths(&doc),
478                            Err(e) => r.warnings.push(format!("{FILE_PROVIDER_PATHS} {path}: {e}")),
479                        }
480                    }
481                    c.meta_data.push((name, value.unwrap_or_default()));
482                }
483                s.components.push(c);
484            }
485        }
486        s.warnings.extend(r.warnings);
487        Ok(s)
488    }
489}
490
491/// Summarize a network security config into `out` records; returns the
492/// base config's cleartext setting when it states one.
493fn network_security(doc: &Element, table: Option<&ResTable>, out: &mut Vec<String>) -> Option<bool> {
494    let cleartext = |el: &Element| el.plain("cleartextTrafficPermitted").and_then(|a| a.as_bool());
495    let anchors = |el: &Element| -> String {
496        let list: Vec<String> = el
497            .children_named("trust-anchors")
498            .flat_map(|t| t.children_named("certificates"))
499            .map(|c| {
500                let src = c.plain("src").map_or(String::new(), |a| match (a.reference(), table) {
501                    (Some(id), Some(t)) => format!("@{}", t.ref_name(id)),
502                    _ => a.value.clone(),
503                });
504                match c.plain("overridePins").and_then(|a| a.as_bool()) {
505                    Some(true) => format!("{src}(overridePins)"),
506                    _ => src,
507                }
508            })
509            .collect();
510        list.join(",")
511    };
512    let mut base_cleartext = None;
513    for el in &doc.children {
514        match el.name.as_str() {
515            "base-config" => {
516                base_cleartext = cleartext(el);
517                out.push(format!("base-config cleartextTrafficPermitted={} trust-anchors={}", fmt_opt(cleartext(el)), anchors(el)));
518            }
519            "debug-overrides" => out.push(format!("debug-overrides trust-anchors={}", anchors(el))),
520            "domain-config" => domain_config(el, &cleartext, &anchors, out),
521            _ => {}
522        }
523    }
524    base_cleartext
525}
526
527fn domain_config(el: &Element, cleartext: &dyn Fn(&Element) -> Option<bool>, anchors: &dyn Fn(&Element) -> String, out: &mut Vec<String>) {
528    let domains: Vec<String> = el
529        .children_named("domain")
530        .map(|d| {
531            let sub = d.plain("includeSubdomains").and_then(|a| a.as_bool()).unwrap_or(false);
532            if sub { format!("*.{}", d.text) } else { d.text.clone() }
533        })
534        .collect();
535    let pins: usize = el.children_named("pin-set").map(|p| p.children_named("pin").count()).sum();
536    out.push(format!(
537        "domain-config domains={} cleartextTrafficPermitted={} trust-anchors={} pins={pins}",
538        domains.join(","),
539        fmt_opt(cleartext(el)),
540        anchors(el)
541    ));
542    for nested in el.children_named("domain-config") {
543        domain_config(nested, cleartext, anchors, out);
544    }
545}
546
547fn fmt_opt(b: Option<bool>) -> String {
548    b.map_or("-".to_string(), |b| b.to_string())
549}
550
551/// `<paths>` children of a FileProvider config.
552fn provider_paths(doc: &Element) -> Vec<String> {
553    doc.children
554        .iter()
555        .map(|p| {
556            let attr = |n: &str| p.plain(n).map_or("-".to_string(), |a| value(&a.value).into_owned());
557            format!("{} name={} path={}", p.name, attr("name"), attr("path"))
558        })
559        .collect()
560}
561
562impl Component {
563    fn line(&self) -> String {
564        let mut s = format!("{} {} exported={} ({})", self.kind, value(&self.class), self.exported, self.exported_reason);
565        if !self.enabled {
566            s.push_str(" enabled=false");
567        }
568        if let Some(t) = &self.target {
569            s.push_str(&format!(" target={}", value(t)));
570        }
571        if let Some(p) = &self.permission {
572            s.push_str(&format!(" permission={}{}", value(p), if self.permission_from_app { "(application)" } else { "" }));
573        }
574        if self.kind == "provider" {
575            s.push_str(&format!(" authorities={} grantUriPermissions={}", self.authorities(), self.grant_uri_permissions));
576            if let Some(p) = &self.read_permission {
577                s.push_str(&format!(" readPermission={}", value(p)));
578            }
579            if let Some(p) = &self.write_permission {
580                s.push_str(&format!(" writePermission={}", value(p)));
581            }
582        }
583        if let Some(p) = &self.process {
584            s.push_str(&format!(" process={}", value(p)));
585        }
586        s
587    }
588
589    /// The provider's authorities, `;`-separated as the manifest writes them.
590    fn authorities(&self) -> String {
591        self.authorities.iter().map(|a| value(a)).collect::<Vec<_>>().join(";")
592    }
593
594    pub fn json(&self) -> Json {
595        json!({
596            "kind": self.kind,
597            "class": self.class,
598            "target": self.target,
599            "exported": self.exported,
600            "exported_reason": self.exported_reason,
601            "enabled": self.enabled,
602            "permission": self.permission,
603            "permission_from_application": self.permission_from_app,
604            "process": self.process,
605            "authorities": self.authorities,
606            "grant_uri_permissions": self.grant_uri_permissions,
607            "read_permission": self.read_permission,
608            "write_permission": self.write_permission,
609            "intent_filters": self.intent_filters.iter().map(IntentFilter::json).collect::<Vec<_>>(),
610            "deep_links": self.deep_links(),
611            "meta_data": self.meta_data.iter().map(|(k, v)| json!({ "name": k, "value": v })).collect::<Vec<_>>(),
612            "provider_paths": self.provider_paths,
613        })
614    }
615
616    pub fn deep_links(&self) -> Vec<String> {
617        let mut v: Vec<String> = self.intent_filters.iter().flat_map(IntentFilter::deep_links).collect();
618        v.dedup();
619        v
620    }
621}
622
623impl ManifestSummary {
624    /// One record per line, the first word naming the record type:
625    /// `package`, `application`, `uses-permission`, `permission`, a component
626    /// kind, `intent-filter`, `deeplink`, `provider-path`, `nsc`, `warning`.
627    /// Component lines carry the class so `grep exported=true` lists the
628    /// attack surface directly.
629    pub fn lines(&self) -> Vec<String> {
630        let opt = |o: &Option<String>| o.as_deref().map_or("-".to_string(), |v| value(v).into_owned());
631        let opt_i = |o: Option<i64>| o.map_or("-".to_string(), |v| v.to_string());
632        let mut out = vec![format!(
633            "package {} versionCode={} versionName={} minSdk={} targetSdk={} sharedUserId={}",
634            value(&self.package),
635            opt(&self.version_code),
636            opt(&self.version_name),
637            opt_i(self.min_sdk),
638            opt_i(self.target_sdk),
639            opt(&self.shared_user_id)
640        )];
641        let a = &self.application;
642        out.push(format!(
643            "application class={} debuggable={} allowBackup={} testOnly={} usesCleartextTraffic={} cleartext={}({}) networkSecurityConfig={} permission={} fullBackupContent={} dataExtractionRules={}",
644            opt(&a.class),
645            a.debuggable,
646            a.allow_backup,
647            a.test_only,
648            a.uses_cleartext_traffic.map_or("-".to_string(), |b| b.to_string()),
649            a.cleartext_effective.0,
650            a.cleartext_effective.1,
651            opt(&a.network_security_config),
652            opt(&a.permission),
653            opt(&a.full_backup_content),
654            opt(&a.data_extraction_rules),
655        ));
656        out.extend(self.uses_permissions.iter().map(|p| format!("uses-permission {}", value(p))));
657        out.extend(
658            self.permissions.iter().map(|p| format!("permission {} protectionLevel={}", value(&p.name), value(&p.protection_level))),
659        );
660        for c in &self.components {
661            out.push(c.line());
662            let class = value(&c.class);
663            for f in &c.intent_filters {
664                out.push(format!("intent-filter {class} {}", f.line()));
665            }
666            for l in c.deep_links() {
667                out.push(format!("deeplink {class} {}", value(&l)));
668            }
669            for p in &c.provider_paths {
670                out.push(format!("provider-path {} {p}", c.authorities()));
671            }
672        }
673        out.extend(self.network_security.iter().map(|l| format!("nsc {l}")));
674        out.extend(self.warnings.iter().map(|w| format!("warning {w}")));
675        out
676    }
677
678    pub fn json(&self) -> Json {
679        let a = &self.application;
680        json!({
681            "package": self.package,
682            "version_code": self.version_code,
683            "version_name": self.version_name,
684            "min_sdk": self.min_sdk,
685            "target_sdk": self.target_sdk,
686            "shared_user_id": self.shared_user_id,
687            "application": {
688                "class": a.class,
689                "debuggable": a.debuggable,
690                "allow_backup": a.allow_backup,
691                "test_only": a.test_only,
692                "uses_cleartext_traffic": a.uses_cleartext_traffic,
693                "cleartext": a.cleartext_effective.0,
694                "cleartext_source": a.cleartext_effective.1,
695                "network_security_config": a.network_security_config,
696                "permission": a.permission,
697                "full_backup_content": a.full_backup_content,
698                "data_extraction_rules": a.data_extraction_rules,
699            },
700            "uses_permissions": self.uses_permissions,
701            "permissions": self.permissions.iter().map(|p| json!({ "name": p.name, "protection_level": p.protection_level })).collect::<Vec<_>>(),
702            "components": self.components.iter().map(Component::json).collect::<Vec<_>>(),
703            "network_security": self.network_security,
704            "warnings": self.warnings,
705        })
706    }
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    #[test]
714    fn class_names_and_levels() {
715        assert_eq!(class_name("com.a", ".Main"), "com.a.Main");
716        assert_eq!(class_name("com.a", "Main"), "com.a.Main");
717        assert_eq!(class_name("com.a", "org.b.Main"), "org.b.Main");
718        assert_eq!(protection_level_name(0), "normal");
719        assert_eq!(protection_level_name(0x12), "signature|privileged");
720        assert_eq!(protection_level_name(0x4000_0002), "signature|0x40000000");
721    }
722
723    #[test]
724    fn deep_link_patterns() {
725        let f = IntentFilter {
726            actions: vec!["android.intent.action.VIEW".into()],
727            categories: vec!["android.intent.category.BROWSABLE".into()],
728            data: vec![
729                DataSpec { scheme: Some("https".into()), ..Default::default() },
730                DataSpec { host: Some("a.com".into()), ..Default::default() },
731                DataSpec { path: Some(("pathPrefix".into(), "/x".into())), ..Default::default() },
732                DataSpec { scheme: Some("myapp".into()), ..Default::default() },
733            ],
734        };
735        assert_eq!(f.deep_links(), ["https://a.com/x*", "myapp://a.com/x*"]);
736        let none = IntentFilter { actions: vec!["android.intent.action.MAIN".into()], ..Default::default() };
737        assert!(none.deep_links().is_empty());
738    }
739}