use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::{
FILTER_PACK_SIGNATURE_DOMAIN, require_schema, validate_b64_bounded, validate_b64_len,
validate_hex64, validate_id,
};
use crate::ValidationError;
pub const POLICY_BUNDLE_SCHEMA: &str = "leanctx.hive.policy-bundle/v1";
pub const MAX_POLICY_BUNDLE_BYTES: usize = 2 * 1024 * 1024;
pub const MAX_FILTER_PACK_STATEMENT_B64: usize = 2 * 1024 * 1024;
pub const MAX_FILTER_PACKS: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedStatementV1 {
pub statement_b64: String,
pub kid: String,
pub signature: String,
}
impl SignedStatementV1 {
pub fn validate(&self, max_encoded: usize) -> Result<(), ValidationError> {
validate_b64_bounded(&self.statement_b64, max_encoded, "statement_b64")?;
validate_id(&self.kid, "kid")?;
validate_b64_len(&self.signature, 64, "signature")
}
}
pub fn filter_pack_signing_input(statement: &[u8]) -> Vec<u8> {
let mut bytes = FILTER_PACK_SIGNATURE_DOMAIN.as_bytes().to_vec();
bytes.extend_from_slice(statement);
bytes
}
pub const FILTER_PACK_SCHEMA: &str = "leanctx.filter-pack/v1";
pub const MAX_FILTER_PATTERNS: usize = 64;
pub const MAX_FILTER_LABEL_BYTES: usize = 48;
pub const MAX_FILTER_PATTERN_CHARS: usize = 1000;
pub const MAX_FILTER_PATTERN_COMPILED_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FilterActionV1 {
Redact,
Block,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FilterPatternV1 {
pub id: String,
pub kind: String,
pub pattern: String,
pub action: FilterActionV1,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FilterPackV1 {
pub schema: String,
pub pack_id: String,
pub version: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub patterns: Vec<FilterPatternV1>,
}
fn valid_slug(value: &str, max: usize) -> bool {
!value.is_empty()
&& value.len() <= max
&& value.as_bytes()[0].is_ascii_alphanumeric()
&& value.bytes().all(|b| {
b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-')
})
}
impl FilterPatternV1 {
pub fn compile(&self) -> Result<regex::Regex, ValidationError> {
regex::RegexBuilder::new(&self.pattern)
.size_limit(MAX_FILTER_PATTERN_COMPILED_BYTES)
.dfa_size_limit(MAX_FILTER_PATTERN_COMPILED_BYTES)
.build()
.map_err(|_| ValidationError::new(format!("pattern {} does not compile", self.id)))
}
pub fn rule_label(&self) -> String {
format!("hive:{}", self.label.as_deref().unwrap_or(&self.id))
}
}
impl FilterPackV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, FILTER_PACK_SCHEMA)?;
if !valid_slug(&self.pack_id, 128) || self.version == 0 {
return Err(ValidationError::new("pack_id or version is malformed"));
}
if let Some(title) = &self.title {
super::validate_text(title, 200, "title", false)?;
}
if self.patterns.is_empty() || self.patterns.len() > MAX_FILTER_PATTERNS {
return Err(ValidationError::new("a filter pack holds 1-64 patterns"));
}
let mut ids = std::collections::BTreeSet::new();
for pattern in &self.patterns {
if !valid_slug(&pattern.id, MAX_FILTER_LABEL_BYTES) || !ids.insert(pattern.id.as_str())
{
return Err(ValidationError::new(format!(
"pattern id {:?} is malformed or repeats",
pattern.id
)));
}
if pattern.kind != "regex" {
return Err(ValidationError::new(format!(
"pattern {}: unknown kind",
pattern.id
)));
}
if pattern.pattern.is_empty()
|| pattern.pattern.chars().count() > MAX_FILTER_PATTERN_CHARS
{
return Err(ValidationError::new(format!(
"pattern {}: 1-1000 characters",
pattern.id
)));
}
if let Some(label) = &pattern.label
&& (label.len() > MAX_FILTER_LABEL_BYTES
|| !valid_slug(label, MAX_FILTER_LABEL_BYTES))
{
return Err(ValidationError::new(format!(
"pattern {}: label is malformed",
pattern.id
)));
}
pattern.compile()?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FilterPackRefV1 {
pub pack_id: String,
pub version: u64,
pub sha256: String,
pub envelope: SignedStatementV1,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RequiredFilterPackV1 {
pub pack_id: String,
pub version: u64,
pub sha256: String,
}
pub fn validate_required_filter_packs(
packs: &[RequiredFilterPackV1],
) -> Result<(), ValidationError> {
if packs.len() > MAX_FILTER_PACKS {
return Err(ValidationError::new("too many required filter packs"));
}
for (i, pack) in packs.iter().enumerate() {
if !valid_slug(&pack.pack_id, 128) || pack.version == 0 {
return Err(ValidationError::new(
"required filter pack id or version is malformed",
));
}
validate_hex64(&pack.sha256, "sha256")?;
if i > 0 && packs[i - 1].pack_id >= pack.pack_id {
return Err(ValidationError::new(
"required filter packs must be in ascending pack_id order without repeats",
));
}
}
Ok(())
}
pub fn unmet_required_packs(
required: &[RequiredFilterPackV1],
available: &[FilterPackRefV1],
) -> Vec<String> {
required
.iter()
.filter_map(
|r| match available.iter().find(|p| p.pack_id == r.pack_id) {
None => Some(format!("required filter pack {} is missing", r.pack_id)),
Some(p) if p.version != r.version => Some(format!(
"required filter pack {} is version {}, the policy requires {}",
r.pack_id, p.version, r.version
)),
Some(p) if p.sha256 != r.sha256 => Some(format!(
"required filter pack {} version {} is not the one the policy signed",
r.pack_id, r.version
)),
Some(_) => None,
},
)
.collect()
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PolicyBundleV1 {
pub schema: String,
pub policy_version: u64,
pub org_policy: Option<Value>,
pub unpin: Option<Value>,
pub filter_packs: Vec<FilterPackRefV1>,
}
impl PolicyBundleV1 {
pub fn validate(&self) -> Result<(), ValidationError> {
require_schema(&self.schema, POLICY_BUNDLE_SCHEMA)?;
for envelope in [&self.org_policy, &self.unpin].into_iter().flatten() {
if !envelope.is_object() {
return Err(ValidationError::new("policy envelopes must be objects"));
}
}
if self.filter_packs.len() > MAX_FILTER_PACKS {
return Err(ValidationError::new("too many filter packs"));
}
let mut seen = std::collections::BTreeSet::new();
for pack in &self.filter_packs {
validate_id(&pack.pack_id, "pack_id")?;
validate_hex64(&pack.sha256, "sha256")?;
pack.envelope.validate(MAX_FILTER_PACK_STATEMENT_B64)?;
if !seen.insert(pack.pack_id.as_str()) {
return Err(ValidationError::new("pack_id repeats"));
}
}
Ok(())
}
}