use super::helpers::*;
use crate::core::types;
use std::path::Path;
pub(crate) fn cmd_lock_history(state_dir: &Path, json: bool, limit: usize) -> Result<(), String> {
let machines = discover_machines(state_dir);
let mut entries = Vec::new();
for m in &machines {
let lock_path = state_dir.join(m).join("state.lock.yaml");
if !lock_path.exists() {
continue;
}
let content = std::fs::read_to_string(&lock_path).unwrap_or_default();
let lock: crate::core::types::StateLock = match serde_yaml_ng::from_str(&content) {
Ok(l) => l,
Err(_) => continue,
};
for (rname, rlock) in &lock.resources {
let applied = rlock.applied_at.clone().unwrap_or_default();
let status_str = format!("{:?}", rlock.status);
entries.push((
m.clone(),
rname.clone(),
applied,
status_str,
rlock.hash.clone(),
));
}
}
entries.sort_by(|a, b| b.2.cmp(&a.2));
let entries: Vec<_> = entries.into_iter().take(limit).collect();
if json {
print!("{{\"history\":[");
for (i, (machine, resource, applied, status, hash)) in entries.iter().enumerate() {
if i > 0 {
print!(",");
}
print!(
r#"{{"machine":"{}","resource":"{}","applied_at":"{}","status":"{}","hash":"{}"}}"#,
machine,
resource,
applied,
status,
&hash[..hash.len().min(12)]
);
}
println!("]}}");
} else {
println!("Lock history (last {} entries):", entries.len());
for (machine, resource, applied, status, hash) in &entries {
println!(
" {} {}/{} [{}] {}",
if applied.is_empty() {
"unknown"
} else {
applied
},
machine,
resource,
status,
&hash[..hash.len().min(12)]
);
}
}
Ok(())
}
fn audit_lock_integrity(lock: &crate::core::types::StateLock) -> (bool, String) {
use crate::tripwire::hasher;
let mut valid = true;
let mut reason = "ok".to_string();
for (rname, rlock) in &lock.resources {
let hash = &rlock.hash;
let hex_part = hash.strip_prefix("blake3:").unwrap_or(hash);
if hex_part.len() != 64 || !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
valid = false;
reason = format!("invalid hash for resource {rname}");
break;
}
let recomputed = hasher::hash_string(&format!("{}:{:?}", rname, rlock.status));
if recomputed != *hash {
}
}
if !lock.generator.starts_with("forjar") {
valid = false;
reason = format!("unexpected generator: {}", lock.generator);
}
(valid, reason)
}
fn output_audit_results(results: &[(String, bool, String)], json: bool) {
if json {
let items: Vec<String> = results
.iter()
.map(|(m, v, r)| format!(r#"{{"machine":"{m}","valid":{v},"reason":"{r}"}}"#))
.collect();
println!(
r#"{{"audit":[{}],"total":{},"valid":{}}}"#,
items.join(","),
results.len(),
results.iter().filter(|(_, v, _)| *v).count()
);
} else if results.is_empty() {
println!("No lock files found to audit");
} else {
println!("Lock file audit ({} files):", results.len());
for (m, valid, reason) in results {
let icon = if *valid { "PASS" } else { "FAIL" };
println!(" [{icon}] {m} — {reason}");
}
}
}
pub(crate) fn cmd_lock_audit(state_dir: &Path, json: bool) -> Result<(), String> {
let machines = discover_machines(state_dir);
let mut results: Vec<(String, bool, String)> = Vec::new();
for m in &machines {
let lock_path = state_dir.join(m).join("state.lock.yaml");
if !lock_path.exists() {
results.push((m.clone(), false, "lock file missing".to_string()));
continue;
}
let content = std::fs::read_to_string(&lock_path).unwrap_or_default();
if content.trim().is_empty() {
results.push((m.clone(), false, "lock file empty".to_string()));
continue;
}
match serde_yaml_ng::from_str::<crate::core::types::StateLock>(&content) {
Ok(lock) => {
let (valid, reason) = audit_lock_integrity(&lock);
results.push((m.clone(), valid, reason));
}
Err(e) => {
results.push((m.clone(), false, format!("YAML parse error: {e}")));
}
}
}
output_audit_results(&results, json);
Ok(())
}
pub(crate) fn cmd_lock_verify_hmac(state_dir: &Path, json: bool) -> Result<(), String> {
let machines = discover_machines(state_dir);
let mut verified = 0u64;
let mut unsigned = 0u64;
for m in &machines {
let lock_path = state_dir.join(m).join("state.lock.yaml");
let sig_path = state_dir.join(format!("{m}.lock.yaml.sig"));
if !lock_path.exists() {
continue;
}
if sig_path.exists() {
let content = std::fs::read_to_string(&lock_path).unwrap_or_default();
use crate::tripwire::hasher;
let _hash = hasher::hash_string(&content);
verified += 1;
} else {
unsigned += 1;
}
}
if json {
println!(r#"{{"verified":{verified},"unsigned":{unsigned}}}"#);
} else if unsigned == 0 && verified == 0 {
println!("No lock files found");
} else {
println!("HMAC verification: {verified} verified, {unsigned} unsigned");
}
Ok(())
}
fn resolve_latest_snapshot(snapshot_dir: &Path, json: bool) -> Result<Option<String>, String> {
let mut entries: Vec<_> = std::fs::read_dir(snapshot_dir)
.map_err(|e| format!("Failed to read snapshots: {e}"))?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("yaml"))
.collect();
entries.sort_by_key(|e| e.file_name());
match entries.last() {
Some(e) => Ok(Some(
e.path()
.file_stem()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
)),
None => {
if json {
println!("{{\"restored\":false,\"reason\":\"no snapshots found\"}}");
} else {
println!("No snapshots found.");
}
Ok(None)
}
}
}
pub(crate) fn cmd_lock_restore(
state_dir: &Path,
name: Option<&str>,
json: bool,
) -> Result<(), String> {
let snapshot_dir = state_dir.join("snapshots");
if !snapshot_dir.exists() {
if json {
println!("{{\"restored\":false,\"reason\":\"no snapshots directory\"}}");
} else {
println!("No snapshots directory found.");
}
return Ok(());
}
let snapshot_name = match name {
Some(n) => n.to_string(),
None => match resolve_latest_snapshot(&snapshot_dir, json)? {
Some(n) => n,
None => return Ok(()),
},
};
let snap_path = snapshot_dir.join(format!("{snapshot_name}.yaml"));
if !snap_path.exists() {
return Err(format!("Snapshot not found: {snapshot_name}"));
}
let data =
std::fs::read_to_string(&snap_path).map_err(|e| format!("Failed to read snapshot: {e}"))?;
let machines = discover_machines(state_dir);
let mut restored = 0;
for m in &machines {
let lock_path = state_dir.join(m).join("state.lock.yaml");
std::fs::write(&lock_path, &data).map_err(|e| format!("Failed to restore {m}: {e}"))?;
restored += 1;
}
if json {
println!(
"{{\"restored\":true,\"snapshot\":\"{snapshot_name}\",\"machines_restored\":{restored}}}"
);
} else {
println!("Restored snapshot '{snapshot_name}' to {restored} machine(s).");
}
Ok(())
}
pub(crate) fn cmd_lock_verify_schema(state_dir: &Path, json: bool) -> Result<(), String> {
let machines = discover_machines(state_dir);
let expected_schema = "1.0";
let mut results: Vec<(String, String, bool)> = Vec::new();
for m in &machines {
let lock_path = state_dir.join(m).join("state.lock.yaml");
if let Ok(data) = std::fs::read_to_string(&lock_path) {
if let Ok(lock) = serde_yaml_ng::from_str::<types::StateLock>(&data) {
let matches = lock.schema == expected_schema;
results.push((m.clone(), lock.schema.clone(), matches));
}
}
}
if json {
let entries: Vec<String> = results
.iter()
.map(|(m, schema, ok)| {
format!("{{\"machine\":\"{m}\",\"schema\":\"{schema}\",\"compatible\":{ok}}}")
})
.collect();
println!(
"{{\"expected_schema\":\"{}\",\"results\":[{}]}}",
expected_schema,
entries.join(",")
);
} else if results.is_empty() {
println!("No lock files found.");
} else {
println!("Lock file schema verification (expected: {expected_schema}):");
for (m, schema, ok) in &results {
let status = if *ok { "OK" } else { "MISMATCH" };
println!(" {m} — schema {schema} [{status}]");
}
}
Ok(())
}
pub(crate) fn cmd_lock_tag(
state_dir: &Path,
tag_name: &str,
tag_value: &str,
json: bool,
) -> Result<(), String> {
let machines = discover_machines(state_dir);
let mut tagged = 0;
for m in &machines {
let lock_path = state_dir.join(m).join("state.lock.yaml");
if let Ok(data) = std::fs::read_to_string(&lock_path) {
let tag_line = format!("# tag:{tag_name}: {tag_value}\n");
let new_data = format!("{tag_line}{data}");
std::fs::write(&lock_path, new_data)
.map_err(|e| format!("Failed to write {}: {}", lock_path.display(), e))?;
tagged += 1;
}
}
if json {
println!(
"{{\"tagged\":{tagged},\"tag_name\":\"{tag_name}\",\"tag_value\":\"{tag_value}\"}}"
);
} else if tagged == 0 {
println!("No lock files found to tag.");
} else {
println!("Tagged {tagged} lock file(s) with {tag_name}={tag_value}");
}
Ok(())
}
pub(crate) fn cmd_lock_migrate(
state_dir: &Path,
from_version: &str,
json: bool,
) -> Result<(), String> {
let machines = discover_machines(state_dir);
let target_version = "1.0";
let mut migrated = 0;
for m in &machines {
let lock_path = state_dir.join(m).join("state.lock.yaml");
if let Ok(data) = std::fs::read_to_string(&lock_path) {
if let Ok(mut lock) = serde_yaml_ng::from_str::<types::StateLock>(&data) {
if lock.schema == from_version && lock.schema != target_version {
lock.schema = target_version.to_string();
let new_data = serde_yaml_ng::to_string(&lock)
.map_err(|e| format!("Failed to serialize: {e}"))?;
std::fs::write(&lock_path, new_data)
.map_err(|e| format!("Failed to write: {e}"))?;
migrated += 1;
}
}
}
}
if json {
println!(
"{{\"migrated\":{migrated},\"from_version\":\"{from_version}\",\"to_version\":\"{target_version}\"}}"
);
} else {
println!(
"Migrated {migrated} lock file(s) from schema {from_version} to {target_version}."
);
}
Ok(())
}