Skip to main content

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}