chore: seed sgclaw rust baseline

This commit is contained in:
zyl
2026-03-25 02:17:55 +00:00
parent 5063adc530
commit 8757bbb266
26 changed files with 2825 additions and 0 deletions

111
src/security/mac_policy.rs Normal file
View File

@@ -0,0 +1,111 @@
use std::fs;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::pipe::Action;
use crate::security::SecurityError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MacPolicy {
pub version: String,
pub domains: DomainRules,
pub pipe_actions: PipeActionRules,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DomainRules {
pub allowed: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipeActionRules {
pub allowed: Vec<String>,
#[serde(default)]
pub blocked: Vec<String>,
}
impl MacPolicy {
pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, SecurityError> {
let contents = fs::read_to_string(path)?;
let policy: Self = serde_json::from_str(&contents)?;
policy.validate_rules()?;
Ok(policy)
}
pub fn from_json_str(contents: &str) -> Result<Self, SecurityError> {
let policy: Self = serde_json::from_str(contents)?;
policy.validate_rules()?;
Ok(policy)
}
pub fn validate(&self, action: &Action, expected_domain: &str) -> Result<(), SecurityError> {
let action_name = action.as_str();
if self
.pipe_actions
.blocked
.iter()
.any(|blocked| blocked == action_name)
{
return Err(SecurityError::ActionNotAllowed(action_name.to_string()));
}
if !self
.pipe_actions
.allowed
.iter()
.any(|allowed| allowed == action_name)
{
return Err(SecurityError::ActionNotAllowed(action_name.to_string()));
}
let normalized = normalize_domain(expected_domain);
if normalized.is_empty() {
return Err(SecurityError::DomainNotAllowed(expected_domain.to_string()));
}
if !self
.domains
.allowed
.iter()
.map(|domain| normalize_domain(domain))
.any(|allowed| allowed == normalized)
{
return Err(SecurityError::DomainNotAllowed(normalized));
}
Ok(())
}
fn validate_rules(&self) -> Result<(), SecurityError> {
if self.version.trim().is_empty() {
return Err(SecurityError::InvalidRules(
"rules version must not be empty".to_string(),
));
}
if self.domains.allowed.is_empty() {
return Err(SecurityError::InvalidRules(
"at least one allowed domain is required".to_string(),
));
}
if self.pipe_actions.allowed.is_empty() {
return Err(SecurityError::InvalidRules(
"at least one allowed action is required".to_string(),
));
}
Ok(())
}
}
fn normalize_domain(raw: &str) -> String {
raw.trim()
.trim_start_matches("https://")
.trim_start_matches("http://")
.split('/')
.next()
.unwrap_or_default()
.split(':')
.next()
.unwrap_or_default()
.to_ascii_lowercase()
}