Skip to main content

cargo_capsec/
authorities.rs

1//! The authority registry — a structured catalogue of every standard library and common
2//! third-party function that exercises ambient authority.
3//!
4//! This is the core knowledge base of `cargo-capsec`. Each entry maps a call pattern
5//! (like `std::fs::read` or `TcpStream::connect`) to a [`Category`], [`Risk`] level,
6//! and human-readable description.
7//!
8//! The registry is compiled into the binary via [`build_registry`]. Users can extend it
9//! at runtime with custom patterns loaded from `.capsec.toml` (see [`CustomAuthority`]).
10
11use serde::Serialize;
12
13/// The kind of ambient authority a call exercises.
14///
15/// Every finding is classified into exactly one category. The detector uses this
16/// to group and color-code output, and users can filter by category in `.capsec.toml`.
17///
18/// # Variants
19///
20/// | Category | What it covers | Color in output |
21/// |----------|---------------|-----------------|
22/// | `Fs` | Filesystem reads, writes, deletes | Blue |
23/// | `Net` | TCP/UDP connections, HTTP requests, listeners | Red |
24/// | `Env` | Environment variable access | Yellow |
25/// | `Process` | Subprocess spawning (`Command::new`) | Magenta |
26/// | `Ffi` | Foreign function interface (`extern` blocks) | Cyan |
27#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
28#[non_exhaustive]
29pub enum Category {
30    /// Filesystem access: reads, writes, deletes, directory operations.
31    Fs,
32    /// Network access: TCP/UDP connections, listeners, HTTP clients.
33    Net,
34    /// Environment variable reads and writes.
35    Env,
36    /// Subprocess spawning and execution.
37    Process,
38    /// Foreign function interface — `extern` blocks that bypass Rust's safety model.
39    Ffi,
40}
41
42impl Category {
43    /// Returns the short uppercase label used in text output (e.g., `"FS"`, `"NET"`).
44    pub fn label(&self) -> &'static str {
45        match self {
46            Self::Fs => "FS",
47            Self::Net => "NET",
48            Self::Env => "ENV",
49            Self::Process => "PROC",
50            Self::Ffi => "FFI",
51        }
52    }
53}
54
55/// How dangerous a particular ambient authority call is.
56///
57/// Risk levels are ordered: `Low < Medium < High < Critical`. The CLI's `--min-risk`
58/// and `--fail-on` flags use this ordering to filter and gate findings.
59///
60/// # Assignment rationale
61///
62/// | Level | Meaning | Examples |
63/// |-------|---------|----------|
64/// | `Low` | Read-only metadata, unlikely to leak secrets | `fs::metadata`, `env::current_dir` |
65/// | `Medium` | Can read data or create resources | `fs::read`, `env::var`, `File::open` |
66/// | `High` | Can write, delete, or open network connections | `fs::write`, `TcpStream::connect` |
67/// | `Critical` | Can destroy data or execute arbitrary code | `remove_dir_all`, `Command::new` |
68#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
69#[non_exhaustive]
70pub enum Risk {
71    /// Read-only metadata or low-impact queries.
72    Low,
73    /// Can read sensitive data or create resources.
74    Medium,
75    /// Can write data, delete files, or open network connections.
76    High,
77    /// Can destroy data or execute arbitrary code.
78    Critical,
79}
80
81impl Risk {
82    /// Returns the lowercase label used in JSON output and CLI flags (e.g., `"high"`).
83    pub fn label(&self) -> &'static str {
84        match self {
85            Self::Low => "low",
86            Self::Medium => "medium",
87            Self::High => "high",
88            Self::Critical => "critical",
89        }
90    }
91
92    /// Parses a risk level from a string. Returns [`Risk::Low`] for unrecognized input.
93    ///
94    /// Accepts: `"low"`, `"medium"`, `"high"`, `"critical"`.
95    pub fn parse(s: &str) -> Self {
96        match s {
97            "low" => Self::Low,
98            "medium" => Self::Medium,
99            "high" => Self::High,
100            "critical" => Self::Critical,
101            _ => Self::Low,
102        }
103    }
104}
105
106/// A single entry in the authority registry.
107///
108/// Each `Authority` describes one way that Rust code can exercise ambient authority
109/// over the host system. The [`Detector`](crate::detector::Detector) matches parsed
110/// call sites against these entries to produce [`Finding`](crate::detector::Finding)s.
111#[derive(Debug, Clone)]
112pub struct Authority {
113    /// How to match this authority against a call site.
114    pub pattern: AuthorityPattern,
115    /// What kind of ambient authority this is.
116    pub category: Category,
117    /// Finer-grained classification within the category (e.g., `"read"`, `"write"`, `"connect"`).
118    pub subcategory: &'static str,
119    /// How dangerous this call is.
120    pub risk: Risk,
121    /// Human-readable description shown in audit output.
122    pub description: &'static str,
123}
124
125/// How an [`Authority`] matches against parsed call sites.
126///
127/// There are two matching strategies, reflecting the two kinds of calls in Rust:
128///
129/// - **Path matching** catches qualified calls like `std::fs::read(...)` or `File::open(...)`
130/// - **Contextual method matching** catches method calls like `.output()` or `.spawn()`,
131///   but only when a related path call (like `Command::new`) appears in the same function.
132///   This eliminates false positives from common method names.
133#[derive(Debug, Clone)]
134pub enum AuthorityPattern {
135    /// Match a fully qualified path by suffix.
136    ///
137    /// The call's expanded path must *end with* these segments. For example,
138    /// `&["std", "fs", "read"]` matches both `std::fs::read(...)` and a bare `read(...)`
139    /// that was imported via `use std::fs::read`.
140    Path(&'static [&'static str]),
141
142    /// Match a method call, but only if the same function also contains a call
143    /// matching `requires_path`.
144    ///
145    /// This is the co-occurrence heuristic that prevents `.status()` on an HTTP response
146    /// from being flagged as subprocess execution. The method only fires when the
147    /// required context (e.g., `Command::new`) is present in the same function body.
148    MethodWithContext {
149        /// The method name to match (e.g., `"output"`, `"spawn"`, `"send_to"`).
150        method: &'static str,
151        /// A path pattern that must also appear in the same function for this match to fire.
152        requires_path: &'static [&'static str],
153    },
154}
155
156/// A user-defined authority pattern loaded from `.capsec.toml`.
157///
158/// Custom authorities let teams flag project-specific I/O entry points that the
159/// built-in registry doesn't cover — database query functions, internal RPC clients,
160/// secret-fetching utilities, etc.
161///
162/// # Example `.capsec.toml`
163///
164/// ```toml
165/// [[authority]]
166/// path = ["my_crate", "secrets", "fetch"]
167/// category = "net"
168/// risk = "critical"
169/// description = "Fetches secrets from vault"
170/// ```
171#[derive(Debug, Clone)]
172pub struct CustomAuthority {
173    /// Path segments to match by suffix (e.g., `["my_crate", "secrets", "fetch"]`).
174    pub path: Vec<String>,
175    /// What kind of ambient authority this is.
176    pub category: Category,
177    /// How dangerous this call is.
178    pub risk: Risk,
179    /// Human-readable description shown in audit output.
180    pub description: String,
181}
182
183/// Builds the compiled-in authority registry.
184///
185/// Returns every known ambient authority pattern for the Rust standard library
186/// and popular third-party crates (tokio, reqwest, hyper). The registry contains
187/// 35+ entries covering filesystem, network, environment, process, and FFI patterns.
188///
189/// This is called once at startup by [`Detector::new`](crate::detector::Detector::new).
190/// Users can extend it with [`CustomAuthority`] entries from `.capsec.toml`.
191pub fn build_registry() -> Vec<Authority> {
192    vec![
193        //  Filesystem: std
194        Authority {
195            pattern: AuthorityPattern::Path(&["std", "fs", "read"]),
196            category: Category::Fs,
197            subcategory: "read",
198            risk: Risk::Medium,
199            description: "Read arbitrary file contents",
200        },
201        Authority {
202            pattern: AuthorityPattern::Path(&["std", "fs", "read_to_string"]),
203            category: Category::Fs,
204            subcategory: "read",
205            risk: Risk::Medium,
206            description: "Read arbitrary file contents as string",
207        },
208        Authority {
209            pattern: AuthorityPattern::Path(&["std", "fs", "read_dir"]),
210            category: Category::Fs,
211            subcategory: "read",
212            risk: Risk::Low,
213            description: "List directory contents",
214        },
215        Authority {
216            pattern: AuthorityPattern::Path(&["std", "fs", "metadata"]),
217            category: Category::Fs,
218            subcategory: "read",
219            risk: Risk::Low,
220            description: "Read file metadata",
221        },
222        Authority {
223            pattern: AuthorityPattern::Path(&["std", "fs", "write"]),
224            category: Category::Fs,
225            subcategory: "write",
226            risk: Risk::High,
227            description: "Write arbitrary file contents",
228        },
229        Authority {
230            pattern: AuthorityPattern::Path(&["std", "fs", "create_dir_all"]),
231            category: Category::Fs,
232            subcategory: "write",
233            risk: Risk::Medium,
234            description: "Create directory tree",
235        },
236        Authority {
237            pattern: AuthorityPattern::Path(&["std", "fs", "remove_file"]),
238            category: Category::Fs,
239            subcategory: "write",
240            risk: Risk::High,
241            description: "Delete a file",
242        },
243        Authority {
244            pattern: AuthorityPattern::Path(&["std", "fs", "remove_dir_all"]),
245            category: Category::Fs,
246            subcategory: "write",
247            risk: Risk::Critical,
248            description: "Recursively delete a directory tree",
249        },
250        Authority {
251            pattern: AuthorityPattern::Path(&["std", "fs", "rename"]),
252            category: Category::Fs,
253            subcategory: "write",
254            risk: Risk::Medium,
255            description: "Rename/move a file",
256        },
257        Authority {
258            pattern: AuthorityPattern::Path(&["std", "fs", "copy"]),
259            category: Category::Fs,
260            subcategory: "read+write",
261            risk: Risk::Medium,
262            description: "Copy a file",
263        },
264        //  Filesystem: File
265        Authority {
266            pattern: AuthorityPattern::Path(&["File", "open"]),
267            category: Category::Fs,
268            subcategory: "read",
269            risk: Risk::Medium,
270            description: "Open a file for reading",
271        },
272        Authority {
273            pattern: AuthorityPattern::Path(&["File", "create"]),
274            category: Category::Fs,
275            subcategory: "write",
276            risk: Risk::High,
277            description: "Create/truncate a file for writing",
278        },
279        Authority {
280            pattern: AuthorityPattern::Path(&["OpenOptions", "open"]),
281            category: Category::Fs,
282            subcategory: "read+write",
283            risk: Risk::Medium,
284            description: "Open file with custom options",
285        },
286        //  Filesystem: tokio
287        Authority {
288            pattern: AuthorityPattern::Path(&["tokio", "fs", "read"]),
289            category: Category::Fs,
290            subcategory: "read",
291            risk: Risk::Medium,
292            description: "Async read file contents",
293        },
294        Authority {
295            pattern: AuthorityPattern::Path(&["tokio", "fs", "read_to_string"]),
296            category: Category::Fs,
297            subcategory: "read",
298            risk: Risk::Medium,
299            description: "Async read file as string",
300        },
301        Authority {
302            pattern: AuthorityPattern::Path(&["tokio", "fs", "write"]),
303            category: Category::Fs,
304            subcategory: "write",
305            risk: Risk::High,
306            description: "Async write file contents",
307        },
308        Authority {
309            pattern: AuthorityPattern::Path(&["tokio", "fs", "remove_file"]),
310            category: Category::Fs,
311            subcategory: "write",
312            risk: Risk::High,
313            description: "Async delete a file",
314        },
315        //  Network: std
316        Authority {
317            pattern: AuthorityPattern::Path(&["TcpStream", "connect"]),
318            category: Category::Net,
319            subcategory: "connect",
320            risk: Risk::High,
321            description: "Open outbound TCP connection",
322        },
323        Authority {
324            pattern: AuthorityPattern::Path(&["TcpListener", "bind"]),
325            category: Category::Net,
326            subcategory: "bind",
327            risk: Risk::High,
328            description: "Bind a TCP listener to a port",
329        },
330        Authority {
331            pattern: AuthorityPattern::Path(&["UdpSocket", "bind"]),
332            category: Category::Net,
333            subcategory: "bind",
334            risk: Risk::High,
335            description: "Bind a UDP socket",
336        },
337        // send_to only flagged if UdpSocket::bind is in the same function
338        Authority {
339            pattern: AuthorityPattern::MethodWithContext {
340                method: "send_to",
341                requires_path: &["UdpSocket", "bind"],
342            },
343            category: Category::Net,
344            subcategory: "connect",
345            risk: Risk::High,
346            description: "Send UDP datagram to address",
347        },
348        //  Network: tokio
349        Authority {
350            pattern: AuthorityPattern::Path(&["tokio", "net", "TcpStream", "connect"]),
351            category: Category::Net,
352            subcategory: "connect",
353            risk: Risk::High,
354            description: "Async outbound TCP connection",
355        },
356        Authority {
357            pattern: AuthorityPattern::Path(&["tokio", "net", "TcpListener", "bind"]),
358            category: Category::Net,
359            subcategory: "bind",
360            risk: Risk::High,
361            description: "Async bind TCP listener",
362        },
363        //  Network: reqwest
364        Authority {
365            pattern: AuthorityPattern::Path(&["reqwest", "get"]),
366            category: Category::Net,
367            subcategory: "connect",
368            risk: Risk::High,
369            description: "HTTP GET request",
370        },
371        Authority {
372            pattern: AuthorityPattern::Path(&["reqwest", "Client", "new"]),
373            category: Category::Net,
374            subcategory: "connect",
375            risk: Risk::Medium,
376            description: "Create HTTP client",
377        },
378        Authority {
379            pattern: AuthorityPattern::Path(&["reqwest", "Client", "get"]),
380            category: Category::Net,
381            subcategory: "connect",
382            risk: Risk::High,
383            description: "HTTP GET via client",
384        },
385        Authority {
386            pattern: AuthorityPattern::Path(&["reqwest", "Client", "post"]),
387            category: Category::Net,
388            subcategory: "connect",
389            risk: Risk::High,
390            description: "HTTP POST via client",
391        },
392        //  Network: hyper
393        Authority {
394            pattern: AuthorityPattern::Path(&["hyper", "Client", "request"]),
395            category: Category::Net,
396            subcategory: "connect",
397            risk: Risk::High,
398            description: "Hyper HTTP request",
399        },
400        Authority {
401            pattern: AuthorityPattern::Path(&["hyper", "Server", "bind"]),
402            category: Category::Net,
403            subcategory: "bind",
404            risk: Risk::High,
405            description: "Bind Hyper HTTP server",
406        },
407        //  Environment
408        // (no duplicate ["env", "var"] — import expansion handles `use std::env`)
409        Authority {
410            pattern: AuthorityPattern::Path(&["std", "env", "var"]),
411            category: Category::Env,
412            subcategory: "read",
413            risk: Risk::Medium,
414            description: "Read environment variable",
415        },
416        Authority {
417            pattern: AuthorityPattern::Path(&["std", "env", "vars"]),
418            category: Category::Env,
419            subcategory: "read",
420            risk: Risk::Medium,
421            description: "Read all environment variables",
422        },
423        Authority {
424            pattern: AuthorityPattern::Path(&["std", "env", "set_var"]),
425            category: Category::Env,
426            subcategory: "write",
427            risk: Risk::High,
428            description: "Modify environment variable",
429        },
430        Authority {
431            pattern: AuthorityPattern::Path(&["std", "env", "remove_var"]),
432            category: Category::Env,
433            subcategory: "write",
434            risk: Risk::Medium,
435            description: "Remove environment variable",
436        },
437        Authority {
438            pattern: AuthorityPattern::Path(&["std", "env", "current_dir"]),
439            category: Category::Env,
440            subcategory: "read",
441            risk: Risk::Low,
442            description: "Read current working directory",
443        },
444        Authority {
445            pattern: AuthorityPattern::Path(&["std", "env", "set_current_dir"]),
446            category: Category::Env,
447            subcategory: "write",
448            risk: Risk::High,
449            description: "Change working directory",
450        },
451        //  Process
452        Authority {
453            pattern: AuthorityPattern::Path(&["Command", "new"]),
454            category: Category::Process,
455            subcategory: "spawn",
456            risk: Risk::Critical,
457            description: "Create command for subprocess execution",
458        },
459        // .output(), .spawn(), .status() only flagged if Command::new is in the same function
460        Authority {
461            pattern: AuthorityPattern::MethodWithContext {
462                method: "output",
463                requires_path: &["Command", "new"],
464            },
465            category: Category::Process,
466            subcategory: "spawn",
467            risk: Risk::Critical,
468            description: "Execute subprocess and capture output",
469        },
470        Authority {
471            pattern: AuthorityPattern::MethodWithContext {
472                method: "spawn",
473                requires_path: &["Command", "new"],
474            },
475            category: Category::Process,
476            subcategory: "spawn",
477            risk: Risk::Critical,
478            description: "Spawn subprocess",
479        },
480        Authority {
481            pattern: AuthorityPattern::MethodWithContext {
482                method: "status",
483                requires_path: &["Command", "new"],
484            },
485            category: Category::Process,
486            subcategory: "spawn",
487            risk: Risk::Critical,
488            description: "Execute subprocess and get exit status",
489        },
490    ]
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496
497    #[test]
498    fn registry_is_not_empty() {
499        let reg = build_registry();
500        assert!(reg.len() > 30);
501    }
502
503    #[test]
504    fn all_categories_represented() {
505        let reg = build_registry();
506        let cats: std::collections::HashSet<_> = reg.iter().map(|a| &a.category).collect();
507        assert!(cats.contains(&Category::Fs));
508        assert!(cats.contains(&Category::Net));
509        assert!(cats.contains(&Category::Env));
510        assert!(cats.contains(&Category::Process));
511    }
512
513    #[test]
514    fn risk_ordering() {
515        assert!(Risk::Low < Risk::Medium);
516        assert!(Risk::Medium < Risk::High);
517        assert!(Risk::High < Risk::Critical);
518    }
519}