1use clap_noun_verb::Result as VerbResult;
6use clap_noun_verb_macros::verb;
7use serde::Serialize;
8
9pub use ggen_core::marketplace::policy::{PackContext, PolicyReport};
11pub use ggen_core::marketplace::profile::{predefined_profiles, Profile, ProfileId};
12
13#[derive(Serialize)]
18pub struct ListOutput {
19 pub profiles: Vec<ProfileSummary>,
20 pub total: usize,
21}
22
23#[derive(Serialize)]
24pub struct ProfileSummary {
25 pub id: String,
26 pub name: String,
27 pub description: String,
28 pub policy_count: usize,
29 pub trust_requirement: String,
30 pub receipt_requirement: String,
31}
32
33#[derive(Serialize)]
34pub struct ValidateOutput {
35 pub profile_id: String,
36 pub passed: bool,
37 pub violation_count: usize,
38 pub policies_checked: usize,
39 pub violations: Vec<ViolationSummary>,
40}
41
42#[derive(Serialize)]
43pub struct ViolationSummary {
44 pub policy_id: String,
45 pub pack_id: String,
46 pub description: String,
47}
48
49#[derive(Serialize)]
50pub struct ShowOutput {
51 pub profile_id: String,
52 pub name: String,
53 pub description: String,
54 pub policies: Vec<PolicySummary>,
55 pub trust_requirement: String,
56 pub receipt_requirement: String,
57 pub runtime_constraints: Vec<RuntimeConstraintSummary>,
58}
59
60#[derive(Serialize)]
61pub struct PolicySummary {
62 pub id: String,
63 pub name: String,
64 pub description: String,
65 pub rule_count: usize,
66}
67
68#[derive(Serialize)]
69pub struct RuntimeConstraintSummary {
70 pub allowed_runtimes: Vec<String>,
71 pub forbid_defaults: bool,
72 pub require_explicit: bool,
73}
74
75fn load_pack_contexts_from_project() -> crate::Result<Vec<PackContext>> {
91 use ggen_core::marketplace::metadata::{get_pack_cache_dir, load_pack_metadata};
92 use ggen_core::packs::lockfile::PackLockfile;
93 use std::path::Path;
94
95 let lockfile_path = Path::new(".ggen/packs.lock");
96 if !lockfile_path.exists() {
97 return Err(ggen_core::utils::error::Error::new(
98 "No project found. Please install packs first with 'ggen packs install <pack-id>'",
99 ));
100 }
101
102 let lockfile = PackLockfile::from_file(lockfile_path).map_err(|e| {
103 ggen_core::utils::error::Error::new(&format!("Failed to load lockfile: {}", e))
104 })?;
105
106 let mut pack_contexts = Vec::new();
107 for (pack_id, locked_pack) in &lockfile.packs {
108 let package_id = ggen_core::marketplace::models::PackageId::new(pack_id).map_err(|e| {
109 ggen_core::utils::error::Error::new(&format!("Invalid package ID {}: {}", pack_id, e))
110 })?;
111
112 let cache_dir = get_pack_cache_dir(&package_id, &locked_pack.version);
113
114 let metadata = load_pack_metadata(&cache_dir).map_err(|e| {
115 ggen_core::utils::error::Error::new(&format!(
116 "Failed to load metadata for pack {}: {}",
117 pack_id, e
118 ))
119 })?;
120
121 let (template_defaults, runtime) = load_pack_config_from_cache(&cache_dir);
122
123 let pack_context = PackContext::new(pack_id.clone())
124 .with_template_defaults(template_defaults)
125 .with_signed_receipts(metadata.signature.is_some())
126 .with_runtime(runtime)
127 .with_trust_tier(metadata.trust_tier)
128 .with_signature_verification(metadata.signature.is_some());
129
130 pack_contexts.push(pack_context);
131 }
132
133 Ok(pack_contexts)
134}
135
136fn load_pack_config_from_cache(cache_dir: &std::path::Path) -> (bool, Option<String>) {
145 use std::fs;
146
147 let pack_toml_path = cache_dir.join("pack.toml");
148 if !pack_toml_path.exists() {
149 return (false, None);
150 }
151
152 let content = match fs::read_to_string(&pack_toml_path) {
153 Ok(c) => c,
154 Err(_) => return (false, None),
155 };
156
157 let value: toml::Value = match toml::from_str(&content) {
158 Ok(v) => v,
159 Err(_) => return (false, None),
160 };
161
162 let template_defaults = value
163 .get("pack")
164 .and_then(|p| p.get("use_defaults"))
165 .and_then(|v| v.as_bool())
166 .unwrap_or(false);
167
168 let runtime = value
169 .get("pack")
170 .and_then(|p| p.get("runtime"))
171 .and_then(|v| v.as_str())
172 .map(|s| s.to_string());
173
174 (template_defaults, runtime)
175}
176
177#[verb]
179pub fn list(verbose: bool) -> VerbResult<ListOutput> {
180 let profiles = predefined_profiles();
181
182 if verbose {
183 log::info!("Available Policy Profiles:");
184 for profile in &profiles {
185 log::info!(" - {} ({})", profile.id.as_str(), profile.name);
186 log::info!(" Description: {}", profile.description);
187 log::info!(" Policies: {}", profile.policy_overlays.len());
188 log::info!(" Trust Tier: {:?}", profile.trust_requirements);
189 log::info!(" Receipt Spec: {:?}", profile.receipt_requirements);
190 }
191 }
192
193 let profiles_summary: Vec<ProfileSummary> = profiles
194 .iter()
195 .map(|p| ProfileSummary {
196 id: p.id.as_str().to_string(),
197 name: p.name.clone(),
198 description: p.description.clone(),
199 policy_count: p.policy_overlays.len(),
200 trust_requirement: format!("{:?}", p.trust_requirements),
201 receipt_requirement: format!("{:?}", p.receipt_requirements),
202 })
203 .collect();
204
205 Ok(ListOutput {
206 profiles: profiles_summary,
207 total: profiles.len(),
208 })
209}
210
211#[verb]
213pub fn validate(profile: String) -> VerbResult<ValidateOutput> {
214 let profile_obj = ggen_core::marketplace::profile::get_profile(&profile).map_err(|e| {
216 clap_noun_verb::NounVerbError::argument_error(format!("Profile not found: {}", e))
217 })?;
218
219 let pack_contexts = load_pack_contexts_from_project()
221 .map_err(|e| clap_noun_verb::NounVerbError::argument_error(format!("{}", e)))?;
222
223 let report = profile_obj.enforce(&pack_contexts).map_err(|e| {
225 clap_noun_verb::NounVerbError::execution_error(format!("Policy enforcement failed: {}", e))
226 })?;
227
228 let violations: Vec<ViolationSummary> = report
230 .violations
231 .iter()
232 .map(|v| ViolationSummary {
233 policy_id: v.policy_id.as_str().to_string(),
234 pack_id: v.pack_id.clone(),
235 description: v.description.clone(),
236 })
237 .collect();
238
239 if report.passed {
240 log::info!("✓ Profile '{}' validation passed", profile);
241 } else {
242 log::error!("✗ Profile '{}' validation failed", profile);
243 log::error!(" Violations: {}", report.violation_count());
244 for violation in &violations {
245 log::error!(" - {}: {}", violation.pack_id, violation.description);
246 }
247 }
248
249 Ok(ValidateOutput {
250 profile_id: profile,
251 passed: report.passed,
252 violation_count: report.violation_count(),
253 policies_checked: report.policies_checked.len(),
254 violations,
255 })
256}
257
258#[verb]
260pub fn show(profile_id: String) -> VerbResult<ShowOutput> {
261 let profile = ggen_core::marketplace::profile::get_profile(&profile_id).map_err(|e| {
262 clap_noun_verb::NounVerbError::argument_error(format!("Profile not found: {}", e))
263 })?;
264
265 log::info!("Profile: {} ({})", profile.id.as_str(), profile.name);
266 log::info!("Description: {}", profile.description);
267 log::info!("Policies ({}):", profile.policy_overlays.len());
268 for policy in &profile.policy_overlays {
269 log::info!(" - {} ({})", policy.id.as_str(), policy.name);
270 log::info!(" {}", policy.description);
271 log::info!(" Rules: {}", policy.rules.len());
272 }
273 log::info!("Trust Tier: {:?}", profile.trust_requirements);
274 log::info!("Receipt Spec: {:?}", profile.receipt_requirements);
275 log::info!(
276 "Runtime Constraints ({}):",
277 profile.runtime_constraints.len()
278 );
279 for (idx, constraint) in profile.runtime_constraints.iter().enumerate() {
280 log::info!(" Constraint {}:", idx + 1);
281 log::info!(" Allowed Runtimes: {:?}", constraint.allowed_runtimes);
282 log::info!(" Forbid Defaults: {}", constraint.forbid_defaults);
283 log::info!(" Require Explicit: {}", constraint.require_explicit);
284 }
285
286 let policies: Vec<PolicySummary> = profile
287 .policy_overlays
288 .iter()
289 .map(|p| PolicySummary {
290 id: p.id.as_str().to_string(),
291 name: p.name.clone(),
292 description: p.description.clone(),
293 rule_count: p.rules.len(),
294 })
295 .collect();
296
297 let runtime_constraints: Vec<RuntimeConstraintSummary> = profile
298 .runtime_constraints
299 .iter()
300 .map(|c| RuntimeConstraintSummary {
301 allowed_runtimes: c.allowed_runtimes.clone(),
302 forbid_defaults: c.forbid_defaults,
303 require_explicit: c.require_explicit,
304 })
305 .collect();
306
307 Ok(ShowOutput {
308 profile_id: profile.id.as_str().to_string(),
309 name: profile.name,
310 description: profile.description,
311 policies,
312 trust_requirement: format!("{:?}", profile.trust_requirements),
313 receipt_requirement: format!("{:?}", profile.receipt_requirements),
314 runtime_constraints,
315 })
316}
317
318#[verb]
320pub fn check() -> VerbResult<ValidateOutput> {
321 let profile_obj =
323 ggen_core::marketplace::profile::get_profile("enterprise-strict").map_err(|e| {
324 clap_noun_verb::NounVerbError::argument_error(format!(
325 "Default profile not found: {}",
326 e
327 ))
328 })?;
329
330 let pack_contexts = load_pack_contexts_from_project()
332 .map_err(|e| clap_noun_verb::NounVerbError::argument_error(format!("{}", e)))?;
333
334 let report = profile_obj.enforce(&pack_contexts).map_err(|e| {
336 clap_noun_verb::NounVerbError::execution_error(format!("Policy enforcement failed: {}", e))
337 })?;
338
339 let violations: Vec<ViolationSummary> = report
341 .violations
342 .iter()
343 .map(|v| ViolationSummary {
344 policy_id: v.policy_id.as_str().to_string(),
345 pack_id: v.pack_id.clone(),
346 description: v.description.clone(),
347 })
348 .collect();
349
350 if report.passed {
351 log::info!(
352 "✓ Current environment passes '{}' profile",
353 profile_obj.name
354 );
355 } else {
356 log::error!("✗ Current environment fails '{}' profile", profile_obj.name);
357 log::error!(" Violations: {}", report.violation_count());
358 for violation in &violations {
359 log::error!(" - {}: {}", violation.pack_id, violation.description);
360 }
361 }
362
363 Ok(ValidateOutput {
364 profile_id: profile_obj.id.as_str().to_string(),
365 passed: report.passed,
366 violation_count: report.violation_count(),
367 policies_checked: report.policies_checked.len(),
368 violations,
369 })
370}