use crate::PluginMetadata;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum PluginCapability {
ToolRegistration,
HookRegistration,
FileSystemAccess,
NetworkAccess,
ShellExecution,
SecretAccess,
}
#[derive(Debug)]
pub struct SecurityValidationResult {
pub is_valid: bool,
pub warnings: Vec<String>,
pub errors: Vec<String>,
}
pub struct PluginSecurityValidator {
blocked_names: Vec<String>,
max_capabilities: Option<usize>,
}
impl PluginSecurityValidator {
pub fn new() -> Self {
Self {
blocked_names: Vec::new(),
max_capabilities: None,
}
}
pub fn block_name(&mut self, name: impl Into<String>) {
self.blocked_names.push(name.into());
}
pub fn set_max_capabilities(&mut self, max: usize) {
self.max_capabilities = Some(max);
}
pub fn validate(&self, metadata: &PluginMetadata) -> SecurityValidationResult {
let mut errors = Vec::new();
let mut warnings = Vec::new();
if self.blocked_names.contains(&metadata.name) {
errors.push(format!("Plugin '{}' is blocked", metadata.name));
}
if metadata.name.is_empty() {
errors.push("Plugin name cannot be empty".into());
}
if metadata.version.is_empty() {
errors.push("Plugin version cannot be empty".into());
}
if let Some(max) = self.max_capabilities
&& metadata.capabilities.len() > max
{
errors.push(format!(
"Plugin requests {} capabilities (max: {})",
metadata.capabilities.len(),
max
));
}
for cap in &metadata.capabilities {
match cap {
PluginCapability::ShellExecution => {
warnings.push("Plugin requests shell execution capability".into());
}
PluginCapability::SecretAccess => {
warnings.push("Plugin requests secret/credential access".into());
}
PluginCapability::FileSystemAccess => {
warnings.push("Plugin requests filesystem access".into());
}
PluginCapability::NetworkAccess => {
warnings.push("Plugin requests network access".into());
}
_ => {}
}
}
if let Some(ref min_version) = metadata.min_core_version
&& !is_version_compatible(min_version, env!("CARGO_PKG_VERSION"))
{
errors.push(format!(
"Plugin requires core version >= {} (current: {})",
min_version,
env!("CARGO_PKG_VERSION")
));
}
let is_valid = errors.is_empty();
SecurityValidationResult {
is_valid,
warnings,
errors,
}
}
}
impl Default for PluginSecurityValidator {
fn default() -> Self {
Self::new()
}
}
fn is_version_compatible(required: &str, current: &str) -> bool {
let req_parts: Vec<u32> = required.split('.').filter_map(|p| p.parse().ok()).collect();
let cur_parts: Vec<u32> = current.split('.').filter_map(|p| p.parse().ok()).collect();
for i in 0..3 {
let req = req_parts.get(i).copied().unwrap_or(0);
let cur = cur_parts.get(i).copied().unwrap_or(0);
if cur > req {
return true;
}
if cur < req {
return false;
}
}
true }
#[cfg(test)]
mod tests {
use super::*;
fn make_metadata(name: &str, caps: Vec<PluginCapability>) -> PluginMetadata {
PluginMetadata {
name: name.into(),
version: "1.0.0".into(),
description: "Test".into(),
author: None,
min_core_version: None,
capabilities: caps,
}
}
#[test]
fn test_validate_clean_plugin() {
let validator = PluginSecurityValidator::new();
let meta = make_metadata("safe-plugin", vec![PluginCapability::ToolRegistration]);
let result = validator.validate(&meta);
assert!(result.is_valid);
assert!(result.errors.is_empty());
}
#[test]
fn test_validate_blocked_name() {
let mut validator = PluginSecurityValidator::new();
validator.block_name("evil-plugin");
let meta = make_metadata("evil-plugin", vec![]);
let result = validator.validate(&meta);
assert!(!result.is_valid);
}
#[test]
fn test_validate_empty_name() {
let validator = PluginSecurityValidator::new();
let meta = make_metadata("", vec![]);
let result = validator.validate(&meta);
assert!(!result.is_valid);
}
#[test]
fn test_validate_dangerous_capabilities_warn() {
let validator = PluginSecurityValidator::new();
let meta = make_metadata(
"risky",
vec![
PluginCapability::ShellExecution,
PluginCapability::SecretAccess,
],
);
let result = validator.validate(&meta);
assert!(result.is_valid); assert_eq!(result.warnings.len(), 2);
}
#[test]
fn test_validate_max_capabilities() {
let mut validator = PluginSecurityValidator::new();
validator.set_max_capabilities(1);
let meta = make_metadata(
"greedy",
vec![
PluginCapability::ToolRegistration,
PluginCapability::HookRegistration,
PluginCapability::NetworkAccess,
],
);
let result = validator.validate(&meta);
assert!(!result.is_valid);
}
#[test]
fn test_version_compatible() {
assert!(is_version_compatible("0.1.0", "0.1.0"));
assert!(is_version_compatible("0.1.0", "0.2.0"));
assert!(is_version_compatible("0.1.0", "1.0.0"));
assert!(!is_version_compatible("1.0.0", "0.9.0"));
assert!(!is_version_compatible("0.2.0", "0.1.9"));
}
#[test]
fn test_version_incompatible_core() {
let validator = PluginSecurityValidator::new();
let mut meta = make_metadata("new-plugin", vec![]);
meta.min_core_version = Some("999.0.0".into());
let result = validator.validate(&meta);
assert!(!result.is_valid);
}
#[test]
fn test_capability_serialization() {
let cap = PluginCapability::ShellExecution;
let json = serde_json::to_string(&cap).unwrap();
let restored: PluginCapability = serde_json::from_str(&json).unwrap();
assert_eq!(restored, PluginCapability::ShellExecution);
}
}