1use serde::{Deserialize, Serialize};
47use serde_json::Value;
48
49use super::{
50 FILTER_PACK_SIGNATURE_DOMAIN, require_schema, validate_b64_bounded, validate_b64_len,
51 validate_hex64, validate_id,
52};
53use crate::ValidationError;
54
55pub const POLICY_BUNDLE_SCHEMA: &str = "leanctx.hive.policy-bundle/v1";
56pub const MAX_POLICY_BUNDLE_BYTES: usize = 2 * 1024 * 1024;
58pub const MAX_FILTER_PACK_STATEMENT_B64: usize = 2 * 1024 * 1024;
60pub const MAX_FILTER_PACKS: usize = 64;
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct SignedStatementV1 {
65 pub statement_b64: String,
67 pub kid: String,
69 pub signature: String,
71}
72
73impl SignedStatementV1 {
74 pub fn validate(&self, max_encoded: usize) -> Result<(), ValidationError> {
75 validate_b64_bounded(&self.statement_b64, max_encoded, "statement_b64")?;
76 validate_id(&self.kid, "kid")?;
77 validate_b64_len(&self.signature, 64, "signature")
78 }
79}
80
81pub fn filter_pack_signing_input(statement: &[u8]) -> Vec<u8> {
83 let mut bytes = FILTER_PACK_SIGNATURE_DOMAIN.as_bytes().to_vec();
84 bytes.extend_from_slice(statement);
85 bytes
86}
87
88pub const FILTER_PACK_SCHEMA: &str = "leanctx.filter-pack/v1";
89pub const MAX_FILTER_PATTERNS: usize = 64;
90pub const MAX_FILTER_LABEL_BYTES: usize = 48;
92pub const MAX_FILTER_PATTERN_CHARS: usize = 1000;
93pub const MAX_FILTER_PATTERN_COMPILED_BYTES: usize = 1024 * 1024;
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
98#[serde(rename_all = "snake_case")]
99pub enum FilterActionV1 {
100 Redact,
102 Block,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(deny_unknown_fields)]
109pub struct FilterPatternV1 {
110 pub id: String,
111 pub kind: String,
113 pub pattern: String,
115 pub action: FilterActionV1,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub label: Option<String>,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123pub struct FilterPackV1 {
124 pub schema: String,
125 pub pack_id: String,
126 pub version: u64,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub title: Option<String>,
129 pub patterns: Vec<FilterPatternV1>,
130}
131
132fn valid_slug(value: &str, max: usize) -> bool {
133 !value.is_empty()
134 && value.len() <= max
135 && value.as_bytes()[0].is_ascii_alphanumeric()
136 && value.bytes().all(|b| {
137 b.is_ascii_lowercase() || b.is_ascii_digit() || matches!(b, b'.' | b'_' | b'-')
138 })
139}
140
141impl FilterPatternV1 {
142 pub fn compile(&self) -> Result<regex::Regex, ValidationError> {
144 regex::RegexBuilder::new(&self.pattern)
145 .size_limit(MAX_FILTER_PATTERN_COMPILED_BYTES)
146 .dfa_size_limit(MAX_FILTER_PATTERN_COMPILED_BYTES)
147 .build()
148 .map_err(|_| ValidationError::new(format!("pattern {} does not compile", self.id)))
149 }
150
151 pub fn rule_label(&self) -> String {
154 format!("hive:{}", self.label.as_deref().unwrap_or(&self.id))
155 }
156}
157
158impl FilterPackV1 {
159 pub fn validate(&self) -> Result<(), ValidationError> {
160 require_schema(&self.schema, FILTER_PACK_SCHEMA)?;
161 if !valid_slug(&self.pack_id, 128) || self.version == 0 {
162 return Err(ValidationError::new("pack_id or version is malformed"));
163 }
164 if let Some(title) = &self.title {
165 super::validate_text(title, 200, "title", false)?;
166 }
167 if self.patterns.is_empty() || self.patterns.len() > MAX_FILTER_PATTERNS {
168 return Err(ValidationError::new("a filter pack holds 1-64 patterns"));
169 }
170 let mut ids = std::collections::BTreeSet::new();
171 for pattern in &self.patterns {
172 if !valid_slug(&pattern.id, MAX_FILTER_LABEL_BYTES) || !ids.insert(pattern.id.as_str())
173 {
174 return Err(ValidationError::new(format!(
175 "pattern id {:?} is malformed or repeats",
176 pattern.id
177 )));
178 }
179 if pattern.kind != "regex" {
180 return Err(ValidationError::new(format!(
181 "pattern {}: unknown kind",
182 pattern.id
183 )));
184 }
185 if pattern.pattern.is_empty()
186 || pattern.pattern.chars().count() > MAX_FILTER_PATTERN_CHARS
187 {
188 return Err(ValidationError::new(format!(
189 "pattern {}: 1-1000 characters",
190 pattern.id
191 )));
192 }
193 if let Some(label) = &pattern.label
194 && (label.len() > MAX_FILTER_LABEL_BYTES
195 || !valid_slug(label, MAX_FILTER_LABEL_BYTES))
196 {
197 return Err(ValidationError::new(format!(
198 "pattern {}: label is malformed",
199 pattern.id
200 )));
201 }
202 pattern.compile()?;
203 }
204 Ok(())
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
210pub struct FilterPackRefV1 {
211 pub pack_id: String,
212 pub version: u64,
214 pub sha256: String,
216 pub envelope: SignedStatementV1,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
221#[serde(deny_unknown_fields)]
222pub struct RequiredFilterPackV1 {
223 pub pack_id: String,
224 pub version: u64,
225 pub sha256: String,
228}
229
230pub fn validate_required_filter_packs(
233 packs: &[RequiredFilterPackV1],
234) -> Result<(), ValidationError> {
235 if packs.len() > MAX_FILTER_PACKS {
236 return Err(ValidationError::new("too many required filter packs"));
237 }
238 for (i, pack) in packs.iter().enumerate() {
239 if !valid_slug(&pack.pack_id, 128) || pack.version == 0 {
240 return Err(ValidationError::new(
241 "required filter pack id or version is malformed",
242 ));
243 }
244 validate_hex64(&pack.sha256, "sha256")?;
245 if i > 0 && packs[i - 1].pack_id >= pack.pack_id {
246 return Err(ValidationError::new(
247 "required filter packs must be in ascending pack_id order without repeats",
248 ));
249 }
250 }
251 Ok(())
252}
253
254pub fn unmet_required_packs(
257 required: &[RequiredFilterPackV1],
258 available: &[FilterPackRefV1],
259) -> Vec<String> {
260 required
261 .iter()
262 .filter_map(
263 |r| match available.iter().find(|p| p.pack_id == r.pack_id) {
264 None => Some(format!("required filter pack {} is missing", r.pack_id)),
265 Some(p) if p.version != r.version => Some(format!(
266 "required filter pack {} is version {}, the policy requires {}",
267 r.pack_id, p.version, r.version
268 )),
269 Some(p) if p.sha256 != r.sha256 => Some(format!(
270 "required filter pack {} version {} is not the one the policy signed",
271 r.pack_id, r.version
272 )),
273 Some(_) => None,
274 },
275 )
276 .collect()
277}
278
279#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
281pub struct PolicyBundleV1 {
282 pub schema: String,
283 pub policy_version: u64,
285 pub org_policy: Option<Value>,
286 pub unpin: Option<Value>,
287 pub filter_packs: Vec<FilterPackRefV1>,
288}
289
290impl PolicyBundleV1 {
291 pub fn validate(&self) -> Result<(), ValidationError> {
292 require_schema(&self.schema, POLICY_BUNDLE_SCHEMA)?;
293 for envelope in [&self.org_policy, &self.unpin].into_iter().flatten() {
294 if !envelope.is_object() {
295 return Err(ValidationError::new("policy envelopes must be objects"));
296 }
297 }
298 if self.filter_packs.len() > MAX_FILTER_PACKS {
299 return Err(ValidationError::new("too many filter packs"));
300 }
301 let mut seen = std::collections::BTreeSet::new();
302 for pack in &self.filter_packs {
303 validate_id(&pack.pack_id, "pack_id")?;
304 validate_hex64(&pack.sha256, "sha256")?;
305 pack.envelope.validate(MAX_FILTER_PACK_STATEMENT_B64)?;
306 if !seen.insert(pack.pack_id.as_str()) {
307 return Err(ValidationError::new("pack_id repeats"));
308 }
309 }
310 Ok(())
311 }
312}