agentsec_core/platform/mod.rs
1//! Platform probe abstraction.
2//!
3//! `agentsec-core` is designed to support multiple Agent platforms
4//! (Claude Code, Cursor, Codex, Gemini CLI, Copilot, ...). Each platform
5//! has its own config layout (target paths, dotfile decomposition rules,
6//! MCP server config schema), and the scan / registry pipeline must be
7//! able to enumerate them without baking platform-specific paths into
8//! the pipeline code.
9//!
10//! [`PlatformProbe`] is the single trait every platform implements. The
11//! scan / registry / unknown modules consume `&dyn PlatformProbe` so
12//! that adding a new platform is a matter of writing a new probe impl
13//! (typically in a sibling crate, e.g. `agentsec-platform-claude`).
14//!
15//! ## Phase 2 status
16//!
17//! `agentsec-core` now ships **no platform implementations**. The
18//! Claude Code probe lives in the sibling crate
19//! `agentsec-platform-claude`; binaries are responsible for
20//! instantiating and registering probes before invoking
21//! [`crate::scan::run`] / [`crate::scan::unknown::classify`], which
22//! take `&[&dyn PlatformProbe]` slices.
23
24use crate::Paths;
25use crate::error::Result;
26use std::path::{Path, PathBuf};
27
28/// One MCP server entry extracted from a platform-specific config file.
29///
30/// Returned by [`PlatformProbe::extract_mcp_servers`]. Carries the
31/// server name (the key under `mcpServers.<name>` in Claude Code's
32/// case) plus a display-path describing where it was found, so that
33/// downstream reporting can cite the exact source.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct McpServerEntry {
36 /// Server name (e.g. the key under `mcpServers.<name>`).
37 pub name: String,
38 /// Human-readable provenance string used in scan reports. Typically
39 /// the config file path, optionally suffixed with a scope marker
40 /// (e.g. `~/.claude.json#projects./foo.mcpServers`).
41 pub display_path: String,
42}
43
44/// One virtual sub-entry produced by [`PlatformProbe::decompose_file`].
45///
46/// Used to split a single config file (e.g. `~/.claude.json`) into
47/// security-relevant JSON blocks so that unrelated background writes
48/// (session counters, timestamps, cache) do not show as Modified noise
49/// in scan diffs. Each entry contributes one [`crate::scan::inventory::PathEntry`]
50/// keyed by `<file>#<fragment>` and hashed over `payload`.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct FragmentEntry {
53 /// Suffix appended after `#` in the virtual path. Examples:
54 /// `mcpServers`, `projects./foo.hooks`, `(no-watched-block)`.
55 pub fragment: String,
56 /// Canonical bytes to hash for this fragment. Typically the
57 /// `serde_json::to_string` of the watched JSON sub-value.
58 pub payload: Vec<u8>,
59}
60
61/// One platform's probe. Implementations are typically owned by a
62/// sibling crate (e.g. `agentsec-platform-claude::ClaudeCodePlatform`)
63/// and registered with the `agentsec` binary at startup.
64///
65/// All methods are read-only. Implementations must not mutate any
66/// path under the user's home or project tree — the scan pipeline
67/// relies on this invariant to remain safe to run repeatedly.
68pub trait PlatformProbe: Send + Sync {
69 /// Stable identifier used in scan reports and registry metadata.
70 /// Convention: kebab-case, e.g. `"claude-code"`, `"cursor"`,
71 /// `"gemini-cli"`. Must not change across versions of the probe.
72 fn id(&self) -> &'static str;
73
74 /// Inventory target roots — the (category-label, absolute-path)
75 /// pairs that [`crate::scan::inventory::collect`] walks. Category
76 /// labels appear verbatim in [`crate::scan::inventory::PathEntry`]
77 /// and downstream snapshot diffs, so probes must keep them stable
78 /// across versions for diff continuity.
79 fn target_roots(&self, paths: &Paths) -> Vec<(String, PathBuf)>;
80
81 /// Config files that may declare MCP servers. The registry /
82 /// unknown-classifier reads these to enumerate installed servers
83 /// for Typosquat detection. May overlap with [`Self::target_roots`].
84 fn mcp_config_paths(&self, paths: &Paths) -> Vec<PathBuf>;
85
86 /// Parse one MCP-server-bearing config file's contents and extract
87 /// all declared server entries. `path` is supplied so the impl can
88 /// build accurate `display_path` strings and pick the right schema
89 /// (a probe may handle multiple config layouts, e.g. global vs
90 /// per-project).
91 fn extract_mcp_servers(&self, content: &str, path: &Path) -> Result<Vec<McpServerEntry>>;
92
93 /// Optionally split a single inventory file into virtual JSON-block
94 /// fragments. Returning `Ok(None)` means "use the default whole-file
95 /// SHA-256". Returning `Ok(Some(fragments))` means
96 /// [`crate::scan::inventory::collect`] should emit one virtual
97 /// `<path>#<fragment>` entry per element instead. `category` is
98 /// the (probe-supplied) category label of the file being scanned;
99 /// impls typically gate on a specific category string (e.g.
100 /// Claude Code uses this for `local_config` = `~/.claude.json`).
101 ///
102 /// Default impl returns `Ok(None)` so every probe gets whole-file
103 /// hashing by default without boilerplate.
104 fn decompose_file(
105 &self,
106 _category: &str,
107 _path: &Path,
108 _content: &[u8],
109 ) -> Result<Option<Vec<FragmentEntry>>> {
110 Ok(None)
111 }
112
113 /// Category labels this probe considers security-critical. Diff
114 /// renderers prefix changes to these categories with a critical
115 /// marker. Default impl returns an empty slice. Labels should be
116 /// a subset of the categories returned by [`Self::target_roots`].
117 fn critical_categories(&self) -> &'static [&'static str] {
118 &[]
119 }
120}