Skip to main content

agentshield/ir/
mod.rs

1//! Unified Intermediate Representation for agent extension analysis.
2//!
3//! All adapters produce a `ScanTarget`. All detectors consume a `ScanTarget`.
4//! This decouples framework-specific parsing from security analysis.
5
6pub(crate) mod capability;
7pub mod data_surface;
8pub mod dependency_surface;
9pub mod execution_surface;
10pub mod provenance_surface;
11pub mod taint_builder;
12pub mod tool_surface;
13
14use serde::{Deserialize, Serialize};
15use std::path::PathBuf;
16
17pub use data_surface::DataSurface;
18pub use dependency_surface::DependencySurface;
19pub use execution_surface::ExecutionSurface;
20pub use provenance_surface::ProvenanceSurface;
21pub use tool_surface::{
22    Capability, CapabilityDeclaration, CapabilityDeclarationSource, CapabilityEvidence, ToolSurface,
23};
24
25/// Complete scan target — the unified IR that all analysis operates on.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ScanTarget {
28    /// Human-readable name of the extension.
29    pub name: String,
30    /// Framework that produced this target.
31    pub framework: Framework,
32    /// Root directory of the extension.
33    pub root_path: PathBuf,
34    /// Tool definitions declared by the extension.
35    pub tools: Vec<ToolSurface>,
36    /// Execution capabilities discovered in source code.
37    pub execution: ExecutionSurface,
38    /// Data flow surfaces (inputs, outputs, sources, sinks).
39    pub data: DataSurface,
40    /// Dependency information.
41    pub dependencies: DependencySurface,
42    /// Provenance metadata (author, repo, signatures).
43    pub provenance: ProvenanceSurface,
44    /// Raw source files included in the scan.
45    pub source_files: Vec<SourceFile>,
46}
47
48/// Which agent framework this extension targets.
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum Framework {
52    Mcp,
53    OpenClaw,
54    HermesAgent,
55    LangChain,
56    CrewAi,
57    GptActions,
58    CursorRules,
59    VercelAi,
60    AutoGen,
61    LlamaIndex,
62    SemanticKernel,
63    Unknown,
64}
65
66impl std::fmt::Display for Framework {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            Self::Mcp => write!(f, "MCP"),
70            Self::OpenClaw => write!(f, "OpenClaw"),
71            Self::HermesAgent => write!(f, "Hermes Agent"),
72            Self::LangChain => write!(f, "LangChain"),
73            Self::CrewAi => write!(f, "CrewAI"),
74            Self::GptActions => write!(f, "GPT Actions"),
75            Self::CursorRules => write!(f, "Cursor Rules"),
76            Self::VercelAi => write!(f, "Vercel AI SDK"),
77            Self::AutoGen => write!(f, "AutoGen"),
78            Self::LlamaIndex => write!(f, "LlamaIndex"),
79            Self::SemanticKernel => write!(f, "Semantic Kernel"),
80            Self::Unknown => write!(f, "Unknown"),
81        }
82    }
83}
84
85/// A source file included in the scan.
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct SourceFile {
88    pub path: PathBuf,
89    pub language: Language,
90    pub content: String,
91    pub size_bytes: u64,
92    pub content_hash: String,
93}
94
95/// Programming language of a source file.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
97#[serde(rename_all = "lowercase")]
98pub enum Language {
99    Python,
100    TypeScript,
101    JavaScript,
102    Shell,
103    Json,
104    Toml,
105    Yaml,
106    Markdown,
107    Unknown,
108}
109
110impl Language {
111    pub fn from_extension(ext: &str) -> Self {
112        match ext.to_lowercase().as_str() {
113            "py" => Self::Python,
114            "ts" | "tsx" => Self::TypeScript,
115            "js" | "jsx" | "mjs" | "cjs" => Self::JavaScript,
116            "sh" | "bash" | "zsh" => Self::Shell,
117            "json" => Self::Json,
118            "toml" => Self::Toml,
119            "yml" | "yaml" => Self::Yaml,
120            "md" | "markdown" => Self::Markdown,
121            _ => Self::Unknown,
122        }
123    }
124
125    pub fn is_documentation(&self) -> bool {
126        matches!(self, Self::Markdown)
127    }
128}
129
130/// Location in source code.
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
132pub struct SourceLocation {
133    pub file: PathBuf,
134    pub line: usize,
135    pub column: usize,
136    pub end_line: Option<usize>,
137    pub end_column: Option<usize>,
138}
139
140/// Where a function argument originates — the key taint abstraction.
141///
142/// Detectors don't need full taint analysis. They just need to know
143/// where a function argument came from.
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
145#[serde(rename_all = "snake_case")]
146pub enum ArgumentSource {
147    /// Hardcoded literal string — generally safe.
148    Literal(String),
149    /// Comes from function parameter — potentially user/LLM-controlled.
150    Parameter { name: String },
151    /// Comes from environment variable.
152    EnvVar { name: String },
153    /// Constructed via string formatting/concatenation — dangerous.
154    Interpolated,
155    /// Unable to determine statically.
156    Unknown,
157    /// Parameter was sanitized before being passed (e.g., via `validatePath`).
158    Sanitized { sanitizer: String },
159}
160
161/// The family of sink an argument flows into.
162///
163/// A sanitizer only neutralizes taint for the sink family it actually protects:
164/// a path validator makes a value safe for a file sink but not for a network
165/// sink, and a type coercion (`str()`/`Number()`) does not sanitize any
166/// injection sink. Detectors pass the sink they guard so a `Sanitized` argument
167/// is only treated as safe when its sanitizer category matches.
168#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
169pub enum SinkClass {
170    /// Shell/command execution.
171    Command,
172    /// Filesystem path.
173    FilePath,
174    /// Network URL/host.
175    NetworkUrl,
176    /// Dynamic code execution (eval and friends).
177    DynamicExec,
178}
179
180impl ArgumentSource {
181    /// Whether this source is potentially attacker-controlled, ignoring sink
182    /// category. Treats any `Sanitized` value as safe.
183    ///
184    /// Prefer [`ArgumentSource::is_tainted_for_sink`] in sink detectors: a
185    /// sanitizer of the wrong category (e.g. a URL validator guarding a file
186    /// path) must not suppress the finding.
187    pub fn is_tainted(&self) -> bool {
188        !matches!(self, Self::Literal(_) | Self::Sanitized { .. })
189    }
190
191    /// Whether this source is tainted for a specific sink family.
192    ///
193    /// A `Sanitized` value is safe only when its sanitizer category protects
194    /// `sink`; otherwise it stays tainted. `Literal` is always safe; every
195    /// other source is always tainted.
196    pub fn is_tainted_for_sink(&self, sink: SinkClass) -> bool {
197        match self {
198            Self::Literal(_) => false,
199            Self::Sanitized { sanitizer } => {
200                !crate::analysis::cross_file::sanitizer_allows_sink(sanitizer, sink)
201            }
202            _ => true,
203        }
204    }
205}