Skip to main content

permission_auditor/
db.rs

1//! The curated permission → risk database.
2//!
3//! Covers the complete Manifest V3 permission surface: every named API
4//! permission documented in the Chrome Extensions permission reference, plus
5//! the host-access match-pattern tokens that appear under `host_permissions`.
6//! Each entry pairs a risk tier with a plain-English description of what the
7//! grant actually allows an extension to do.
8
9/// The four-tier risk classification used by the audit.
10///
11/// `Low < Medium < High < Critical`. `Critical` is reserved for grants that
12/// amount to total control of a tab, every page, or the user's machine, and
13/// for the combinations that make a manifest meaningfully hostile (arbitrary
14/// host access together with code injection / cookie access). `Derivative`
15/// ordering is implemented via `Ord` so the highest of a set of findings can
16/// be computed with `max`.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub enum RiskLevel {
19    /// Least-privileged. No persistent broad access; scoped to an explicit
20    /// user gesture or to the extension's own sandbox.
21    Low,
22    /// Sensitive but bounded. Grants meaningful read/write access to a
23    /// category of user data (history, bookmarks, downloads) or to a
24    /// single, named site.
25    Medium,
26    /// Broad, persistent, cross-origin or sensitive-system access. Any
27    /// extension requesting these deserves careful review by itself.
28    High,
29    /// Effectively total control. Arbitrary code on every site, native
30    /// process execution, or the broadest host grants combined with
31    /// code/cookie access. These are the permissions malware and spyware
32    /// extensions reach for.
33    Critical,
34}
35
36impl RiskLevel {
37    /// An uppercase label suitable for badges / UI chips.
38    ///
39    /// ```
40    /// use permission_auditor::RiskLevel;
41    /// assert_eq!(RiskLevel::Critical.label(), "CRITICAL");
42    /// assert_eq!(RiskLevel::Low.label(), "LOW");
43    /// ```
44    pub fn label(self) -> &'static str {
45        match self {
46            RiskLevel::Low => "LOW",
47            RiskLevel::Medium => "MEDIUM",
48            RiskLevel::High => "HIGH",
49            RiskLevel::Critical => "CRITICAL",
50        }
51    }
52
53    /// One-line summary of what the tier means, for report headers.
54    pub fn summary(self) -> &'static str {
55        match self {
56            RiskLevel::Low => "Low risk: scoped or sandboxed access with no broad reach.",
57            RiskLevel::Medium => {
58                "Medium risk: meaningful access to a category of user data or a named site."
59            }
60            RiskLevel::High => {
61                "High risk: broad, persistent cross-origin or sensitive-system access."
62            }
63            RiskLevel::Critical => {
64                "Critical risk: effectively total control of pages, sessions, or the machine."
65            }
66        }
67    }
68}
69
70/// A single database row: the manifest token exactly as it appears in a
71/// manifest, its risk tier, and a plain-English description.
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct PermissionEntry {
74    /// The permission token, e.g. `"tabs"`, `"cookies"`, `"<all_urls>"`.
75    pub token: &'static str,
76    /// The risk classification.
77    pub level: RiskLevel,
78    /// Concise explanation of what the extension can do.
79    pub description: &'static str,
80}
81
82/// The full curated MV3 permission risk database.
83///
84/// Ordered Low → Medium → High → Critical for readability. Host
85/// match-patterns (scheme wildcards, `<all_urls>`, scoped patterns) are
86/// synthesised at audit time by [`crate::classify_host_pattern`], so only
87/// the named/broad tokens appear here.
88#[rustfmt::skip]
89pub const RISK_DATABASE: &[PermissionEntry] = &[
90    // ============================================================
91    // LOW — scoped or sandboxed, no persistent broad reach.
92    // ============================================================
93    PermissionEntry {
94        token: "activeTab",
95        level: RiskLevel::Low,
96        description: "Grants temporary access to the current tab only when the user \
97                      explicitly clicks the extension action. No persistent background \
98                      access to any site.",
99    },
100    PermissionEntry {
101        token: "contextMenus",
102        level: RiskLevel::Low,
103        description: "Adds items to the browser's right-click context menu. No page \
104                      content access by itself.",
105    },
106    PermissionEntry {
107        token: "storage",
108        level: RiskLevel::Low,
109        description: "Stores extension data locally (synced/local). Confined to the \
110                      extension's own sandbox; cannot read site data.",
111    },
112    PermissionEntry {
113        token: "alarms",
114        level: RiskLevel::Low,
115        description: "Schedules code to run at a future time. No content access by \
116                      itself.",
117    },
118    PermissionEntry {
119        token: "idle",
120        level: RiskLevel::Low,
121        description: "Detects when the machine is idle or locked. No page content \
122                      access.",
123    },
124    PermissionEntry {
125        token: "notifications",
126        level: RiskLevel::Low,
127        description: "Shows desktop notifications. No page content access.",
128    },
129    PermissionEntry {
130        token: "offscreen",
131        level: RiskLevel::Low,
132        description: "Creates offscreen documents for DOM work without a visible page. \
133                      No extra site access by itself.",
134    },
135    PermissionEntry {
136        token: "power",
137        level: RiskLevel::Low,
138        description: "Keeps the screen or system awake. No content access.",
139    },
140    PermissionEntry {
141        token: "sidePanel",
142        level: RiskLevel::Low,
143        description: "Shows content in the browser side panel. No extra host access by \
144                      itself.",
145    },
146    PermissionEntry {
147        token: "tts",
148        level: RiskLevel::Low,
149        description: "Text-to-speech output. No content access.",
150    },
151    PermissionEntry {
152        token: "unlimitedStorage",
153        level: RiskLevel::Low,
154        description: "Removes the extension storage quota. No extra site access.",
155    },
156    PermissionEntry {
157        token: "userScripts",
158        level: RiskLevel::Low,
159        description: "Registers user-authored scripts (the MV3 user-script API). \
160                      Powerful if a user installs a hostile script, but grants no \
161                      host access the user did not already consent to.",
162    },
163    PermissionEntry {
164        token: "declarativeNetRequest",
165        level: RiskLevel::Low,
166        description: "Blocks or modifies network requests via static rules (the MV3 \
167                      content-blocker path). Less powerful than webRequest blocking, \
168                      but can still read request URLs.",
169    },
170    PermissionEntry {
171        token: "declarativeNetRequestFeedback",
172        level: RiskLevel::Low,
173        description: "Observes which declarativeNetRequest rules matched. No extra \
174                      host access.",
175    },
176    PermissionEntry {
177        token: "gcm",
178        level: RiskLevel::Low,
179        description: "Receives push messages via Google Cloud Messaging. No content \
180                      access.",
181    },
182    PermissionEntry {
183        token: "action",
184        level: RiskLevel::Low,
185        description: "Configures the extension's toolbar action icon. No host access.",
186    },
187    PermissionEntry {
188        token: "favicon",
189        level: RiskLevel::Low,
190        description: "Reads favicons for URLs. No page content access.",
191    },
192    PermissionEntry {
193        token: "declarativeContent",
194        level: RiskLevel::Low,
195        description: "Reacts to page URL/CSS state via declarative rules. No arbitrary \
196                      script execution.",
197    },
198
199    // ============================================================
200    // MEDIUM — bounded but sensitive user data, or named-site access.
201    // ============================================================
202    PermissionEntry {
203        token: "bookmarks",
204        level: RiskLevel::Medium,
205        description: "Reads and modifies the user's full bookmark tree.",
206    },
207    PermissionEntry {
208        token: "history",
209        level: RiskLevel::Medium,
210        description: "Reads and clears the user's full browsing history.",
211    },
212    PermissionEntry {
213        token: "downloads",
214        level: RiskLevel::Medium,
215        description: "Initiates, monitors, and opens downloads; can open arbitrary \
216                      files from the download shelf.",
217    },
218    PermissionEntry {
219        token: "downloads.open",
220        level: RiskLevel::Medium,
221        description: "Opens downloaded files on disk. Combined with a hostile download \
222                      this can execute local content.",
223    },
224    PermissionEntry {
225        token: "downloads.shelf",
226        level: RiskLevel::Medium,
227        description: "Hides or shows the download shelf; can mask a stealthy \
228                      download.",
229    },
230    PermissionEntry {
231        token: "downloads.ui",
232        level: RiskLevel::Medium,
233        description: "Controls the downloads UI surface.",
234    },
235    PermissionEntry {
236        token: "geolocation",
237        level: RiskLevel::Medium,
238        description: "Reads the user's GPS / IP-derived location (subject to a per-site \
239                      permission prompt).",
240    },
241    PermissionEntry {
242        token: "clipboardWrite",
243        level: RiskLevel::Medium,
244        description: "Writes to the system clipboard. Can overwrite a copied password \
245                      or inject pasted content.",
246    },
247    PermissionEntry {
248        token: "clipboardRead",
249        level: RiskLevel::Medium,
250        description: "Reads the system clipboard, which frequently contains copied \
251                      passwords, tokens, or private text.",
252    },
253    PermissionEntry {
254        token: "identity",
255        level: RiskLevel::Medium,
256        description: "Triggers OAuth sign-in and obtains the user's signed-in account \
257                      email / profile and an auth token.",
258    },
259    PermissionEntry {
260        token: "identity.email",
261        level: RiskLevel::Medium,
262        description: "Returns the user's signed-in email address directly.",
263    },
264    PermissionEntry {
265        token: "management",
266        level: RiskLevel::Medium,
267        description: "Lists, enables, disables, and uninstalls other installed \
268                      extensions.",
269    },
270    PermissionEntry {
271        token: "tabs",
272        level: RiskLevel::Medium,
273        description: "Reads the URL and title of every open tab and receives tab-update \
274                      events. Effectively full browsing-session visibility.",
275    },
276    PermissionEntry {
277        token: "tabGroups",
278        level: RiskLevel::Medium,
279        description: "Reads and modifies tab groups, exposing which sites the user \
280                      clusters together.",
281    },
282    PermissionEntry {
283        token: "topSites",
284        level: RiskLevel::Medium,
285        description: "Reads the user's most-visited sites (the new-tab shortcuts).",
286    },
287    PermissionEntry {
288        token: "sessions",
289        level: RiskLevel::Medium,
290        description: "Reads recently closed tabs and windows across devices.",
291    },
292    PermissionEntry {
293        token: "pageCapture",
294        level: RiskLevel::Medium,
295        description: "Saves the current page as an MHTML archive, capturing rendered \
296                      content.",
297    },
298    PermissionEntry {
299        token: "search",
300        level: RiskLevel::Medium,
301        description: "Sets the default search provider and issues queries.",
302    },
303    PermissionEntry {
304        token: "browsingData",
305        level: RiskLevel::Medium,
306        description: "Clears cookies, cache, history, and other browsing data — can \
307                      wipe a user's session.",
308    },
309    PermissionEntry {
310        token: "fontSettings",
311        level: RiskLevel::Medium,
312        description: "Changes browser font settings.",
313    },
314    PermissionEntry {
315        token: "readingList",
316        level: RiskLevel::Medium,
317        description: "Reads and modifies the user's reading list.",
318    },
319
320    // ============================================================
321    // HIGH — broad, persistent, cross-origin or sensitive-system.
322    // ============================================================
323    PermissionEntry {
324        token: "cookies",
325        level: RiskLevel::High,
326        description: "Reads and modifies all cookies for any site the extension has \
327                      host access to, including session and auth cookies.",
328    },
329    PermissionEntry {
330        token: "webRequest",
331        level: RiskLevel::High,
332        description: "Observes (and in MV2 could block) every network request and \
333                      response, exposing full URLs, headers, and bodies including \
334                      credentials.",
335    },
336    PermissionEntry {
337        token: "webRequestBlocking",
338        level: RiskLevel::High,
339        description: "MV2-only blocking webRequest — full request interception and \
340                      modification. Removed from MV3.",
341    },
342    PermissionEntry {
343        token: "debugger",
344        level: RiskLevel::High,
345        description: "Attaches the Chrome DevTools Protocol to a tab, giving full DOM, \
346                      network, and JS execution control — effectively total control \
347                      of the page.",
348    },
349    PermissionEntry {
350        token: "nativeMessaging",
351        level: RiskLevel::High,
352        description: "Talks to a native application installed on the user's machine. \
353                      Escapes the browser sandbox entirely.",
354    },
355    PermissionEntry {
356        token: "fileSystem",
357        level: RiskLevel::High,
358        description: "Reads and writes files outside the browser sandbox (where granted \
359                      by the platform).",
360    },
361    PermissionEntry {
362        token: "fileBrowserHandler",
363        level: RiskLevel::High,
364        description: "Reads and writes files via the ChromeOS file browser.",
365    },
366    PermissionEntry {
367        token: "proxy",
368        level: RiskLevel::High,
369        description: "Configures the browser's proxy settings. A hostile extension can \
370                      redirect all traffic through an attacker-controlled server.",
371    },
372    PermissionEntry {
373        token: "privacy",
374        level: RiskLevel::High,
375        description: "Reads and changes privacy-related browser settings (do-not-track, \
376                      third-party cookies, hyperlink auditing).",
377    },
378    PermissionEntry {
379        token: "system.cpu",
380        level: RiskLevel::High,
381        description: "Reads detailed CPU metadata useful for device fingerprinting.",
382    },
383    PermissionEntry {
384        token: "system.memory",
385        level: RiskLevel::High,
386        description: "Reads physical memory capacity, a device-fingerprinting signal.",
387    },
388    PermissionEntry {
389        token: "system.storage",
390        level: RiskLevel::High,
391        description: "Reads attached storage device metadata and can eject devices.",
392    },
393    PermissionEntry {
394        token: "system.network",
395        level: RiskLevel::High,
396        description: "Reads network interface metadata exposing the local network \
397                      topology.",
398    },
399    PermissionEntry {
400        token: "system.display",
401        level: RiskLevel::High,
402        description: "Reads display metadata (resolution, DPI) usable for \
403                      fingerprinting.",
404    },
405    PermissionEntry {
406        token: "system.audio",
407        level: RiskLevel::High,
408        description: "Reads audio device metadata.",
409    },
410    PermissionEntry {
411        token: "vpnProvider",
412        level: RiskLevel::High,
413        description: "Configures a VPN (ChromeOS), which can redirect and intercept \
414                      all network traffic.",
415    },
416    PermissionEntry {
417        token: "enterprise.networkingAttributes",
418        level: RiskLevel::High,
419        description: "Reads detailed network attributes (ChromeOS enterprise), exposing \
420                      internal network identity.",
421    },
422    PermissionEntry {
423        token: "enterprise.deviceAttributes",
424        level: RiskLevel::High,
425        description: "Reads ChromeOS enterprise device identity attributes.",
426    },
427    PermissionEntry {
428        token: "webNavigation",
429        level: RiskLevel::High,
430        description: "Receives the full navigation events of every frame in every tab, \
431                      including the URL of every frame and redirect.",
432    },
433    PermissionEntry {
434        token: "scripting",
435        level: RiskLevel::High,
436        description: "Injects arbitrary JavaScript into pages for which the extension \
437                      has host permission — full page control at runtime (MV3 \
438                      successor to tabs.executeScript).",
439    },
440    PermissionEntry {
441        token: "contentSettings",
442        level: RiskLevel::High,
443        description: "Reads and modifies per-site content settings (cookies, \
444                      javascript, plugins, mic, camera) for every origin.",
445    },
446    PermissionEntry {
447        token: "usbDevices",
448        level: RiskLevel::High,
449        description: "Accesses listed USB devices directly, bypassing the page.",
450    },
451    PermissionEntry {
452        token: "serial",
453        level: RiskLevel::High,
454        description: "Reads and writes to serial ports.",
455    },
456    PermissionEntry {
457        token: "bluetoothLowEnergy",
458        level: RiskLevel::High,
459        description: "Discovers and talks to BLE devices.",
460    },
461    PermissionEntry {
462        token: "hid",
463        level: RiskLevel::High,
464        description: "Talks to raw HID devices (keyboards, security keys).",
465    },
466    PermissionEntry {
467        token: "audio",
468        level: RiskLevel::High,
469        description: "Captures and renders audio from / to the system.",
470    },
471    PermissionEntry {
472        token: "audioModem",
473        level: RiskLevel::High,
474        description: "Encodes/decodes data over audio (ultrasonic pairing).",
475    },
476    PermissionEntry {
477        token: "bluetooth",
478        level: RiskLevel::High,
479        description: "Discovers and connects to Bluetooth devices.",
480    },
481    PermissionEntry {
482        token: "mdns",
483        level: RiskLevel::High,
484        description: "Discovers services on the local network via mDNS.",
485    },
486    PermissionEntry {
487        token: "platformInfo",
488        level: RiskLevel::High,
489        description: "Reads OS / arch / platform metadata for fingerprinting.",
490    },
491    PermissionEntry {
492        token: "processes",
493        level: RiskLevel::High,
494        description: "Observes browser process metadata and CPU usage per tab.",
495    },
496    PermissionEntry {
497        token: "networking.onc",
498        level: RiskLevel::High,
499        description: "Configures ChromeOS network connections (Open Network \
500                      Configuration).",
501    },
502    PermissionEntry {
503        token: "networking.config",
504        level: RiskLevel::High,
505        description: "Configures network credentials on ChromeOS.",
506    },
507    PermissionEntry {
508        token: "documentScan",
509        level: RiskLevel::High,
510        description: "Reads from attached document scanners.",
511    },
512    PermissionEntry {
513        token: "accessibilityFeatures.modify",
514        level: RiskLevel::High,
515        description: "Modifies accessibility settings (can be abused to alter input \
516                      handling).",
517    },
518    PermissionEntry {
519        token: "input",
520        level: RiskLevel::High,
521        description: "ChromeOS IME input — can observe keystrokes.",
522    },
523    PermissionEntry {
524        token: "languageSettings",
525        level: RiskLevel::High,
526        description: "Reads and changes the user's language settings.",
527    },
528    PermissionEntry {
529        token: "wallpaper",
530        level: RiskLevel::High,
531        description: "Sets the ChromeOS wallpaper.",
532    },
533    PermissionEntry {
534        token: "enterprise.hardwarePlatform",
535        level: RiskLevel::High,
536        description: "Reads hardware platform identity (enterprise).",
537    },
538    PermissionEntry {
539        token: "clipboard",
540        level: RiskLevel::High,
541        description: "Reads and writes the system clipboard (combined read+write).",
542    },
543
544    // ============================================================
545    // CRITICAL — effectively total control. The malware set.
546    // ============================================================
547    PermissionEntry {
548        token: "<all_urls>",
549        level: RiskLevel::Critical,
550        description: "Requests host access to every site on the web. Combined with \
551                      content scripts or scripting this means any page's content, \
552                      forms, and credentials can be read and modified at will — the \
553                      canonical spyware grant.",
554    },
555    PermissionEntry {
556        token: "*://*/*",
557        level: RiskLevel::Critical,
558        description: "Match pattern granting host access to every http/https URL. \
559                      Equivalent in effect to <all_urls> for normal browsing.",
560    },
561    PermissionEntry {
562        token: "http://*/*",
563        level: RiskLevel::Critical,
564        description: "Host access to every plain-http URL, including login pages and \
565                      intranet sites.",
566    },
567    PermissionEntry {
568        token: "https://*/*",
569        level: RiskLevel::Critical,
570        description: "Host access to every secure URL on the web.",
571    },
572    PermissionEntry {
573        token: "*://*",
574        level: RiskLevel::Critical,
575        description: "Truncated but still blanket http/https host access.",
576    },
577    PermissionEntry {
578        token: "file:///*",
579        level: RiskLevel::Critical,
580        description: "Host access to local files via the file:// scheme, subject to \
581                      the 'allow access to file URLs' toggle. Reads files on disk.",
582    },
583    PermissionEntry {
584        token: "urn:*",
585        level: RiskLevel::Critical,
586        description: "Host access to URN resources (broad scheme grant).",
587    },
588];
589
590/// Look up a single permission token in the database.
591///
592/// Returns `None` for tokens that are not present; callers wanting
593/// pattern-aware classification of host match-patterns should use
594/// [`crate::audit`] or [`crate::classify_host_pattern`] instead, which
595/// synthesise findings for `https://*.example.com/*`-style patterns.
596///
597/// ```
598/// use permission_auditor::{find_permission, RiskLevel};
599/// let c = find_permission("cookies").unwrap();
600/// assert_eq!(c.level, RiskLevel::High);
601/// assert!(find_permission("not-real").is_none());
602/// ```
603pub fn find_permission(token: &str) -> Option<&'static PermissionEntry> {
604    RISK_DATABASE.iter().find(|e| e.token == token)
605}
606
607#[cfg(test)]
608mod tests {
609    use super::*;
610
611    #[test]
612    fn database_is_comprehensive() {
613        // The audit crate should cover meaningfully more of the MV3 surface
614        // than the lookup-only sibling crate.
615        assert!(RISK_DATABASE.len() >= 60, "only {} entries", RISK_DATABASE.len());
616        let crit = RISK_DATABASE.iter().filter(|e| e.level == RiskLevel::Critical).count();
617        let high = RISK_DATABASE.iter().filter(|e| e.level == RiskLevel::High).count();
618        let med = RISK_DATABASE.iter().filter(|e| e.level == RiskLevel::Medium).count();
619        let low = RISK_DATABASE.iter().filter(|e| e.level == RiskLevel::Low).count();
620        assert!(crit >= 5, "need >=5 Critical entries, got {crit}");
621        assert!(high >= 25, "need >=25 High entries, got {high}");
622        assert!(med >= 12, "need >=12 Medium entries, got {med}");
623        assert!(low >= 12, "need >=12 Low entries, got {low}");
624    }
625
626    #[test]
627    fn every_entry_round_trips_through_lookup() {
628        for entry in RISK_DATABASE {
629            let found = find_permission(entry.token);
630            assert!(found.is_some(), "{:?} not found", entry.token);
631            assert_eq!(found.unwrap().level, entry.level);
632            assert!(entry.description.len() >= 25, "thin description for {:?}", entry.token);
633        }
634    }
635
636    #[test]
637    fn canonical_high_risk_tokens_present() {
638        for tok in ["cookies", "webRequest", "debugger", "nativeMessaging", "scripting", "webNavigation"] {
639            assert!(find_permission(tok).is_some(), "missing {tok}");
640        }
641    }
642
643    #[test]
644    fn canonical_critical_tokens_present() {
645        for tok in ["<all_urls>", "*://*/*", "https://*/*", "file:///*"] {
646            assert!(find_permission(tok).is_some(), "missing {tok}");
647        }
648    }
649
650    #[test]
651    fn unknown_token_is_none_not_critical() {
652        // Critical correctness: never silently escalate an unknown token.
653        assert!(find_permission("totally-fake-xyz").is_none());
654        assert!(find_permission("").is_none());
655    }
656
657    #[test]
658    fn risk_level_ordering() {
659        assert!(RiskLevel::Low < RiskLevel::Medium);
660        assert!(RiskLevel::Medium < RiskLevel::High);
661        assert!(RiskLevel::High < RiskLevel::Critical);
662    }
663
664    #[test]
665    fn labels_and_summaries() {
666        for lvl in [RiskLevel::Low, RiskLevel::Medium, RiskLevel::High, RiskLevel::Critical] {
667            assert!(!lvl.label().is_empty());
668            assert!(lvl.summary().len() > 20);
669        }
670    }
671}