Skip to content
Merged
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
112 changes: 108 additions & 4 deletions src/api/handlers/attestations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,16 @@ pub async fn get_summary(

let (keylime_successful, keylime_consecutive) = sum_keylime_attestation_counts(&state).await;

let (_, repo_failed) = state
let (_, repo_failed, repo_timed_out) = state
.attestation_repo
.query_counts(range_start, range_end)
.await?;

let total_successful = keylime_successful;
let total_failed = repo_failed.max(keylime_consecutive);
let total_timed_out = repo_timed_out;

let total = total_successful + total_failed;
let total = total_successful + total_failed + total_timed_out;
let success_rate = if total > 0 {
(total_successful as f64 / total as f64) * 100.0
} else {
Expand All @@ -174,6 +175,7 @@ pub async fn get_summary(
Ok(Json(ApiResponse::ok(AttestationSummary {
total_successful,
total_failed,
total_timed_out,
average_latency_ms: 0.0,
success_rate,
})))
Expand All @@ -190,17 +192,24 @@ pub async fn get_timeline(

let (keylime_successful, keylime_consecutive) = sum_keylime_attestation_counts(&state).await;

let (_, repo_failed) = state
let (_, repo_failed, repo_timed_out) = state
.attestation_repo
.query_counts(range_start, range_end)
.await?;

let fallback_successful = keylime_successful;
let fallback_failed = repo_failed.max(keylime_consecutive);
let fallback_timed_out = repo_timed_out;

let buckets = state
.attestation_repo
.query_timeline(range_start, range_end, fallback_successful, fallback_failed)
.query_timeline(
range_start,
range_end,
fallback_successful,
fallback_failed,
fallback_timed_out,
)
.await?;

Ok(Json(ApiResponse::ok(buckets)))
Expand Down Expand Up @@ -647,4 +656,99 @@ mod tests {
"when repo has no data (e.g. fresh restart), keylime consecutive should be used"
);
}

fn push_fail_agent() -> VerifierAgent {
VerifierAgent {
agent_id: "b1b2c3d4-0000-1111-2222-333344445555".into(),
attestation_status: Some("FAIL".into()),
accept_attestations: Some(false),
attestation_count: Some(50),
consecutive_attestation_failures: Some(3),
..default_verifier()
}
}

#[test]
fn fail_agent_produces_non_timeout_failure() {
let agent = push_fail_agent();
let state = AgentState::from_push_agent(&agent);
assert_eq!(state, AgentState::Fail);
assert!(state.is_failed());
assert!(!state.is_timeout());

let result = build_attestation_result(&agent, state).unwrap();
assert!(!result.success);
assert_ne!(result.failure_type, Some(FailureType::Timeout));
}

#[test]
fn timeout_state_is_timeout_but_also_failed() {
let state = AgentState::Timeout;
assert!(state.is_failed());
assert!(state.is_timeout());
}

#[test]
fn fail_state_is_failed_but_not_timeout() {
let state = AgentState::Fail;
assert!(state.is_failed());
assert!(!state.is_timeout());
}

#[tokio::test]
async fn three_way_split_counts_pass_fail_timeout_separately() {
let repo = FallbackAttestationRepository::new();

let pass = build_attestation_result(&push_pass_agent(), AgentState::Pass).unwrap();
let fail = build_attestation_result(&push_fail_agent(), AgentState::Fail).unwrap();
let timeout = build_attestation_result(&push_timeout_agent(), AgentState::Timeout).unwrap();

assert!(pass.success);
assert!(!fail.success);
assert_ne!(fail.failure_type, Some(FailureType::Timeout));
assert!(!timeout.success);
assert_eq!(timeout.failure_type, Some(FailureType::Timeout));

for _ in 0..10 {
repo.store_result(&pass).await.unwrap();
}
for _ in 0..3 {
repo.store_result(&fail).await.unwrap();
}
for _ in 0..2 {
repo.store_result(&timeout).await.unwrap();
}

let start = Utc::now() - chrono::Duration::hours(1);
let end = Utc::now() + chrono::Duration::hours(1);
let (successful, failed, timed_out) = repo.query_counts(start, end).await.unwrap();
assert_eq!(successful, 10);
assert_eq!(failed, 3);
assert_eq!(timed_out, 2);

let total = successful + failed + timed_out;
let success_rate = (successful as f64 / total as f64) * 100.0;
let expected_rate = (10.0 / 15.0) * 100.0;
assert!(
(success_rate - expected_rate).abs() < 0.01,
"success_rate should be ~66.67%, got {success_rate}"
);
}

#[tokio::test]
async fn three_way_timeline_distributes_timeout_separately() {
let repo = FallbackAttestationRepository::new();
let end = Utc::now();
let start = end - chrono::Duration::hours(6);

let buckets = repo.query_timeline(start, end, 100, 10, 5).await.unwrap();
assert_eq!(buckets.len(), 6);

let total_success: u64 = buckets.iter().map(|b| b.successful).sum();
let total_failed: u64 = buckets.iter().map(|b| b.failed).sum();
let total_timed_out: u64 = buckets.iter().map(|b| b.timed_out).sum();
assert_eq!(total_success, 100);
assert_eq!(total_failed, 10);
assert_eq!(total_timed_out, 5);
}
}
4 changes: 4 additions & 0 deletions src/models/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ impl AgentState {
| AgentState::Timeout
)
}

pub fn is_timeout(self) -> bool {
matches!(self, AgentState::Timeout)
}
}

impl TryFrom<i32> for AgentState {
Expand Down
1 change: 1 addition & 0 deletions src/models/attestation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ pub struct TimelineBucket {
pub hour: DateTime<Utc>,
pub successful: u64,
pub failed: u64,
pub timed_out: u64,
}

#[cfg(test)]
Expand Down
3 changes: 3 additions & 0 deletions src/models/kpi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct FleetKpis {
pub struct AttestationSummary {
pub total_successful: u64,
pub total_failed: u64,
pub total_timed_out: u64,
pub average_latency_ms: f64,
pub success_rate: f64,
}
Expand Down Expand Up @@ -73,12 +74,14 @@ mod tests {
let summary = AttestationSummary {
total_successful: 1000,
total_failed: 5,
total_timed_out: 3,
average_latency_ms: 42.0,
success_rate: 99.5,
};
let json = serde_json::to_value(&summary).unwrap();
assert_eq!(json["total_successful"], 1000);
assert_eq!(json["total_failed"], 5);
assert_eq!(json["total_timed_out"], 3);
assert_eq!(json["success_rate"], 99.5);
}

Expand Down
Loading