agentsec-core 0.4.0

AgentSec core library — scan / web / paste logic, pure Rust
Documentation
//! Platform probe abstraction.
//!
//! `agentsec-core` is designed to support multiple Agent platforms
//! (Claude Code, Cursor, Codex, Gemini CLI, Copilot, ...). Each platform
//! has its own config layout (target paths, dotfile decomposition rules,
//! MCP server config schema), and the scan / registry pipeline must be
//! able to enumerate them without baking platform-specific paths into
//! the pipeline code.
//!
//! [`PlatformProbe`] is the single trait every platform implements. The
//! scan / registry / unknown modules consume `&dyn PlatformProbe` so
//! that adding a new platform is a matter of writing a new probe impl
//! (typically in a sibling crate, e.g. `agentsec-platform-claude`).
//!
//! ## Phase 2 status
//!
//! `agentsec-core` now ships **no platform implementations**. The
//! Claude Code probe lives in the sibling crate
//! `agentsec-platform-claude`; binaries are responsible for
//! instantiating and registering probes before invoking
//! [`crate::scan::run`] / [`crate::scan::unknown::classify`], which
//! take `&[&dyn PlatformProbe]` slices.

use crate::Paths;
use crate::error::Result;
use std::path::{Path, PathBuf};

/// One MCP server entry extracted from a platform-specific config file.
///
/// Returned by [`PlatformProbe::extract_mcp_servers`]. Carries the
/// server name (the key under `mcpServers.<name>` in Claude Code's
/// case) plus a display-path describing where it was found, so that
/// downstream reporting can cite the exact source.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct McpServerEntry {
    /// Server name (e.g. the key under `mcpServers.<name>`).
    pub name: String,
    /// Human-readable provenance string used in scan reports. Typically
    /// the config file path, optionally suffixed with a scope marker
    /// (e.g. `~/.claude.json#projects./foo.mcpServers`).
    pub display_path: String,
}

/// One virtual sub-entry produced by [`PlatformProbe::decompose_file`].
///
/// Used to split a single config file (e.g. `~/.claude.json`) into
/// security-relevant JSON blocks so that unrelated background writes
/// (session counters, timestamps, cache) do not show as Modified noise
/// in scan diffs. Each entry contributes one [`crate::scan::inventory::PathEntry`]
/// keyed by `<file>#<fragment>` and hashed over `payload`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FragmentEntry {
    /// Suffix appended after `#` in the virtual path. Examples:
    /// `mcpServers`, `projects./foo.hooks`, `(no-watched-block)`.
    pub fragment: String,
    /// Canonical bytes to hash for this fragment. Typically the
    /// `serde_json::to_string` of the watched JSON sub-value.
    pub payload: Vec<u8>,
}

/// One platform's probe. Implementations are typically owned by a
/// sibling crate (e.g. `agentsec-platform-claude::ClaudeCodePlatform`)
/// and registered with the `agentsec` binary at startup.
///
/// All methods are read-only. Implementations must not mutate any
/// path under the user's home or project tree — the scan pipeline
/// relies on this invariant to remain safe to run repeatedly.
pub trait PlatformProbe: Send + Sync {
    /// Stable identifier used in scan reports and registry metadata.
    /// Convention: kebab-case, e.g. `"claude-code"`, `"cursor"`,
    /// `"gemini-cli"`. Must not change across versions of the probe.
    fn id(&self) -> &'static str;

    /// Inventory target roots — the (category-label, absolute-path)
    /// pairs that [`crate::scan::inventory::collect`] walks. Category
    /// labels appear verbatim in [`crate::scan::inventory::PathEntry`]
    /// and downstream snapshot diffs, so probes must keep them stable
    /// across versions for diff continuity.
    fn target_roots(&self, paths: &Paths) -> Vec<(String, PathBuf)>;

    /// Config files that may declare MCP servers. The registry /
    /// unknown-classifier reads these to enumerate installed servers
    /// for Typosquat detection. May overlap with [`Self::target_roots`].
    fn mcp_config_paths(&self, paths: &Paths) -> Vec<PathBuf>;

    /// Parse one MCP-server-bearing config file's contents and extract
    /// all declared server entries. `path` is supplied so the impl can
    /// build accurate `display_path` strings and pick the right schema
    /// (a probe may handle multiple config layouts, e.g. global vs
    /// per-project).
    fn extract_mcp_servers(&self, content: &str, path: &Path) -> Result<Vec<McpServerEntry>>;

    /// Optionally split a single inventory file into virtual JSON-block
    /// fragments. Returning `Ok(None)` means "use the default whole-file
    /// SHA-256". Returning `Ok(Some(fragments))` means
    /// [`crate::scan::inventory::collect`] should emit one virtual
    /// `<path>#<fragment>` entry per element instead. `category` is
    /// the (probe-supplied) category label of the file being scanned;
    /// impls typically gate on a specific category string (e.g.
    /// Claude Code uses this for `local_config` = `~/.claude.json`).
    ///
    /// Default impl returns `Ok(None)` so every probe gets whole-file
    /// hashing by default without boilerplate.
    fn decompose_file(
        &self,
        _category: &str,
        _path: &Path,
        _content: &[u8],
    ) -> Result<Option<Vec<FragmentEntry>>> {
        Ok(None)
    }

    /// Category labels this probe considers security-critical. Diff
    /// renderers prefix changes to these categories with a critical
    /// marker. Default impl returns an empty slice. Labels should be
    /// a subset of the categories returned by [`Self::target_roots`].
    fn critical_categories(&self) -> &'static [&'static str] {
        &[]
    }
}