1use std::path::Path;
2
3use serde::Serialize;
4
5use crate::error::Result;
6use crate::lockfile;
7use crate::manifest::CapabilityType;
8
9#[derive(Debug, Serialize)]
10pub struct CheckResult {
11 pub id: String,
12 #[serde(rename = "type")]
13 pub capability_type: CapabilityType,
14 pub target: String,
15 pub status: String,
16 #[serde(skip_serializing_if = "Vec::is_empty")]
17 pub files: Vec<String>,
18}
19
20#[derive(Debug, Serialize)]
22pub struct PolicyGap {
23 pub id: String,
24 pub target: String,
25 pub rule: usize,
27 pub description: String,
28 pub reason: String,
29}
30
31#[derive(Debug, Serialize)]
32pub struct CheckOutcome {
33 pub valid: bool,
34 pub results: Vec<CheckResult>,
35 #[serde(skip_serializing_if = "Vec::is_empty")]
38 pub gaps: Vec<PolicyGap>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum CheckScope {
43 ProjectAndGlobal,
44 Global,
45}
46
47pub fn run_checks(repo_root: &Path, scope: CheckScope) -> Result<CheckOutcome> {
48 let mut results = Vec::new();
49 let mut gaps = Vec::new();
50
51 if scope == CheckScope::ProjectAndGlobal
52 && let Some(lf) = lockfile::read_optional_lockfile(&lockfile::project_lockfile(repo_root))?
53 {
54 check_lockfile(repo_root, &lf, &mut results, &mut gaps);
55 }
56
57 if let Some(home) = home_dir() {
58 let lock_path = crate::paths::global_lockfile(&home);
59 if let Some(lf) = lockfile::read_optional_lockfile(&lock_path)? {
60 check_lockfile(&home, &lf, &mut results, &mut gaps);
61 }
62 }
63
64 let valid = results.iter().all(|r| r.status == "ok");
65 Ok(CheckOutcome {
66 valid,
67 results,
68 gaps,
69 })
70}
71
72fn check_lockfile(
73 scope_root: &Path,
74 lf: &lockfile::Lockfile,
75 results: &mut Vec<CheckResult>,
76 gaps: &mut Vec<PolicyGap>,
77) {
78 for (id, entry) in lf.capabilities.iter() {
79 for (target_id, target_entry) in entry.targets.iter() {
80 for unenforced in &target_entry.unenforced_rules {
81 gaps.push(PolicyGap {
82 id: id.clone(),
83 target: target_id.clone(),
84 rule: unenforced.rule,
85 description: unenforced.description.clone(),
86 reason: unenforced.reason.clone(),
87 });
88 }
89
90 let mut failing_files = Vec::new();
91
92 if target_entry.installed_path.is_empty() {
93 failing_files.push(id.clone());
94 } else {
95 let path = scope_root.join(&target_entry.installed_path);
96 match crate::cache::hash_tree(&path) {
97 Ok(hash) if hash == target_entry.sha256 => {}
98 Ok(_) | Err(_) => failing_files.push(target_entry.installed_path.clone()),
99 }
100 }
101
102 for hook in &target_entry.managed_hooks {
103 if lockfile::managed_hook_status(scope_root, hook) != "clean" {
104 failing_files.push(format!("{}#{}", hook.settings_path, hook.event));
105 }
106 }
107
108 for permission in &target_entry.managed_permissions {
109 if crate::policy::managed_permission_status(scope_root, permission) != "clean" {
110 let location = crate::policy::permission_location(permission);
111 if !failing_files.contains(&location) {
112 failing_files.push(location);
113 }
114 }
115 }
116
117 if let Some(managed_entry) = &target_entry.managed_mcp_entry
118 && lockfile::managed_mcp_entry_status(scope_root, id, managed_entry) != "clean"
119 {
120 failing_files.push(format!("{}#{}", managed_entry.config_path, id));
121 }
122
123 let status = if failing_files.is_empty() {
124 "ok"
125 } else {
126 "modified"
127 };
128
129 results.push(CheckResult {
130 id: id.clone(),
131 capability_type: entry.capability_type,
132 target: target_id.clone(),
133 status: status.to_string(),
134 files: failing_files,
135 });
136 }
137 }
138}
139
140fn home_dir() -> Option<std::path::PathBuf> {
141 std::env::var("HOME").ok().map(std::path::PathBuf::from)
142}