1use std::collections::BTreeMap;
2
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use utoipa::ToSchema;
6
7use crate::extraction_input_digest;
8
9use super::{
10 ConfigContractDefinition, ConfigRevision, DeliveryDecision, DeliveryEffects, DeliveryIssue,
11 DeliveryIssueCode, ProductionEligibilityEvidence, ProductionEligibilityInput,
12 ReleaseTrustEvidence, ReleaseTrustProvider, SecretProvider, ServiceRelease,
13 config_revision_matches_contract, production_eligibility_evidence_integrity_is_valid,
14 release_trust_evidence_integrity_is_valid, service_release_integrity_is_valid,
15};
16
17pub const POLICY_PACK_PROTOCOL: &str = "lenso.policy-pack.v1";
18pub const POLICY_EVIDENCE_PROTOCOL: &str = "lenso.policy-evidence.v1";
19
20#[derive(
21 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
22)]
23#[serde(rename_all = "snake_case")]
24pub enum PolicyEnvironmentProfile {
25 Development,
26 Production,
27}
28
29#[derive(
30 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
31)]
32#[serde(rename_all = "snake_case")]
33pub enum PolicyRuleSeverity {
34 Required,
35 Advisory,
36}
37
38#[derive(
39 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
40)]
41#[serde(rename_all = "camelCase")]
42pub struct PolicyRule {
43 pub rule_id: String,
44 pub evidence_key: String,
45 pub severity: PolicyRuleSeverity,
46 pub advisory_in_development: bool,
47 pub remediation: String,
48 pub next_action: String,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
52#[serde(rename_all = "camelCase")]
53pub struct PolicyPack {
54 pub protocol: String,
55 pub pack_id: String,
56 pub pack_digest: String,
57 pub version: String,
58 pub environment_profile: PolicyEnvironmentProfile,
59 pub rules: Vec<PolicyRule>,
60}
61
62#[derive(
63 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, ToSchema,
64)]
65#[serde(rename_all = "snake_case")]
66pub enum PolicyEvaluationSurface {
67 Local,
68 Ci,
69 Cli,
70 SystemPlane,
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
74#[serde(rename_all = "camelCase")]
75pub struct PolicyRuleResult {
76 pub rule_id: String,
77 pub severity: PolicyRuleSeverity,
78 pub decision: DeliveryDecision,
79 pub evidence_references: Vec<String>,
80 pub remediation: String,
81 pub next_actions: Vec<String>,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)]
85#[serde(rename_all = "camelCase")]
86pub struct PolicyEvidence {
87 pub protocol: String,
88 pub evidence_id: String,
89 pub evidence_digest: String,
90 pub pack_id: String,
91 pub pack_digest: String,
92 pub evaluated_subject: String,
93 pub input_digests: BTreeMap<String, String>,
94 pub decision: DeliveryDecision,
95 pub rule_results: Vec<PolicyRuleResult>,
96 pub issues: Vec<DeliveryIssue>,
97 pub effects: DeliveryEffects,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)]
101#[serde(rename_all = "camelCase")]
102pub struct DeliveryPolicyInputs {
103 pub release: ServiceRelease,
104 pub trust: ReleaseTrustEvidence,
105 pub config_contract: ConfigContractDefinition,
106 pub config: ConfigRevision,
107 pub eligibility: ProductionEligibilityEvidence,
108 pub eligibility_input: ProductionEligibilityInput,
109}
110
111#[must_use]
112pub fn production_policy_pack() -> PolicyPack {
113 build_policy_pack(PolicyEnvironmentProfile::Production)
114}
115
116#[must_use]
117pub fn development_policy_pack() -> PolicyPack {
118 build_policy_pack(PolicyEnvironmentProfile::Development)
119}
120
121#[must_use]
122pub fn evaluate_delivery_policy(
123 pack: &PolicyPack,
124 inputs: &DeliveryPolicyInputs,
125 trust_provider: &dyn ReleaseTrustProvider,
126 secret_provider: &dyn SecretProvider,
127 _surface: PolicyEvaluationSurface,
128) -> PolicyEvidence {
129 let pack_valid = policy_pack_integrity_is_valid(pack);
130 let release_valid = service_release_integrity_is_valid(&inputs.release);
131 let trust_valid =
132 release_trust_evidence_integrity_is_valid(&inputs.trust, &inputs.release, trust_provider);
133 let config_valid =
134 config_revision_matches_contract(&inputs.config, &inputs.config_contract, secret_provider)
135 && inputs.config.service_id == inputs.release.service_id
136 && inputs.config_contract.reference == inputs.release.config_contract.reference
137 && inputs.config_contract.digest == inputs.release.config_contract.digest
138 && inputs.config.contract_digest == inputs.release.config_contract.digest;
139 let eligibility_valid = production_eligibility_evidence_integrity_is_valid(
140 &inputs.eligibility,
141 &inputs.eligibility_input,
142 &inputs.release,
143 trust_provider,
144 );
145 let facts = BTreeMap::from([
146 ("release.integrity".to_owned(), Some(release_valid)),
147 (
148 "supply_chain.trusted".to_owned(),
149 Some(trust_valid && inputs.trust.decision == DeliveryDecision::Passed),
150 ),
151 ("config.valid".to_owned(), Some(config_valid)),
152 ])
153 .into_iter()
154 .chain(inputs.eligibility.facts.iter().map(|(key, value)| {
155 (
156 key.clone(),
157 if eligibility_valid {
158 *value
159 } else {
160 Some(false)
161 },
162 )
163 }))
164 .collect::<BTreeMap<_, _>>();
165
166 let mut issues = Vec::new();
167 if !pack_valid {
168 issues.push(DeliveryIssue {
169 code: DeliveryIssueCode::PolicyRuleBlocked,
170 message: "The Policy Pack identity, digest, environment profile, or required rule set is invalid.".to_owned(),
171 evidence_references: vec![pack.pack_id.clone()],
172 remediation: "Use the canonical versioned Policy Pack for the selected environment profile.".to_owned(),
173 next_actions: vec!["Reload the canonical Policy Pack and evaluate the unchanged evidence again.".to_owned()],
174 });
175 }
176 let rule_results = pack
177 .rules
178 .iter()
179 .map(|rule| {
180 let passed = facts
181 .get(&rule.evidence_key)
182 .copied()
183 .flatten()
184 .unwrap_or(false);
185 let decision = if passed {
186 DeliveryDecision::Passed
187 } else if pack.environment_profile == PolicyEnvironmentProfile::Development
188 && rule.advisory_in_development
189 {
190 DeliveryDecision::Advisory
191 } else {
192 DeliveryDecision::Blocked
193 };
194 if decision == DeliveryDecision::Blocked {
195 issues.push(DeliveryIssue {
196 code: DeliveryIssueCode::PolicyRuleBlocked,
197 message: format!(
198 "Policy rule `{}` is blocked because `{}` is false or unknown.",
199 rule.rule_id, rule.evidence_key
200 ),
201 evidence_references: vec![rule.evidence_key.clone()],
202 remediation: rule.remediation.clone(),
203 next_actions: vec![rule.next_action.clone()],
204 });
205 }
206 PolicyRuleResult {
207 rule_id: rule.rule_id.clone(),
208 severity: rule.severity,
209 decision,
210 evidence_references: vec![rule.evidence_key.clone()],
211 remediation: rule.remediation.clone(),
212 next_actions: vec![rule.next_action.clone()],
213 }
214 })
215 .collect::<Vec<_>>();
216 let decision = if !pack_valid
217 || rule_results
218 .iter()
219 .any(|result| result.decision == DeliveryDecision::Blocked)
220 {
221 DeliveryDecision::Blocked
222 } else if rule_results
223 .iter()
224 .any(|result| result.decision == DeliveryDecision::Advisory)
225 {
226 DeliveryDecision::Advisory
227 } else {
228 DeliveryDecision::Passed
229 };
230 let input_digests = BTreeMap::from([
231 ("release".to_owned(), inputs.release.release_digest.clone()),
232 ("trust".to_owned(), inputs.trust.evidence_digest.clone()),
233 ("config".to_owned(), inputs.config.revision_digest.clone()),
234 (
235 "eligibility".to_owned(),
236 inputs.eligibility.evidence_digest.clone(),
237 ),
238 ]);
239 #[derive(Serialize)]
240 struct EvidenceContent<'a> {
241 protocol: &'a str,
242 pack_id: &'a str,
243 pack_digest: &'a str,
244 evaluated_subject: &'a str,
245 input_digests: &'a BTreeMap<String, String>,
246 decision: DeliveryDecision,
247 rule_results: &'a [PolicyRuleResult],
248 issues: &'a [DeliveryIssue],
249 }
250 let content = EvidenceContent {
251 protocol: POLICY_EVIDENCE_PROTOCOL,
252 pack_id: &pack.pack_id,
253 pack_digest: &pack.pack_digest,
254 evaluated_subject: &inputs.release.release_id,
255 input_digests: &input_digests,
256 decision,
257 rule_results: &rule_results,
258 issues: &issues,
259 };
260 let evidence_digest = extraction_input_digest(
261 serde_json::to_vec(&content).expect("policy evidence must serialize"),
262 );
263 PolicyEvidence {
264 protocol: POLICY_EVIDENCE_PROTOCOL.to_owned(),
265 evidence_id: format!("policy-evidence:{evidence_digest}"),
266 evidence_digest,
267 pack_id: pack.pack_id.clone(),
268 pack_digest: pack.pack_digest.clone(),
269 evaluated_subject: inputs.release.release_id.clone(),
270 input_digests,
271 decision,
272 rule_results,
273 issues,
274 effects: DeliveryEffects::default(),
275 }
276}
277
278#[must_use]
279pub fn policy_pack_integrity_is_valid(pack: &PolicyPack) -> bool {
280 *pack == build_policy_pack(pack.environment_profile)
281}
282
283#[must_use]
284pub fn policy_evidence_integrity_is_valid(evidence: &PolicyEvidence) -> bool {
285 #[derive(Serialize)]
286 struct EvidenceContent<'a> {
287 protocol: &'a str,
288 pack_id: &'a str,
289 pack_digest: &'a str,
290 evaluated_subject: &'a str,
291 input_digests: &'a BTreeMap<String, String>,
292 decision: DeliveryDecision,
293 rule_results: &'a [PolicyRuleResult],
294 issues: &'a [DeliveryIssue],
295 }
296 let content = EvidenceContent {
297 protocol: evidence.protocol.as_str(),
298 pack_id: evidence.pack_id.as_str(),
299 pack_digest: evidence.pack_digest.as_str(),
300 evaluated_subject: evidence.evaluated_subject.as_str(),
301 input_digests: &evidence.input_digests,
302 decision: evidence.decision,
303 rule_results: &evidence.rule_results,
304 issues: &evidence.issues,
305 };
306 evidence.protocol == POLICY_EVIDENCE_PROTOCOL
307 && evidence.evidence_id == format!("policy-evidence:{}", evidence.evidence_digest)
308 && extraction_input_digest(
309 serde_json::to_vec(&content).expect("policy evidence must serialize"),
310 ) == evidence.evidence_digest
311}
312
313#[must_use]
314pub fn production_policy_evidence_is_valid(
315 evidence: &PolicyEvidence,
316 inputs: &DeliveryPolicyInputs,
317 trust_provider: &dyn ReleaseTrustProvider,
318 secret_provider: &dyn SecretProvider,
319) -> bool {
320 evidence
321 == &evaluate_delivery_policy(
322 &production_policy_pack(),
323 inputs,
324 trust_provider,
325 secret_provider,
326 PolicyEvaluationSurface::SystemPlane,
327 )
328}
329
330fn build_policy_pack(environment_profile: PolicyEnvironmentProfile) -> PolicyPack {
331 let required_keys = [
332 "release.integrity",
333 "supply_chain.trusted",
334 "config.valid",
335 "contracts.compatible",
336 "migrations.safe",
337 "workflows.compatible",
338 "rollback.safe",
339 "providers.compatible",
340 "identity.production",
341 "tenancy.mode.production",
342 "tenancy.enforced",
343 "call_policies.declared",
344 "dependencies.ready",
345 "resilience.declared",
346 "reliability.complete",
347 "edge.valid",
348 "environment.verification_fresh",
349 "production.eligible",
350 ];
351 let rules = required_keys
352 .into_iter()
353 .map(|key| PolicyRule {
354 rule_id: format!("lenso.production.{}", key.replace('_', "-")),
355 evidence_key: key.to_owned(),
356 severity: PolicyRuleSeverity::Required,
357 advisory_in_development: matches!(
358 key,
359 "rollback.safe"
360 | "providers.compatible"
361 | "identity.production"
362 | "tenancy.mode.production"
363 | "environment.verification_fresh"
364 | "production.eligible"
365 ),
366 remediation: format!("Provide passing canonical evidence for `{key}`."),
367 next_action: "Refresh canonical evidence and evaluate the same Policy Pack again."
368 .to_owned(),
369 })
370 .collect::<Vec<_>>();
371 let version = "v1".to_owned();
372 let pack_digest = extraction_input_digest(
373 serde_json::to_vec(&(
374 POLICY_PACK_PROTOCOL,
375 version.as_str(),
376 environment_profile,
377 rules.as_slice(),
378 ))
379 .expect("Policy Pack must serialize"),
380 );
381 PolicyPack {
382 protocol: POLICY_PACK_PROTOCOL.to_owned(),
383 pack_id: format!("policy-pack:{pack_digest}"),
384 pack_digest,
385 version,
386 environment_profile,
387 rules,
388 }
389}