1use super::*;
2use std::collections::{BTreeMap, BTreeSet};
3
4mod artifacts;
5mod canisters;
6mod config_digests;
7mod controllers;
8mod module_hashes;
9mod pools;
10mod receipt_resume;
11mod root_subnet;
12mod safety;
13mod verifier_readiness;
14
15use artifacts::compare_artifacts;
16#[cfg(test)]
17pub(super) use artifacts::{
18 ARTIFACT_DUPLICATE_DIFF_CATEGORY, ARTIFACT_FILE_DIGEST_MISMATCH_CODE,
19 ARTIFACT_FILE_SHA256_DIFF_CATEGORY, ARTIFACT_MISSING_CODE, ARTIFACT_ROLE_CONFLICT_CODE,
20 ARTIFACT_ROLE_CONFLICT_DIFF_CATEGORY, DUPLICATE_ARTIFACT_OBSERVED_CODE,
21 DUPLICATE_PLANNED_ARTIFACT_ROLE_CODE, PLANNED_ARTIFACT_DUPLICATE_DIFF_CATEGORY,
22 PLANNED_ARTIFACT_ROLE_CONFLICT_CODE, PLANNED_ARTIFACT_ROLE_CONFLICT_DIFF_CATEGORY,
23};
24#[cfg(test)]
25pub(super) use canisters::{
26 CANISTER_DUPLICATE_DIFF_CATEGORY, CANISTER_EXTRA_DIFF_CATEGORY, CANISTER_ID_ROLE_CONFLICT_CODE,
27 CANISTER_ID_ROLE_CONFLICT_DIFF_CATEGORY, CANISTER_ROLE_AMBIGUOUS_CODE,
28 CANISTER_ROLE_AMBIGUOUS_DIFF_CATEGORY, CANISTER_ROLE_MISMATCH_CODE, CANISTER_UNOBSERVED_CODE,
29 DUPLICATE_CANISTER_OBSERVED_CODE, DUPLICATE_PLANNED_CANISTER_ROLE_CODE,
30 EXTRA_CANISTER_OBSERVED_CODE, PLANNED_CANISTER_DUPLICATE_DIFF_CATEGORY,
31 PLANNED_CANISTER_ID_CONFLICT_CODE, PLANNED_CANISTER_ID_CONFLICT_DIFF_CATEGORY,
32 PLANNED_CANISTER_ROLE_CONFLICT_CODE, PLANNED_CANISTER_ROLE_CONFLICT_DIFF_CATEGORY,
33 ROLE_MISMATCH_DIFF_CATEGORY, UNSAFE_CONTROL_CLASS_CODE,
34};
35use canisters::{compare_canisters, compare_observed_canister_id_conflicts};
36#[cfg(test)]
37pub(super) use config_digests::{RAW_CONFIG_DIGEST_MISMATCH_CODE, RAW_CONFIG_SHA256_DIFF_CATEGORY};
38use config_digests::{compare_embedded_config, compare_raw_config};
39use controllers::compare_authority_profile;
40#[cfg(test)]
41pub(super) use controllers::{
42 CONTROLLER_AUTHORITY_OVERLAP_CODE, CONTROLLER_EXTRA_DIFF_CATEGORY,
43 CONTROLLER_MISSING_DIFF_CATEGORY, CONTROLLERS_UNOBSERVED_CODE,
44 EXPECTED_CONTROLLER_MISSING_CODE, EXTRA_CONTROLLER_OBSERVED_CODE,
45};
46use module_hashes::compare_module_hashes;
47#[cfg(test)]
48pub(super) use module_hashes::{
49 INSTALLED_MODULE_HASH_AMBIGUOUS_CODE, INSTALLED_MODULE_HASH_AMBIGUOUS_DIFF_CATEGORY,
50 INSTALLED_MODULE_HASH_DIFF_CATEGORY, INSTALLED_MODULE_HASH_MISMATCH_CODE,
51};
52#[cfg(test)]
53pub(super) use pools::{
54 CANISTER_POOL_ROLE_CONFLICT_CODE, CANISTER_POOL_ROLE_CONFLICT_DIFF_CATEGORY,
55 DUPLICATE_PLANNED_POOL_CODE, DUPLICATE_POOL_CANISTER_OBSERVED_CODE,
56 EXTRA_POOL_CANISTER_OBSERVED_CODE, PLANNED_POOL_CONFLICT_CODE,
57 PLANNED_POOL_CONFLICT_DIFF_CATEGORY, PLANNED_POOL_DUPLICATE_DIFF_CATEGORY,
58 PLANNED_POOL_ID_CONFLICT_CODE, PLANNED_POOL_ID_CONFLICT_DIFF_CATEGORY,
59 POOL_CANISTER_DIFF_CATEGORY, POOL_CANISTER_DUPLICATE_DIFF_CATEGORY,
60 POOL_CANISTER_ID_CONFLICT_CODE, POOL_CANISTER_ID_CONFLICT_DIFF_CATEGORY,
61 POOL_CANISTER_ID_DIFF_CATEGORY, POOL_CANISTER_ID_MISMATCH_CODE, POOL_CANISTER_MISSING_CODE,
62 POOL_CONTROL_CLASS_DIFF_CATEGORY, POOL_EXTRA_DIFF_CATEGORY, UNSAFE_POOL_CONTROL_CLASS_CODE,
63};
64use pools::{compare_observed_canister_pool_role_conflicts, compare_pools};
65pub use receipt_resume::compare_plan_inventory_and_receipt;
66#[cfg(test)]
67pub(super) use receipt_resume::{
68 DUPLICATE_RECEIPT_PHASE_CODE, DUPLICATE_RECEIPT_ROLE_PHASE_CODE,
69 RECEIPT_EXECUTION_STATUS_MISMATCH_CODE, RECEIPT_PHASE_CONFLICT_CODE,
70 RECEIPT_PLAN_MISMATCH_CODE, RECEIPT_POSTCONDITION_UNVERIFIED_CODE,
71 RECEIPT_ROLE_PHASE_CONFLICT_CODE,
72};
73#[cfg(test)]
74pub(super) use root_subnet::ROOT_AUTH_CLOUD_ENGINE_SUBNET_CODE;
75pub(super) use root_subnet::apply_root_auth_signer_subnet_check;
76#[cfg(test)]
77pub(super) use root_subnet::{
78 RootSubnetEvidence, RootSubnetEvidenceSource, apply_root_auth_signer_subnet_check_with_source,
79};
80pub use safety::safety_report_from_diff;
81pub(in crate::deployment_truth::report) use safety::{resume_safety_reasons, safety_status};
82use verifier_readiness::compare_verifier_readiness;
83#[cfg(test)]
84pub(super) use verifier_readiness::{
85 DUPLICATE_PLANNED_VERIFIER_ROLE_EPOCH_CODE, DUPLICATE_VERIFIER_ROLE_EPOCH_OBSERVED_CODE,
86 PLANNED_VERIFIER_ROLE_EPOCH_CONFLICT_CODE, PLANNED_VERIFIER_ROLE_EPOCH_CONFLICT_DIFF_CATEGORY,
87 PLANNED_VERIFIER_ROLE_EPOCH_DUPLICATE_DIFF_CATEGORY, VERIFIER_NOT_OBSERVED_LABEL,
88 VERIFIER_ROLE_EPOCH_CONFLICT_CODE, VERIFIER_ROLE_EPOCH_CONFLICT_DIFF_CATEGORY,
89 VERIFIER_ROLE_EPOCH_DIFF_CATEGORY, VERIFIER_ROLE_EPOCH_DUPLICATE_DIFF_CATEGORY,
90 VERIFIER_ROLE_EPOCH_STALE_CODE, VERIFIER_ROLE_EPOCH_UNOBSERVED_CODE,
91};
92
93pub(in crate::deployment_truth) const DEPLOYMENT_MANIFEST_MISMATCH_CODE: &str =
94 "deployment_manifest_mismatch";
95pub(in crate::deployment_truth) const OBSERVATION_GAP_CODE: &str = "observation_gap";
96pub(in crate::deployment_truth) const UNVERIFIED_DEPLOYMENT_ROOT_CODE: &str =
97 "unverified_deployment_root";
98pub(in crate::deployment_truth) const PLAN_ASSUMPTION_CODE: &str = "plan_assumption";
99pub(in crate::deployment_truth) const IDENTITY_UNOBSERVED_CODE: &str = "identity_unobserved";
100pub(in crate::deployment_truth) const NETWORK_MISMATCH_CODE: &str = "network_mismatch";
101pub(in crate::deployment_truth) const ROOT_TRUST_ANCHOR_MISMATCH_CODE: &str =
102 "root_trust_anchor_mismatch";
103pub(in crate::deployment_truth) const DEPLOYMENT_MANIFEST_UNOBSERVED_CODE: &str =
104 "deployment_manifest_unobserved";
105
106struct DuplicateEvidenceGroup {
110 subject: String,
111 count: usize,
112 evidence_label: String,
113 is_conflict: bool,
114}
115
116#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct LocalDeploymentCheckRequest {
121 pub deployment_name: String,
122 pub network: String,
123 pub workspace_root: std::path::PathBuf,
124 pub icp_root: std::path::PathBuf,
125 pub config_path: Option<std::path::PathBuf>,
126 pub observed_at: String,
127 pub runtime_variant: String,
128 pub build_profile: String,
129}
130
131pub fn check_local_deployment(
133 request: &LocalDeploymentCheckRequest,
134) -> Result<DeploymentCheckV1, DeploymentTruthError> {
135 let plan = build_local_deployment_plan(&LocalDeploymentPlanRequest {
136 deployment_name: request.deployment_name.clone(),
137 network: request.network.clone(),
138 workspace_root: request.workspace_root.clone(),
139 icp_root: request.icp_root.clone(),
140 config_path: request.config_path.clone(),
141 runtime_variant: request.runtime_variant.clone(),
142 build_profile: request.build_profile.clone(),
143 });
144 let inventory = collect_local_deployment_inventory(&LocalInventoryRequest {
145 deployment_name: request.deployment_name.clone(),
146 network: request.network.clone(),
147 workspace_root: request.workspace_root.clone(),
148 icp_root: request.icp_root.clone(),
149 config_path: request.config_path.clone(),
150 observed_at: request.observed_at.clone(),
151 })?;
152 let mut diff = compare_plan_to_inventory(&plan, &inventory);
153 apply_root_auth_signer_subnet_check(&mut diff, &inventory, &request.network, &request.icp_root);
154 let report = safety_report_from_diff(
155 format!(
156 "local:{}:{}:report",
157 request.network, request.deployment_name
158 ),
159 Some(format!(
160 "local:{}:{}:diff",
161 request.network, request.deployment_name
162 )),
163 &diff,
164 );
165
166 Ok(DeploymentCheckV1 {
167 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
168 check_id: format!(
169 "local:{}:{}:check",
170 request.network, request.deployment_name
171 ),
172 plan,
173 inventory,
174 diff,
175 report,
176 })
177}
178
179fn refresh_resume_safety(diff: &mut DeploymentDiffV1) {
180 diff.resume_safety.status = safety_status(&diff.hard_failures, &diff.warnings);
181 diff.resume_safety.reasons = resume_safety_reasons(&diff.hard_failures, &diff.warnings);
182}
183
184#[must_use]
186pub fn compare_plan_to_inventory(
187 plan: &DeploymentPlanV1,
188 inventory: &DeploymentInventoryV1,
189) -> DeploymentDiffV1 {
190 let mut artifact_diff = Vec::new();
191 let mut controller_diff = Vec::new();
192 let mut pool_diff = Vec::new();
193 let mut embedded_config_diff = Vec::new();
194 let mut module_hash_diff = Vec::new();
195 let mut verifier_readiness_diff = Vec::new();
196 let mut hard_failures = Vec::new();
197 let mut warnings = Vec::new();
198
199 compare_identity(plan, inventory, &mut hard_failures);
200 compare_authority_profile(plan, &mut controller_diff, &mut hard_failures);
201 compare_artifacts(
202 plan,
203 inventory,
204 &mut artifact_diff,
205 &mut hard_failures,
206 &mut warnings,
207 );
208 compare_observed_canister_id_conflicts(
209 inventory,
210 &mut controller_diff,
211 &mut hard_failures,
212 &mut warnings,
213 );
214 compare_observed_canister_pool_role_conflicts(inventory, &mut pool_diff, &mut hard_failures);
215 compare_canisters(
216 plan,
217 inventory,
218 &mut controller_diff,
219 &mut hard_failures,
220 &mut warnings,
221 );
222 compare_pools(
223 plan,
224 inventory,
225 &mut pool_diff,
226 &mut hard_failures,
227 &mut warnings,
228 );
229 compare_module_hashes(
230 plan,
231 inventory,
232 &mut module_hash_diff,
233 &mut hard_failures,
234 &mut warnings,
235 );
236 compare_raw_config(
237 plan,
238 inventory,
239 &mut embedded_config_diff,
240 &mut hard_failures,
241 );
242 compare_embedded_config(
243 plan,
244 inventory,
245 &mut embedded_config_diff,
246 &mut hard_failures,
247 &mut warnings,
248 );
249 compare_verifier_readiness(
250 plan,
251 inventory,
252 &mut verifier_readiness_diff,
253 &mut hard_failures,
254 &mut warnings,
255 );
256 record_plan_assumptions(plan, &mut hard_failures, &mut warnings);
257 for gap in &inventory.unresolved_observations {
258 warnings.push(SafetyFindingV1 {
259 code: OBSERVATION_GAP_CODE.to_string(),
260 message: gap.description.clone(),
261 severity: SafetySeverityV1::Warning,
262 subject: Some(gap.key.clone()),
263 });
264 }
265
266 let status = safety_status(&hard_failures, &warnings);
267 DeploymentDiffV1 {
268 schema_version: DEPLOYMENT_TRUTH_SCHEMA_VERSION,
269 plan_identity: plan.deployment_identity.clone(),
270 observed_identity: inventory.observed_identity.clone(),
271 artifact_diff,
272 controller_diff,
273 pool_diff,
274 embedded_config_diff,
275 module_hash_diff,
276 verifier_readiness_diff,
277 resume_safety: ResumeSafetyV1 {
278 status,
279 reasons: resume_safety_reasons(&hard_failures, &warnings),
280 },
281 hard_failures,
282 warnings,
283 resumable_phases: Vec::new(),
284 }
285}
286
287fn record_plan_assumptions(
288 plan: &DeploymentPlanV1,
289 hard_failures: &mut Vec<SafetyFindingV1>,
290 warnings: &mut Vec<SafetyFindingV1>,
291) {
292 for assumption in &plan.unresolved_assumptions {
293 if assumption.key == "local_state.unverified_root_canister_id" {
294 hard_failures.push(SafetyFindingV1 {
295 code: UNVERIFIED_DEPLOYMENT_ROOT_CODE.to_string(),
296 message: assumption.description.clone(),
297 severity: SafetySeverityV1::HardFailure,
298 subject: Some(assumption.key.clone()),
299 });
300 } else {
301 warnings.push(SafetyFindingV1 {
302 code: PLAN_ASSUMPTION_CODE.to_string(),
303 message: assumption.description.clone(),
304 severity: SafetySeverityV1::Warning,
305 subject: Some(assumption.key.clone()),
306 });
307 }
308 }
309}
310
311fn compare_identity(
312 plan: &DeploymentPlanV1,
313 inventory: &DeploymentInventoryV1,
314 hard_failures: &mut Vec<SafetyFindingV1>,
315) {
316 let Some(observed) = &inventory.observed_identity else {
317 hard_failures.push(finding(
318 IDENTITY_UNOBSERVED_CODE,
319 "deployment identity was not observed",
320 SafetySeverityV1::HardFailure,
321 None,
322 ));
323 return;
324 };
325
326 if observed.network != plan.deployment_identity.network {
327 hard_failures.push(finding(
328 NETWORK_MISMATCH_CODE,
329 format!(
330 "plan network {} differs from observed network {}",
331 plan.deployment_identity.network, observed.network
332 ),
333 SafetySeverityV1::HardFailure,
334 Some("deployment_identity.network".to_string()),
335 ));
336 }
337 if let (Some(expected), Some(actual)) = (
338 plan.deployment_identity.root_principal.as_ref(),
339 observed.root_principal.as_ref(),
340 ) && expected != actual
341 {
342 hard_failures.push(finding(
343 ROOT_TRUST_ANCHOR_MISMATCH_CODE,
344 format!("plan root {expected} differs from observed root {actual}"),
345 SafetySeverityV1::HardFailure,
346 Some("deployment_identity.root_principal".to_string()),
347 ));
348 }
349 match (
350 plan.deployment_identity.deployment_manifest_digest.as_ref(),
351 observed.deployment_manifest_digest.as_ref(),
352 ) {
353 (Some(expected), Some(actual)) if expected != actual => {
354 hard_failures.push(finding(
355 DEPLOYMENT_MANIFEST_MISMATCH_CODE,
356 "deployment manifest digest differs from the observed local config",
357 SafetySeverityV1::HardFailure,
358 Some("deployment_identity.deployment_manifest_digest".to_string()),
359 ));
360 }
361 (Some(_), None) => {
362 hard_failures.push(finding(
363 DEPLOYMENT_MANIFEST_UNOBSERVED_CODE,
364 "deployment manifest digest was not observed",
365 SafetySeverityV1::HardFailure,
366 Some("deployment_identity.deployment_manifest_digest".to_string()),
367 ));
368 }
369 _ => {}
370 }
371}
372
373fn finding(
374 code: impl Into<String>,
375 message: impl Into<String>,
376 severity: SafetySeverityV1,
377 subject: Option<String>,
378) -> SafetyFindingV1 {
379 SafetyFindingV1 {
380 code: code.into(),
381 message: message.into(),
382 severity,
383 subject,
384 }
385}
386
387fn diff_item(
388 category: impl Into<String>,
389 subject: impl Into<String>,
390 expected: Option<String>,
391 observed: Option<String>,
392 severity: SafetySeverityV1,
393) -> DiffItemV1 {
394 DiffItemV1 {
395 category: category.into(),
396 subject: subject.into(),
397 expected,
398 observed,
399 severity,
400 }
401}
402
403fn duplicate_evidence_groups<T>(
404 items: &[T],
405 subject: impl Fn(&T) -> String,
406 evidence: impl Fn(&T) -> String,
407 evidence_separator: &str,
408) -> Vec<DuplicateEvidenceGroup> {
409 let mut groups = Vec::new();
410 for (subject, entries) in group_by_subject(items, |item| Some(subject(item))) {
411 if entries.len() <= 1 {
412 continue;
413 }
414 let evidence_values = entries
415 .iter()
416 .map(|entry| evidence(entry))
417 .collect::<BTreeSet<_>>();
418 groups.push(DuplicateEvidenceGroup {
419 subject,
420 count: entries.len(),
421 evidence_label: evidence_values
422 .iter()
423 .cloned()
424 .collect::<Vec<_>>()
425 .join(evidence_separator),
426 is_conflict: evidence_values.len() > 1,
427 });
428 }
429 groups
430}
431
432fn conflicting_assignment_groups<T>(
433 items: &[T],
434 subject: impl Fn(&T) -> Option<String>,
435 value: impl Fn(&T) -> String,
436 value_separator: &str,
437) -> Vec<DuplicateEvidenceGroup> {
438 let mut groups = Vec::new();
439 for (subject, entries) in group_by_subject(items, subject) {
440 if entries.len() <= 1 {
441 continue;
442 }
443 let values = entries
444 .iter()
445 .map(|entry| value(entry))
446 .collect::<BTreeSet<_>>();
447 if values.len() <= 1 {
448 continue;
449 }
450 groups.push(DuplicateEvidenceGroup {
451 subject,
452 count: entries.len(),
453 evidence_label: values
454 .iter()
455 .cloned()
456 .collect::<Vec<_>>()
457 .join(value_separator),
458 is_conflict: true,
459 });
460 }
461 groups
462}
463
464fn group_by_subject<T>(
465 items: &[T],
466 subject: impl Fn(&T) -> Option<String>,
467) -> BTreeMap<String, Vec<&T>> {
468 let mut by_subject = BTreeMap::<String, Vec<&T>>::new();
469 for item in items {
470 if let Some(subject) = subject(item) {
471 by_subject.entry(subject).or_default().push(item);
472 }
473 }
474 by_subject
475}