Skip to main content

ggen_cli_lib/cmds/
policy.rs

1//! Policy Commands
2//!
3//! This module provides policy management commands wired to the marketplace layer.
4
5use clap_noun_verb::Result as VerbResult;
6use clap_noun_verb_macros::verb;
7use serde::Serialize;
8
9// Re-export marketplace types for policy enforcement
10pub use ggen_core::marketplace::policy::{PackContext, PolicyReport};
11pub use ggen_core::marketplace::profile::{predefined_profiles, Profile, ProfileId};
12
13// ============================================================================
14// Output Types
15// ============================================================================
16
17#[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
75// ============================================================================
76// Domain Helper Functions
77// ============================================================================
78
79/// Load pack contexts from the current project's lockfile
80///
81/// This function reads `.ggen/packs.lock` and loads metadata for each
82/// installed pack to create `PackContext` objects for policy validation.
83///
84/// # Errors
85///
86/// Returns error if:
87/// - Lockfile doesn't exist
88/// - Lockfile is invalid
89/// - Pack metadata cannot be loaded
90fn 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
136// ============================================================================
137// Verb Functions
138// ============================================================================
139
140/// Load template_defaults and runtime from pack.toml in the cache directory.
141///
142/// Reads the `[pack]` section for `use_defaults` and `runtime` fields.
143/// Returns `(use_template_defaults, runtime)` tuple.
144fn 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/// List all available policy profiles
178#[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/// Validate current project against a policy profile
212#[verb]
213pub fn validate(profile: String) -> VerbResult<ValidateOutput> {
214    // Get the profile
215    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    // Load pack contexts from project
220    let pack_contexts = load_pack_contexts_from_project()
221        .map_err(|e| clap_noun_verb::NounVerbError::argument_error(format!("{}", e)))?;
222
223    // Enforce policy
224    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    // Format violations
229    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/// Show detailed profile information
259#[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/// Check current environment against default profile
319#[verb]
320pub fn check() -> VerbResult<ValidateOutput> {
321    // Use Production as the default profile
322    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    // Load pack contexts from project
331    let pack_contexts = load_pack_contexts_from_project()
332        .map_err(|e| clap_noun_verb::NounVerbError::argument_error(format!("{}", e)))?;
333
334    // Enforce policy
335    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    // Format violations
340    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}