1use serde::{Deserialize, Serialize};
11
12use super::{
13 FILTER_PACK_SIGNATURE_DOMAIN, require_schema, validate_b64_bounded, validate_b64_len,
14 validate_hex64, validate_id,
15};
16use crate::ValidationError;
17
18pub const POLICY_BUNDLE_SCHEMA: &str = "leanctx.hive.policy-bundle/v1";
19pub const MAX_POLICY_BUNDLE_BYTES: usize = 2 * 1024 * 1024;
21pub const MAX_FILTER_PACK_STATEMENT_B64: usize = 2 * 1024 * 1024;
23pub const MAX_FILTER_PACKS: usize = 64;
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct SignedStatementV1 {
28 pub statement_b64: String,
30 pub kid: String,
32 pub signature: String,
34}
35
36impl SignedStatementV1 {
37 pub fn validate(&self, max_encoded: usize) -> Result<(), ValidationError> {
38 validate_b64_bounded(&self.statement_b64, max_encoded, "statement_b64")?;
39 validate_id(&self.kid, "kid")?;
40 validate_b64_len(&self.signature, 64, "signature")
41 }
42}
43
44pub fn filter_pack_signing_input(statement: &[u8]) -> Vec<u8> {
46 let mut bytes = FILTER_PACK_SIGNATURE_DOMAIN.as_bytes().to_vec();
47 bytes.extend_from_slice(statement);
48 bytes
49}
50
51pub const FILTER_PACK_SCHEMA: &str = "leanctx.filter-pack/v1";
52pub const MAX_FILTER_PATTERNS: usize = 64;
53pub const MAX_FILTER_LABEL_BYTES: usize = 48;
55pub const MAX_FILTER_PATTERN_CHARS: usize = 1000;
56pub const MAX_FILTER_PATTERN_COMPILED_BYTES: usize = 1024 * 1024;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum FilterActionV1 {
63 Redact,
65 Block,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(deny_unknown_fields)]
72pub struct FilterPatternV1 {
73 pub id: String,
74 pub kind: String,
76 pub pattern: String,
78 pub action: FilterActionV1,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
80 pub label: Option<String>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct FilterPackV1 {
87 pub schema: String,
88 pub pack_id: String,
89 pub version: u64,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
91 pub title: Option<String>,
92 pub patterns: Vec<FilterPatternV1>,
93}
94
95fn valid_slug(value: &str, max: usize) -> bool {
96 !value.is_empty()
97 && value.len() <= max
98 && value.as_bytes()[0].is_ascii_alphanumeric()
99 && value.bytes().all(|b| {
100 b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-')
101 })
102}
103
104impl FilterPatternV1 {
105 pub fn compile(&self) -> Result<regex::Regex, ValidationError> {
107 regex::RegexBuilder::new(&self.pattern)
108 .size_limit(MAX_FILTER_PATTERN_COMPILED_BYTES)
109 .dfa_size_limit(MAX_FILTER_PATTERN_COMPILED_BYTES)
110 .build()
111 .map_err(|_| ValidationError::new(format!("pattern {} does not compile", self.id)))
112 }
113
114 pub fn rule_label(&self) -> String {
117 format!("hive:{}", self.label.as_deref().unwrap_or(&self.id))
118 }
119}
120
121impl FilterPackV1 {
122 pub fn validate(&self) -> Result<(), ValidationError> {
123 require_schema(&self.schema, FILTER_PACK_SCHEMA)?;
124 if !valid_slug(&self.pack_id, 128) || self.version == 0 {
125 return Err(ValidationError::new("pack_id or version is malformed"));
126 }
127 if let Some(title) = &self.title {
128 super::validate_text(title, 200, "title", false)?;
129 }
130 if self.patterns.is_empty() || self.patterns.len() > MAX_FILTER_PATTERNS {
131 return Err(ValidationError::new("a filter pack holds 1-64 patterns"));
132 }
133 let mut ids = std::collections::BTreeSet::new();
134 for pattern in &self.patterns {
135 if !valid_slug(&pattern.id, MAX_FILTER_LABEL_BYTES) || !ids.insert(pattern.id.as_str())
136 {
137 return Err(ValidationError::new(format!(
138 "pattern id {:?} is malformed or repeats",
139 pattern.id
140 )));
141 }
142 if pattern.kind != "regex" {
143 return Err(ValidationError::new(format!(
144 "pattern {}: unknown kind",
145 pattern.id
146 )));
147 }
148 if pattern.pattern.is_empty()
149 || pattern.pattern.chars().count() > MAX_FILTER_PATTERN_CHARS
150 {
151 return Err(ValidationError::new(format!(
152 "pattern {}: 1-1000 characters",
153 pattern.id
154 )));
155 }
156 if let Some(label) = &pattern.label
157 && (label.len() > MAX_FILTER_LABEL_BYTES
158 || !valid_slug(label, MAX_FILTER_LABEL_BYTES))
159 {
160 return Err(ValidationError::new(format!(
161 "pattern {}: label is malformed",
162 pattern.id
163 )));
164 }
165 pattern.compile()?;
166 }
167 Ok(())
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
173pub struct FilterPackRefV1 {
174 pub pack_id: String,
175 pub version: u64,
177 pub sha256: String,
179 pub envelope: SignedStatementV1,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(deny_unknown_fields)]
187pub struct RequiredFilterPackV1 {
188 pub pack_id: String,
189 pub version: u64,
190 pub sha256: String,
193}
194
195pub fn validate_required_filter_packs(
199 packs: &[RequiredFilterPackV1],
200) -> Result<(), ValidationError> {
201 if packs.len() > MAX_FILTER_PACKS {
202 return Err(ValidationError::new("too many required filter packs"));
203 }
204 for (i, pack) in packs.iter().enumerate() {
205 if !valid_slug(&pack.pack_id, 128) || pack.version == 0 {
206 return Err(ValidationError::new(
207 "required filter pack id or version is malformed",
208 ));
209 }
210 validate_hex64(&pack.sha256, "sha256")?;
211 if i > 0 && packs[i - 1].pack_id >= pack.pack_id {
212 return Err(ValidationError::new(
213 "required filter packs must be in ascending pack_id order without repeats",
214 ));
215 }
216 }
217 Ok(())
218}
219
220#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
222pub struct PolicyBundleV1 {
223 pub schema: String,
224 pub filter_packs: Vec<FilterPackRefV1>,
225}
226
227impl PolicyBundleV1 {
228 pub fn validate(&self) -> Result<(), ValidationError> {
229 require_schema(&self.schema, POLICY_BUNDLE_SCHEMA)?;
230 if self.filter_packs.len() > MAX_FILTER_PACKS {
231 return Err(ValidationError::new("too many filter packs"));
232 }
233 let mut seen = std::collections::BTreeSet::new();
234 for pack in &self.filter_packs {
235 validate_id(&pack.pack_id, "pack_id")?;
236 validate_hex64(&pack.sha256, "sha256")?;
237 pack.envelope.validate(MAX_FILTER_PACK_STATEMENT_B64)?;
238 if !seen.insert(pack.pack_id.as_str()) {
239 return Err(ValidationError::new("pack_id repeats"));
240 }
241 }
242 Ok(())
243 }
244}