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 Project,
47}
48
49pub fn run_checks(repo_root: &Path, scope: CheckScope) -> Result<CheckOutcome> {
50 let mut results = Vec::new();
51 let mut gaps = Vec::new();
52
53 if scope == CheckScope::ProjectAndGlobal
54 && let Some(lf) = lockfile::read_optional_lockfile(&lockfile::project_lockfile(repo_root))?
55 {
56 check_lockfile(repo_root, &lf, &mut results, &mut gaps);
57 }
58
59 if scope == CheckScope::Project
60 && let Some(lf) = lockfile::read_optional_lockfile(&lockfile::project_lockfile(repo_root))?
61 {
62 check_lockfile(repo_root, &lf, &mut results, &mut gaps);
63 }
64
65 if scope != CheckScope::Project
66 && let Some(home) = home_dir()
67 {
68 let lock_path = crate::paths::global_lockfile(&home);
69 if let Some(lf) = lockfile::read_optional_lockfile(&lock_path)? {
70 check_lockfile(&home, &lf, &mut results, &mut gaps);
71 }
72 }
73
74 let valid = results.iter().all(|r| r.status == "ok");
75 Ok(CheckOutcome {
76 valid,
77 results,
78 gaps,
79 })
80}
81
82fn check_lockfile(
83 scope_root: &Path,
84 lf: &lockfile::Lockfile,
85 results: &mut Vec<CheckResult>,
86 gaps: &mut Vec<PolicyGap>,
87) {
88 for (id, entry) in lf.capabilities.iter() {
89 for (target_id, target_entry) in entry.targets.iter() {
90 for unenforced in &target_entry.unenforced_rules {
91 gaps.push(PolicyGap {
92 id: id.clone(),
93 target: target_id.clone(),
94 rule: unenforced.rule,
95 description: unenforced.description.clone(),
96 reason: unenforced.reason.clone(),
97 });
98 }
99
100 let mut failing_files = Vec::new();
101
102 if target_entry.installed_path.is_empty() {
103 failing_files.push(id.clone());
104 } else {
105 let path = scope_root.join(&target_entry.installed_path);
106 match crate::cache::hash_tree(&path) {
107 Ok(hash) if hash == target_entry.sha256 => {}
108 Ok(_) | Err(_) => failing_files.push(target_entry.installed_path.clone()),
109 }
110 }
111
112 for hook in &target_entry.managed_hooks {
113 if lockfile::managed_hook_status(scope_root, hook) != "clean" {
114 failing_files.push(format!("{}#{}", hook.settings_path, hook.event));
115 }
116 }
117
118 for permission in &target_entry.managed_permissions {
119 if crate::policy::managed_permission_status(scope_root, permission) != "clean" {
120 let location = crate::policy::permission_location(permission);
121 if !failing_files.contains(&location) {
122 failing_files.push(location);
123 }
124 }
125 }
126
127 if let Some(managed_entry) = &target_entry.managed_mcp_entry
128 && lockfile::managed_mcp_entry_status(scope_root, id, managed_entry) != "clean"
129 {
130 failing_files.push(format!("{}#{}", managed_entry.config_path, id));
131 }
132
133 let status = if failing_files.is_empty() {
134 "ok"
135 } else {
136 "modified"
137 };
138
139 results.push(CheckResult {
140 id: id.clone(),
141 capability_type: entry.capability_type,
142 target: target_id.clone(),
143 status: status.to_string(),
144 files: failing_files,
145 });
146 }
147 }
148}
149
150fn home_dir() -> Option<std::path::PathBuf> {
151 std::env::var("HOME").ok().map(std::path::PathBuf::from)
152}