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 fleet: Option<String>,
210 pub role: Option<String>,
211 pub profile: Option<String>,
212 pub network: Option<String>,
213}
214
215#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
219pub struct PolicyGateReportV1 {
220 pub schema_version: u32,
221 pub policy_schema_version: u32,
222 pub policy_file_fingerprint: InputFingerprintV1,
223 pub evaluated_envelope_fingerprint: InputFingerprintV1,
224 pub evaluated_envelope_exit_class: ExitClassV1,
225 pub evaluated_payload_schema: PayloadSchemaRefV1,
226 pub evaluated_target: EvidenceTargetV1,
227 pub policy_status: PolicyEvaluationStatusV1,
228 pub gate_exit_class: ExitClassV1,
229 pub requirements: Vec<PolicyRequirementV1>,
230 pub findings: Vec<PolicyFindingV1>,
231}
232
233#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
237pub struct ProjectEvidenceGateReportV1 {
238 pub schema_version: u32,
239 pub manifest_schema_version: u32,
240 pub project_name: String,
241 pub policy_file_fingerprint: InputFingerprintV1,
242 pub manifest_file_fingerprint: InputFingerprintV1,
243 pub policy_status: PolicyEvaluationStatusV1,
244 pub gate_exit_class: ExitClassV1,
245 pub evidence: Vec<ProjectEvidenceGateEntryReportV1>,
246}
247
248#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
252pub struct ProjectEvidenceGateEntryReportV1 {
253 pub kind: String,
254 pub path: String,
255 pub required: bool,
256 pub expected_payload_schema: String,
257 pub expected_target: ProjectEvidenceManifestTargetV1,
258 pub status: PolicyEvaluationStatusV1,
259 pub gate_exit_class: ExitClassV1,
260 pub evaluated_envelope_fingerprint: Option<InputFingerprintV1>,
261 pub policy_report: Option<PolicyGateReportV1>,
262 pub findings: Vec<PolicyFindingV1>,
263}
264
265#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
269pub struct PolicyRequirementV1 {
270 pub requirement_id: String,
271 pub status: PolicyEvaluationStatusV1,
272 pub exit_class: ExitClassV1,
273 pub finding_codes: Vec<String>,
274}
275
276#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
280pub struct PolicyFindingV1 {
281 pub code: String,
282 pub severity: PolicyFindingSeverityV1,
283 pub message: String,
284 pub requirement_id: Option<String>,
285 pub subject: Option<String>,
286 pub expected: Option<serde_json::Value>,
287 pub actual: Option<serde_json::Value>,
288 pub evidence_path: Option<String>,
289 pub target: Option<EvidenceTargetV1>,
290 pub related_input: Option<String>,
291}
292
293#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
297#[serde(rename_all = "snake_case")]
298pub enum PolicyFindingSeverityV1 {
299 Info,
300 Warning,
301 Error,
302}
303
304#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
308#[serde(rename_all = "snake_case")]
309pub enum PolicyEvaluationStatusV1 {
310 Passed,
311 Failed,
312}
313
314impl ProjectEvidenceManifestTargetV1 {
315 pub(super) const fn has_selector(&self) -> bool {
316 self.deployment.is_some()
317 || self.fleet.is_some()
318 || self.role.is_some()
319 || self.profile.is_some()
320 || self.network.is_some()
321 }
322
323 pub(super) fn matches_envelope_target(&self, target: &EvidenceTargetV1) -> bool {
324 self.deployment
325 .as_ref()
326 .is_none_or(|expected| target.deployment.as_ref() == Some(expected))
327 && self
328 .fleet
329 .as_ref()
330 .is_none_or(|expected| target.fleet.as_ref() == Some(expected))
331 && self
332 .role
333 .as_ref()
334 .is_none_or(|expected| target.role.as_ref() == Some(expected))
335 && self
336 .profile
337 .as_ref()
338 .is_none_or(|expected| target.profile.as_ref() == Some(expected))
339 && self
340 .network
341 .as_ref()
342 .is_none_or(|expected| target.network.as_ref() == Some(expected))
343 }
344}
345
346impl PolicyFindingV1 {
347 pub(super) fn error(
348 code: &str,
349 message: impl Into<String>,
350 requirement_id: &str,
351 exit_class: ExitClassV1,
352 ) -> Self {
353 Self {
354 code: code.to_string(),
355 severity: PolicyFindingSeverityV1::Error,
356 message: message.into(),
357 requirement_id: Some(requirement_id.to_string()),
358 subject: Some(exit_class_subject(exit_class).to_string()),
359 expected: None,
360 actual: None,
361 evidence_path: None,
362 target: None,
363 related_input: None,
364 }
365 }
366
367 pub(super) fn warning(code: &str, message: impl Into<String>, requirement_id: &str) -> Self {
368 Self {
369 code: code.to_string(),
370 severity: PolicyFindingSeverityV1::Warning,
371 message: message.into(),
372 requirement_id: Some(requirement_id.to_string()),
373 subject: Some("success_with_warnings".to_string()),
374 expected: None,
375 actual: None,
376 evidence_path: None,
377 target: None,
378 related_input: None,
379 }
380 }
381
382 pub(super) fn expected(mut self, expected: serde_json::Value) -> Self {
383 self.expected = Some(expected);
384 self
385 }
386
387 pub(super) fn actual(mut self, actual: serde_json::Value) -> Self {
388 self.actual = Some(actual);
389 self
390 }
391
392 pub(super) fn exit_class(&self) -> ExitClassV1 {
393 match self.subject.as_deref() {
394 Some("evidence_conflict") => ExitClassV1::EvidenceConflict,
395 Some("missing_required_evidence") => ExitClassV1::MissingRequiredEvidence,
396 _ => ExitClassV1::BlockedByPolicy,
397 }
398 }
399}
400
401const fn exit_class_subject(exit_class: ExitClassV1) -> &'static str {
402 match exit_class {
403 ExitClassV1::EvidenceConflict => "evidence_conflict",
404 ExitClassV1::MissingRequiredEvidence => "missing_required_evidence",
405 _ => "blocked_by_policy",
406 }
407}