Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 31 additions & 15 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ use ocean::{
storage::{EvidenceQuery, SqliteStore, Store},
};

use output::{print_evaluation_table, print_output, EvaluationResult, ModuleRunResult, OutputFormat};
use output::{
print_evaluation_table, print_output, EvaluationResult, ModuleRunResult, OutputFormat,
};

// ---------------------------------------------------------------------------
// CLI structure
Expand Down Expand Up @@ -280,9 +282,9 @@ pub fn run() -> Result<()> {
} => {
if target.is_some() || control.is_some() {
let t = target.as_deref().unwrap_or("*");
let p = control.as_deref().ok_or_else(|| {
anyhow!("--control/-c is required when using --target/-t")
})?;
let p = control
.as_deref()
.ok_or_else(|| anyhow!("--control/-c is required when using --target/-t"))?;
cmd_observe_path(&mut out, format, &cli.db, t, p, &controls_dir, !no_store)
} else if let Some(m) = module.as_deref() {
cmd_observe(&mut out, format, &cli.db, m, !no_store)
Expand All @@ -302,10 +304,19 @@ pub fn run() -> Result<()> {
} => {
if target.is_some() || control.is_some() {
let t = target.as_deref().unwrap_or("*");
let p = control.as_deref().ok_or_else(|| {
anyhow!("--control/-c is required when using --target/-t")
})?;
cmd_test_path(&mut out, format, &cli.db, t, p, &env, &controls_dir, !no_store)
let p = control
.as_deref()
.ok_or_else(|| anyhow!("--control/-c is required when using --target/-t"))?;
cmd_test_path(
&mut out,
format,
&cli.db,
t,
p,
&env,
&controls_dir,
!no_store,
)
} else if let Some(m) = module.as_deref() {
cmd_test(&mut out, format, &cli.db, m, &env, !no_store)
} else {
Expand All @@ -329,12 +340,19 @@ pub fn run() -> Result<()> {
} => {
if target.is_some() || control_path.is_some() {
let t = target.as_deref().unwrap_or("*");
let p = control_path.as_deref().ok_or_else(|| {
anyhow!("--control/-c is required when using --target/-t")
})?;
let p = control_path
.as_deref()
.ok_or_else(|| anyhow!("--control/-c is required when using --target/-t"))?;
cmd_evaluate_path(&mut out, format, &cli.db, t, p, &controls_dir)
} else if let Some(ctrl) = control.as_deref() {
cmd_evaluate(&mut out, format, &cli.db, ctrl, cel.as_deref(), &controls_dir)
cmd_evaluate(
&mut out,
format,
&cli.db,
ctrl,
cel.as_deref(),
&controls_dir,
)
} else {
Err(anyhow!(
"Specify a control ID or use --target/-t and --control/-c"
Expand Down Expand Up @@ -696,9 +714,7 @@ fn target_matches_module(target: &str, module_id: &str) -> bool {
fn resolve_controls(controls_dir: &str, path: &str) -> Result<Vec<Control>> {
let dir = std::path::Path::new(controls_dir);
if !dir.exists() {
return Err(anyhow!(
"controls directory not found: '{controls_dir}'"
));
return Err(anyhow!("controls directory not found: '{controls_dir}'"));
}

let mut all: Vec<Control> = Vec::new();
Expand Down
51 changes: 21 additions & 30 deletions src/control/composite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,10 @@ pub fn evaluate_composite_with_components(

for component in components {
let key = (component.evidence_class, component.activity_id);
let evidence_list: &[Evidence] =
evidence_by_class.get(&key).map(|v| v.as_slice()).unwrap_or(&[]);
let evidence_list: &[Evidence] = evidence_by_class
.get(&key)
.map(|v| v.as_slice())
.unwrap_or(&[]);

// Is this component effective?
let component_effective = evidence_list
Expand Down Expand Up @@ -160,8 +162,7 @@ fn evaluate_assertion(
}
}
CrossCheckAssertion::SupersetOf => {
let missing: Vec<&String> =
referenced.iter().filter(|v| !local.contains(*v)).collect();
let missing: Vec<&String> = referenced.iter().filter(|v| !local.contains(*v)).collect();
if missing.is_empty() {
(true, format!("Local values cover all of '{uses}' export"))
} else {
Expand All @@ -177,7 +178,10 @@ fn evaluate_assertion(
CrossCheckAssertion::ContainsAny => {
let has_overlap = local.iter().any(|v| referenced.contains(v));
if has_overlap {
(true, format!("At least one local value found in '{uses}' export"))
(
true,
format!("At least one local value found in '{uses}' export"),
)
} else {
(
false,
Expand Down Expand Up @@ -465,12 +469,8 @@ mod tests {
#[test]
fn cross_check_subset_of_fails_when_extra_ip() {
// Firewall allows an IP not in WAF egress — cross-check fails.
let waf_ev = make_evidence_with_observables(
3002,
1,
true,
vec![obs("ip_range", "10.0.0.1")],
);
let waf_ev =
make_evidence_with_observables(3002, 1, true, vec![obs("ip_range", "10.0.0.1")]);
let fw_ev = make_evidence_with_observables(
3001,
1,
Expand Down Expand Up @@ -519,12 +519,8 @@ mod tests {

#[test]
fn cross_check_nonempty_passes() {
let waf_ev = make_evidence_with_observables(
3002,
1,
true,
vec![obs("ip_range", "10.0.0.1")],
);
let waf_ev =
make_evidence_with_observables(3002, 1, true, vec![obs("ip_range", "10.0.0.1")]);
let mut map = HashMap::new();
map.insert((3002, Some(1)), vec![waf_ev]);

Expand Down Expand Up @@ -605,14 +601,13 @@ mod tests {
3002,
1,
true,
vec![obs("domain", "example.com"), obs("domain", "cdn.example.com")],
);
let local_ev = make_evidence_with_observables(
3001,
1,
true,
vec![obs("domain", "cdn.example.com")],
vec![
obs("domain", "example.com"),
obs("domain", "cdn.example.com"),
],
);
let local_ev =
make_evidence_with_observables(3001, 1, true, vec![obs("domain", "cdn.example.com")]);
let mut map = HashMap::new();
map.insert((3002, Some(1)), vec![export_ev]);
map.insert((3001, Some(1)), vec![local_ev]);
Expand Down Expand Up @@ -651,12 +646,8 @@ mod tests {

#[test]
fn cross_check_superset_of_passes() {
let export_ev = make_evidence_with_observables(
3002,
1,
true,
vec![obs("ip_range", "10.0.0.1")],
);
let export_ev =
make_evidence_with_observables(3002, 1, true, vec![obs("ip_range", "10.0.0.1")]);
let local_ev = make_evidence_with_observables(
3001,
1,
Expand Down
4 changes: 3 additions & 1 deletion src/control/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ pub mod definition;
pub mod evaluator;
pub mod framework;

pub use composite::{evaluate_composite, evaluate_composite_with_components, ComponentResult, CrossCheckResult};
pub use composite::{
evaluate_composite, evaluate_composite_with_components, ComponentResult, CrossCheckResult,
};
pub use definition::{
ComponentSpec, Control, ControlStatus, CrossCheck, CrossCheckAssertion, EvaluationLogic,
ExportSpec, FrameworkMapping, ModuleRef, UptimeResult,
Expand Down
19 changes: 16 additions & 3 deletions src/dashboard/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@ fn visit_yaml_files(dir: &std::path::Path, controls: &mut Vec<Control>) -> Resul
continue;
}
visit_yaml_files(&path, controls)?;
} else if path.extension().map(|e| e == "yaml" || e == "yml").unwrap_or(false) {
} else if path
.extension()
.map(|e| e == "yaml" || e == "yml")
.unwrap_or(false)
{
let content = std::fs::read_to_string(&path)?;
match Control::load_yaml(&content) {
Ok(control) => controls.push(control),
Expand Down Expand Up @@ -214,10 +218,19 @@ mod tests {
match controls {
Ok(c) => {
// We know there are at least 4 control files
assert!(c.len() >= 2, "expected at least 2 controls, got {}", c.len());
assert!(
c.len() >= 2,
"expected at least 2 controls, got {}",
c.len()
);
// Should be sorted by ID
for w in c.windows(2) {
assert!(w[0].id <= w[1].id, "controls not sorted: {} > {}", w[0].id, w[1].id);
assert!(
w[0].id <= w[1].id,
"controls not sorted: {} > {}",
w[0].id,
w[1].id
);
}
}
Err(_) => {
Expand Down
15 changes: 3 additions & 12 deletions src/dashboard/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,10 +230,7 @@ mod tests {
#[test]
fn next_wraps_around() {
let mut app = App::new();
app.controls = vec![
data::ControlRow::empty("a"),
data::ControlRow::empty("b"),
];
app.controls = vec![data::ControlRow::empty("a"), data::ControlRow::empty("b")];
app.next();
assert_eq!(app.selected, 1);
app.next();
Expand All @@ -243,10 +240,7 @@ mod tests {
#[test]
fn previous_wraps_around() {
let mut app = App::new();
app.controls = vec![
data::ControlRow::empty("a"),
data::ControlRow::empty("b"),
];
app.controls = vec![data::ControlRow::empty("a"), data::ControlRow::empty("b")];
app.previous();
assert_eq!(app.selected, 1); // wrapped from 0
}
Expand Down Expand Up @@ -325,10 +319,7 @@ mod tests {
#[test]
fn handle_key_jk_navigate() {
let mut app = App::new();
app.controls = vec![
data::ControlRow::empty("a"),
data::ControlRow::empty("b"),
];
app.controls = vec![data::ControlRow::empty("a"), data::ControlRow::empty("b")];
app.handle_key(key(KeyCode::Char('j')));
assert_eq!(app.selected, 1);
app.handle_key(key(KeyCode::Char('k')));
Expand Down
54 changes: 27 additions & 27 deletions src/dashboard/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn render_main(frame: &mut Frame, app: &App) {
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // header
Constraint::Min(5), // table
Constraint::Min(5), // table
Constraint::Length(3), // footer
])
.split(area);
Expand All @@ -34,7 +34,11 @@ fn render_main(frame: &mut Frame, app: &App) {
app.controls.len(),
);
let header = Paragraph::new(header_text)
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
.style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.block(Block::default().borders(Borders::BOTTOM));
frame.render_widget(header, chunks[0]);

Expand Down Expand Up @@ -111,8 +115,8 @@ fn render_detail(frame: &mut Frame, app: &App, idx: usize) {
let row = match app.controls.get(idx) {
Some(r) => r,
None => {
let msg = Paragraph::new("No control data available.")
.style(Style::default().fg(Color::Red));
let msg =
Paragraph::new("No control data available.").style(Style::default().fg(Color::Red));
frame.render_widget(msg, area);
return;
}
Expand All @@ -123,15 +127,19 @@ fn render_detail(frame: &mut Frame, app: &App, idx: usize) {
.constraints([
Constraint::Length(3), // header
Constraint::Length(5), // status summary
Constraint::Min(8), // evidence + transcript
Constraint::Min(8), // evidence + transcript
Constraint::Length(3), // footer
])
.split(area);

// Header
let header_text = format!(" {} — {}", row.control.id, row.control.name);
let header = Paragraph::new(header_text)
.style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD))
.style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.block(Block::default().borders(Borders::BOTTOM));
frame.render_widget(header, chunks[0]);

Expand All @@ -152,7 +160,9 @@ fn render_detail(frame: &mut Frame, app: &App, idx: usize) {
Span::raw(" Status: "),
Span::styled(
row.status_text().to_uppercase(),
Style::default().fg(status_color).add_modifier(Modifier::BOLD),
Style::default()
.fg(status_color)
.add_modifier(Modifier::BOLD),
),
Span::raw(" Confidence: "),
Span::raw(row.confidence_text()),
Expand All @@ -167,12 +177,10 @@ fn render_detail(frame: &mut Frame, app: &App, idx: usize) {

// Evidence timeline + transcript
let mut lines: Vec<Line> = Vec::new();
lines.push(Line::from(
Span::styled(
" Evidence Timeline:",
Style::default().add_modifier(Modifier::BOLD),
),
));
lines.push(Line::from(Span::styled(
" Evidence Timeline:",
Style::default().add_modifier(Modifier::BOLD),
)));

if row.evidence.is_empty() {
lines.push(Line::from(" No evidence records found."));
Expand Down Expand Up @@ -200,19 +208,14 @@ fn render_detail(frame: &mut Frame, app: &App, idx: usize) {
}

// Test transcripts
let has_transcript = row
.evidence
.iter()
.any(|e| e.test_transcript.is_some());
let has_transcript = row.evidence.iter().any(|e| e.test_transcript.is_some());

if has_transcript {
lines.push(Line::from(""));
lines.push(Line::from(
Span::styled(
" Test Transcripts:",
Style::default().add_modifier(Modifier::BOLD),
),
));
lines.push(Line::from(Span::styled(
" Test Transcripts:",
Style::default().add_modifier(Modifier::BOLD),
)));
for ev in &row.evidence {
if let Some(ref transcript) = ev.test_transcript {
lines.push(Line::from(format!(
Expand Down Expand Up @@ -245,10 +248,7 @@ fn render_detail(frame: &mut Frame, app: &App, idx: usize) {
}

// Apply scroll offset
let visible_lines: Vec<Line> = lines
.into_iter()
.skip(app.scroll_offset)
.collect();
let visible_lines: Vec<Line> = lines.into_iter().skip(app.scroll_offset).collect();

let evidence_section = Paragraph::new(visible_lines)
.block(Block::default().borders(Borders::TOP).title(" Evidence "));
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub mod secrets;
pub mod storage;

pub use evidence::{ConfidenceLevel, Evidence, StatusId};
pub use module::{Observer, Module, Registry, Tester};
pub use module::{Module, Observer, Registry, Tester};

#[cfg(test)]
pub mod testutil;
Loading