1use crate::evidence_envelope::{
2 EvidenceEnvelopeV1, EvidenceTargetV1, ExitClassV1, InputFingerprintV1, PayloadSchemaRefV1,
3 PayloadSchemaStabilityV1,
4};
5use serde::{Deserialize, Serialize, de};
6use std::{collections::BTreeMap, path::Path};
7use thiserror::Error as ThisError;
8
9#[derive(Debug, ThisError)]
13pub enum PolicyGateError {
14 #[error("invalid policy: {0}")]
15 InvalidPolicy(String),
16
17 #[error("failed to parse policy TOML: {0}")]
18 Toml(#[from] toml::de::Error),
19
20 #[error("failed to parse evidence envelope JSON: {0}")]
21 Json(#[from] serde_json::Error),
22
23 #[error("failed to fingerprint policy input: {0}")]
24 Io(#[from] std::io::Error),
25}
26
27#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
31#[serde(deny_unknown_fields)]
32pub struct CiPolicyV1 {
33 pub schema_version: u32,
34 pub envelope: PolicyEnvelopeRulesV1,
35 pub exit_class: PolicyExitClassRulesV1,
36 pub summary: Option<PolicySummaryRulesV1>,
37 pub build_provenance: Option<PolicyBuildProvenanceRulesV1>,
38 #[serde(default)]
39 pub required_input: Vec<PolicyRequiredInputRuleV1>,
40}
41
42#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
46#[serde(deny_unknown_fields)]
47pub struct PolicyEnvelopeRulesV1 {
48 pub required_schema: String,
49 pub allowed_payload_schemas: Option<Vec<String>>,
50 pub allowed_payload_stability: Option<Vec<PayloadSchemaStabilityV1>>,
51}
52
53#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
57#[serde(deny_unknown_fields)]
58pub struct PolicyExitClassRulesV1 {
59 pub allowed: Vec<ExitClassV1>,
60}
61
62#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
66#[serde(deny_unknown_fields)]
67pub struct PolicySummaryRulesV1 {
68 #[serde(default)]
69 pub fail_on_evidence_conflicts: bool,
70 #[serde(default)]
71 pub fail_on_blocked_actions: bool,
72 pub allow_missing_or_stale_evidence: Option<bool>,
73}
74
75#[derive(Clone, Debug, Default, Eq, PartialEq)]
79pub struct PolicyBuildProvenanceRulesV1 {
80 pub(super) rules: Vec<PolicyBuildProvenanceRuleV1>,
81}
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
87pub(super) enum PolicyBuildProvenanceRuleV1 {
88 CleanSource,
89 CargoLock,
90 WasmGzip,
91 Sha256,
92 PackageIdentityMatchesTarget,
93}
94
95impl PolicyBuildProvenanceRulesV1 {
96 pub(super) fn is_enabled(&self, rule: PolicyBuildProvenanceRuleV1) -> bool {
97 self.rules.contains(&rule)
98 }
99}
100
101impl<'de> Deserialize<'de> for PolicyBuildProvenanceRulesV1 {
102 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
103 where
104 D: de::Deserializer<'de>,
105 {
106 const FIELDS: &[&str] = &[
107 "require_clean_source",
108 "require_cargo_lock",
109 "require_wasm_gzip",
110 "require_sha256",
111 "require_package_identity_matches_target",
112 ];
113 let values = BTreeMap::<String, bool>::deserialize(deserializer)?;
114 let mut rules = Vec::new();
115 for (key, enabled) in values {
116 let rule = match key.as_str() {
117 "require_clean_source" => PolicyBuildProvenanceRuleV1::CleanSource,
118 "require_cargo_lock" => PolicyBuildProvenanceRuleV1::CargoLock,
119 "require_wasm_gzip" => PolicyBuildProvenanceRuleV1::WasmGzip,
120 "require_sha256" => PolicyBuildProvenanceRuleV1::Sha256,
121 "require_package_identity_matches_target" => {
122 PolicyBuildProvenanceRuleV1::PackageIdentityMatchesTarget
123 }
124 unknown => return Err(de::Error::unknown_field(unknown, FIELDS)),
125 };
126 if enabled {
127 rules.push(rule);
128 }
129 }
130 Ok(Self { rules })
131 }
132}
133
134#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
138#[serde(deny_unknown_fields)]
139pub struct PolicyRequiredInputRuleV1 {
140 pub kind: String,
141 pub schema: Option<String>,
142}
143
144#[derive(Clone, Debug, Eq, PartialEq)]
148pub struct PolicyGateRequest<'a> {
149 pub policy_source: &'a str,
150 pub policy_path: &'a Path,
151 pub envelope_path: &'a Path,
152 pub fingerprint_root: &'a Path,
153 pub envelope: EvidenceEnvelopeV1,
154}
155
156#[derive(Clone, Debug, Eq, PartialEq)]
160pub struct ProjectEvidenceManifestGateRequest<'a> {
161 pub policy_source: &'a str,
162 pub policy_path: &'a Path,
163 pub manifest_source: &'a str,
164 pub manifest_path: &'a Path,
165 pub fingerprint_root: &'a Path,
166}
167
168#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
172#[serde(deny_unknown_fields)]
173pub struct ProjectEvidenceManifestV1 {
174 pub schema_version: u32,
175 pub project: ProjectEvidenceManifestProjectV1,
176 pub evidence: Vec<ProjectEvidenceManifestEntryV1>,
177}
178
179#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
183#[serde(deny_unknown_fields)]
184pub struct ProjectEvidenceManifestProjectV1 {
185 pub name: String,
186 pub root: String,
187}
188
189#[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
193#[serde(deny_unknown_fields)]
194pub struct ProjectEvidenceManifestEntryV1 {
195 pub kind: String,
196 pub path: String,
197 pub required: bool,
198 pub payload_schema: String,
199 pub target: ProjectEvidenceManifestTargetV1,
200}
201
202#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
206#[serde(deny_unknown_fields)]
207pub struct ProjectEvidenceManifestTargetV1 {
208 pub deployment: Option<String>,
209 pub app: Option<String>,
210 pub fleet: Option<String>,
211 pub role: Option<String>,
212 pub profile: Option<String>,
213 pub environment: Option<String>,
214}
215
216#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
220pub struct PolicyGateReportV1 {
221 pub schema_version: u32,
222 pub policy_schema_version: u32,
223 pub policy_file_fingerprint: InputFingerprintV1,
224 pub evaluated_envelope_fingerprint: InputFingerprintV1,
225 pub evaluated_envelope_exit_class: ExitClassV1,
226 pub evaluated_payload_schema: PayloadSchemaRefV1,
227 pub evaluated_target: EvidenceTargetV1,
228 pub policy_status: PolicyEvaluationStatusV1,
229 pub gate_exit_class: ExitClassV1,
230 pub requirements: Vec<PolicyRequirementV1>,
231 pub findings: Vec<PolicyFindingV1>,
232}
233
234#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
238pub struct ProjectEvidenceGateReportV1 {
239 pub schema_version: u32,
240 pub manifest_schema_version: u32,
241 pub project_name: String,
242 pub policy_file_fingerprint: InputFingerprintV1,
243 pub manifest_file_fingerprint: InputFingerprintV1,
244 pub policy_status: PolicyEvaluationStatusV1,
245 pub gate_exit_class: ExitClassV1,
246 pub evidence: Vec<ProjectEvidenceGateEntryReportV1>,
247}
248
249#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
253pub struct ProjectEvidenceGateEntryReportV1 {
254 pub kind: String,
255 pub path: String,
256 pub required: bool,
257 pub expected_payload_schema: String,
258 pub expected_target: ProjectEvidenceManifestTargetV1,
259 pub status: PolicyEvaluationStatusV1,
260 pub gate_exit_class: ExitClassV1,
261 pub evaluated_envelope_fingerprint: Option<InputFingerprintV1>,
262 pub policy_report: Option<PolicyGateReportV1>,
263 pub findings: Vec<PolicyFindingV1>,
264}
265
266#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
270pub struct PolicyRequirementV1 {
271 pub requirement_id: String,
272 pub status: PolicyEvaluationStatusV1,
273 pub exit_class: ExitClassV1,
274 pub finding_codes: Vec<String>,
275}
276
277#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
281pub struct PolicyFindingV1 {
282 pub code: String,
283 pub severity: PolicyFindingSeverityV1,
284 pub message: String,
285 pub requirement_id: Option<String>,
286 pub subject: Option<String>,
287 pub expected: Option<serde_json::Value>,
288 pub actual: Option<serde_json::Value>,
289 pub evidence_path: Option<String>,
290 pub target: Option<EvidenceTargetV1>,
291 pub related_input: Option<String>,
292}
293
294#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
298#[serde(rename_all = "snake_case")]
299pub enum PolicyFindingSeverityV1 {
300 Info,
301 Warning,
302 Error,
303}
304
305#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
309#[serde(rename_all = "snake_case")]
310pub enum PolicyEvaluationStatusV1 {
311 Passed,
312 Failed,
313}
314
315impl ProjectEvidenceManifestTargetV1 {
316 pub(super) const fn has_selector(&self) -> bool {
317 self.deployment.is_some()
318 || self.app.is_some()
319 || self.fleet.is_some()
320 || self.role.is_some()
321 || self.profile.is_some()
322 || self.environment.is_some()
323 }
324
325 pub(super) fn matches_envelope_target(&self, target: &EvidenceTargetV1) -> bool {
326 self.deployment
327 .as_ref()
328 .is_none_or(|expected| target.deployment.as_ref() == Some(expected))
329 && self
330 .app
331 .as_ref()
332 .is_none_or(|expected| target.app.as_ref() == Some(expected))
333 && self
334 .fleet
335 .as_ref()
336 .is_none_or(|expected| target.fleet.as_ref() == Some(expected))
337 && self
338 .role
339 .as_ref()
340 .is_none_or(|expected| target.role.as_ref() == Some(expected))
341 && self
342 .profile
343 .as_ref()
344 .is_none_or(|expected| target.profile.as_ref() == Some(expected))
345 && self
346 .environment
347 .as_ref()
348 .is_none_or(|expected| target.environment.as_ref() == Some(expected))
349 }
350}
351
352impl PolicyFindingV1 {
353 pub(super) fn error(
354 code: &str,
355 message: impl Into<String>,
356 requirement_id: &str,
357 exit_class: ExitClassV1,
358 ) -> Self {
359 Self {
360 code: code.to_string(),
361 severity: PolicyFindingSeverityV1::Error,
362 message: message.into(),
363 requirement_id: Some(requirement_id.to_string()),
364 subject: Some(exit_class.label().to_string()),
365 expected: None,
366 actual: None,
367 evidence_path: None,
368 target: None,
369 related_input: None,
370 }
371 }
372
373 pub(super) fn warning(code: &str, message: impl Into<String>, requirement_id: &str) -> Self {
374 Self {
375 code: code.to_string(),
376 severity: PolicyFindingSeverityV1::Warning,
377 message: message.into(),
378 requirement_id: Some(requirement_id.to_string()),
379 subject: Some(ExitClassV1::SuccessWithWarnings.label().to_string()),
380 expected: None,
381 actual: None,
382 evidence_path: None,
383 target: None,
384 related_input: None,
385 }
386 }
387
388 pub(super) fn expected(mut self, expected: serde_json::Value) -> Self {
389 self.expected = Some(expected);
390 self
391 }
392
393 pub(super) fn actual(mut self, actual: serde_json::Value) -> Self {
394 self.actual = Some(actual);
395 self
396 }
397
398 pub(super) fn exit_class(&self) -> ExitClassV1 {
399 match self.subject_exit_class() {
400 Some(ExitClassV1::EvidenceConflict) => ExitClassV1::EvidenceConflict,
401 Some(ExitClassV1::MissingRequiredEvidence) => ExitClassV1::MissingRequiredEvidence,
402 _ => ExitClassV1::BlockedByPolicy,
403 }
404 }
405
406 #[must_use]
407 pub fn subject_exit_class(&self) -> Option<ExitClassV1> {
408 self.subject.as_deref().and_then(ExitClassV1::from_label)
409 }
410}