//! Security architectural rules. use crate::error::ArchDiagnostic; use crate::rule::{ArchRule, FnContext}; // ── SEC-001: Public function with activate inside ───────────────────────────── /// SEC-001: A @public function must not contain activate expressions. /// Unauthenticated callers could trigger graph reads, potentially leaking data. pub struct PublicFnWithActivate; impl ArchRule for PublicFnWithActivate { fn name(&self) -> &str { "SEC-001" } fn description(&self) -> &str { "@public functions must not contain activate — unauthenticated callers could trigger data reads" } fn check(&self, ctx: &FnContext<'_>) -> Vec { if !ctx.has_annotation("public") { return vec![]; } if !ctx.activate_types.is_empty() { return vec![ArchDiagnostic::error( self.name(), format!( "@public function '{}' contains activate — data leak risk for unauthenticated callers (types: {})", ctx.fn_name, ctx.activate_types.join(", ") ), Some(ctx.fn_name.to_string()), )]; } vec![] } } // ── SEC-002: sealed block inside a loop ────────────────────────────────────── /// SEC-002: A sealed block inside a loop incurs encryption overhead per iteration. pub struct SealedInLoop; impl ArchRule for SealedInLoop { fn name(&self) -> &str { "SEC-002" } fn description(&self) -> &str { "sealed blocks inside loops cause encryption overhead on every iteration" } fn check(&self, ctx: &FnContext<'_>) -> Vec { if ctx.has_sealed_in_loop { return vec![ArchDiagnostic::warning( self.name(), format!( "function '{}' contains a sealed block inside a loop — encryption overhead in hot path", ctx.fn_name ), Some(ctx.fn_name.to_string()), )]; } vec![] } } // ── SEC-003: @authenticate without @authorize on mutations ──────────────────── /// SEC-003: Functions whose name suggests mutation (create_*, update_*, delete_*) /// and carry @authenticate should also carry @authorize, otherwise authn without authz. pub struct AuthnWithoutAuthz; impl ArchRule for AuthnWithoutAuthz { fn name(&self) -> &str { "SEC-003" } fn description(&self) -> &str { "@authenticate without @authorize on mutation functions — authentication without authorization" } fn check(&self, ctx: &FnContext<'_>) -> Vec { if !ctx.has_annotation("authenticate") { return vec![]; } if ctx.has_annotation("authorize") { return vec![]; } // Heuristic: mutation function names let is_mutation = ctx.fn_name.starts_with("create_") || ctx.fn_name.starts_with("update_") || ctx.fn_name.starts_with("delete_") || ctx.fn_name.starts_with("write_") || ctx.fn_name.starts_with("mutate_"); if is_mutation { return vec![ArchDiagnostic::warning( self.name(), format!( "function '{}' has @authenticate but not @authorize — authn without authz on a mutation", ctx.fn_name ), Some(ctx.fn_name.to_string()), )]; } vec![] } }