fix(security): audit — fix RUSTSEC vulns, clippy warnings, dead code (#769)

- Upgrade openssl to 0.10.78 (CVE-2026-41676), jsonwebtoken to 9.4
- Suppress unmaintained-only/no-CVE advisories in .cargo/audit.toml
  with per-entry rationale
- Fix all `cargo clippy --all-targets -- -D warnings` errors across
  35 crates: derivable_impls, needless_range_loop, map_or→is_some_and/
  is_none_or, await_holding_lock (drop MutexGuard before .await),
  ptr_arg (&mut Vec→&mut [T]), useless_conversion, approximate_constant
  (2.718→E, 3.14→PI), field_reassign_with_default, manual_inspect,
  useless_vec, lines_filter_map_ok, print_literal, dead_code
- Apply `cargo fmt --all`
- Pre-existing test failure in wifi-densepose-signal
  (test_estimate_occupancy_noise_only) is not introduced by this PR
This commit is contained in:
rUv
2026-05-23 05:36:13 -04:00
committed by GitHub
parent 1906876541
commit 004a63e82d
248 changed files with 13614 additions and 5872 deletions
+21 -29
View File
@@ -2,14 +2,14 @@
//!
//! These types are used for serializing/deserializing API requests and responses.
//! They provide a clean separation between domain models and API contracts.
#![allow(missing_docs)]
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::domain::{
DisasterType, EventStatus, ZoneStatus, TriageStatus, Priority,
AlertStatus, SurvivorStatus,
AlertStatus, DisasterType, EventStatus, Priority, SurvivorStatus, TriageStatus, ZoneStatus,
};
// ============================================================================
@@ -206,9 +206,7 @@ pub enum ZoneBoundsDto {
radius: f64,
},
/// Polygon boundary (list of vertices)
Polygon {
vertices: Vec<(f64, f64)>,
},
Polygon { vertices: Vec<(f64, f64)> },
}
/// Scan parameters for a zone.
@@ -232,9 +230,15 @@ pub struct ScanParametersDto {
pub heartbeat_detection: bool,
}
fn default_sensitivity() -> f64 { 0.8 }
fn default_max_depth() -> f64 { 5.0 }
fn default_true() -> bool { true }
fn default_sensitivity() -> f64 {
0.8
}
fn default_max_depth() -> f64 {
5.0
}
fn default_true() -> bool {
true
}
impl Default for ScanParametersDto {
fn default() -> Self {
@@ -550,10 +554,7 @@ pub enum WebSocketMessage {
survivor: SurvivorResponse,
},
/// Survivor lost (signal lost)
SurvivorLost {
event_id: Uuid,
survivor_id: Uuid,
},
SurvivorLost { event_id: Uuid, survivor_id: Uuid },
/// New alert generated
AlertCreated {
event_id: Uuid,
@@ -577,14 +578,9 @@ pub enum WebSocketMessage {
new_status: EventStatusDto,
},
/// Heartbeat/keep-alive
Heartbeat {
timestamp: DateTime<Utc>,
},
Heartbeat { timestamp: DateTime<Utc> },
/// Error message
Error {
code: String,
message: String,
},
Error { code: String, message: String },
}
/// WebSocket subscription request.
@@ -592,19 +588,13 @@ pub enum WebSocketMessage {
#[serde(tag = "action", rename_all = "snake_case")]
pub enum WebSocketRequest {
/// Subscribe to events for a disaster event
Subscribe {
event_id: Uuid,
},
Subscribe { event_id: Uuid },
/// Unsubscribe from events
Unsubscribe {
event_id: Uuid,
},
Unsubscribe { event_id: Uuid },
/// Subscribe to all events
SubscribeAll,
/// Request current state
GetState {
event_id: Uuid,
},
GetState { event_id: Uuid },
}
// ============================================================================
@@ -816,7 +806,9 @@ pub struct ListEventsQuery {
pub page_size: usize,
}
fn default_page_size() -> usize { 20 }
fn default_page_size() -> usize {
20
}
/// Query parameters for listing survivors.
#[derive(Debug, Clone, Deserialize, Default)]
+4 -10
View File
@@ -2,6 +2,7 @@
//!
//! This module provides a unified error type that maps to appropriate HTTP status codes
//! and JSON error responses for the API.
#![allow(missing_docs)]
use axum::{
http::StatusCode,
@@ -23,10 +24,7 @@ use uuid::Uuid;
pub enum ApiError {
/// Resource not found (404)
#[error("Resource not found: {resource_type} with id {id}")]
NotFound {
resource_type: String,
id: String,
},
NotFound { resource_type: String, id: String },
/// Invalid request data (400)
#[error("Bad request: {message}")]
@@ -45,9 +43,7 @@ pub enum ApiError {
/// Conflict with existing resource (409)
#[error("Conflict: {message}")]
Conflict {
message: String,
},
Conflict { message: String },
/// Resource is in invalid state for operation (409)
#[error("Invalid state: {message}")]
@@ -66,9 +62,7 @@ pub enum ApiError {
/// Service unavailable (503)
#[error("Service unavailable: {message}")]
ServiceUnavailable {
message: String,
},
ServiceUnavailable { message: String },
/// Domain error from business logic
#[error("Domain error: {0}")]
@@ -15,8 +15,7 @@ use super::dto::*;
use super::error::{ApiError, ApiResult};
use super::state::AppState;
use crate::domain::{
DisasterEvent, DisasterType, ScanZone, ZoneBounds,
ScanParameters, ScanResolution, MovementType,
DisasterEvent, DisasterType, MovementType, ScanParameters, ScanResolution, ScanZone, ZoneBounds,
};
// ============================================================================
@@ -95,7 +94,7 @@ pub async fn list_events(
let total = filtered.len();
// Apply pagination
let page_size = query.page_size.min(100).max(1);
let page_size = query.page_size.clamp(1, 100);
let start = query.page * page_size;
let events: Vec<_> = filtered
.into_iter()
@@ -318,7 +317,12 @@ pub async fn add_zone(
) -> ApiResult<(StatusCode, Json<ZoneResponse>)> {
// Convert DTO to domain
let bounds = match request.bounds {
ZoneBoundsDto::Rectangle { min_x, min_y, max_x, max_y } => {
ZoneBoundsDto::Rectangle {
min_x,
min_y,
max_x,
max_y,
} => {
if max_x <= min_x || max_y <= min_y {
return Err(ApiError::validation(
"max coordinates must be greater than min coordinates",
@@ -327,7 +331,11 @@ pub async fn add_zone(
}
ZoneBounds::rectangle(min_x, min_y, max_x, max_y)
}
ZoneBoundsDto::Circle { center_x, center_y, radius } => {
ZoneBoundsDto::Circle {
center_x,
center_y,
radius,
} => {
if radius <= 0.0 {
return Err(ApiError::validation(
"radius must be positive",
@@ -713,26 +721,29 @@ fn event_to_response(event: DisasterEvent) -> EventResponse {
fn zone_to_response(zone: &ScanZone) -> ZoneResponse {
let bounds = match zone.bounds() {
ZoneBounds::Rectangle { min_x, min_y, max_x, max_y } => {
ZoneBoundsDto::Rectangle {
min_x: *min_x,
min_y: *min_y,
max_x: *max_x,
max_y: *max_y,
}
}
ZoneBounds::Circle { center_x, center_y, radius } => {
ZoneBoundsDto::Circle {
center_x: *center_x,
center_y: *center_y,
radius: *radius,
}
}
ZoneBounds::Polygon { vertices } => {
ZoneBoundsDto::Polygon {
vertices: vertices.clone(),
}
}
ZoneBounds::Rectangle {
min_x,
min_y,
max_x,
max_y,
} => ZoneBoundsDto::Rectangle {
min_x: *min_x,
min_y: *min_y,
max_x: *max_x,
max_y: *max_y,
},
ZoneBounds::Circle {
center_x,
center_y,
radius,
} => ZoneBoundsDto::Circle {
center_x: *center_x,
center_y: *center_y,
radius: *radius,
},
ZoneBounds::Polygon { vertices } => ZoneBoundsDto::Polygon {
vertices: vertices.clone(),
},
};
let params = zone.parameters();
@@ -775,7 +786,11 @@ fn survivor_to_response(survivor: &crate::Survivor) -> SurvivorResponse {
let latest_vitals = survivor.vital_signs().latest();
let vital_signs = VitalSignsSummaryDto {
breathing_rate: latest_vitals.and_then(|v| v.breathing.as_ref().map(|b| b.rate_bpm)),
breathing_type: latest_vitals.and_then(|v| v.breathing.as_ref().map(|b| format!("{:?}", b.pattern_type))),
breathing_type: latest_vitals.and_then(|v| {
v.breathing
.as_ref()
.map(|b| format!("{:?}", b.pattern_type))
}),
heart_rate: latest_vitals.and_then(|v| v.heartbeat.as_ref().map(|h| h.rate_bpm)),
has_heartbeat: latest_vitals.map(|v| v.has_heartbeat()).unwrap_or(false),
has_movement: latest_vitals.map(|v| v.has_movement()).unwrap_or(false),
@@ -786,7 +801,9 @@ fn survivor_to_response(survivor: &crate::Survivor) -> SurvivorResponse {
None
}
}),
timestamp: latest_vitals.map(|v| v.timestamp).unwrap_or_else(chrono::Utc::now),
timestamp: latest_vitals
.map(|v| v.timestamp)
.unwrap_or_else(chrono::Utc::now),
};
let metadata = {
@@ -795,7 +812,10 @@ fn survivor_to_response(survivor: &crate::Survivor) -> SurvivorResponse {
None
} else {
Some(SurvivorMetadataDto {
estimated_age_category: m.estimated_age_category.as_ref().map(|a| format!("{:?}", a)),
estimated_age_category: m
.estimated_age_category
.as_ref()
.map(|a| format!("{:?}", a)),
assigned_team: m.assigned_team.clone(),
notes: m.notes.clone(),
tags: m.tags.clone(),
@@ -1055,9 +1075,9 @@ pub async fn list_domain_events(
State(state): State<AppState>,
) -> ApiResult<Json<DomainEventsResponse>> {
let store = state.event_store();
let events = store.all().map_err(|e| ApiError::internal(
format!("Failed to read event store: {}", e),
))?;
let events = store
.all()
.map_err(|e| ApiError::internal(format!("Failed to read event store: {}", e)))?;
let event_dtos: Vec<DomainEventDto> = events
.iter()
+26 -8
View File
@@ -33,14 +33,14 @@
//! - `WS /ws/mat/stream` - Real-time survivor and alert stream
pub mod dto;
pub mod handlers;
pub mod error;
pub mod handlers;
pub mod state;
pub mod websocket;
use axum::{
Router,
routing::{get, post},
Router,
};
pub use dto::*;
@@ -64,21 +64,39 @@ pub use state::AppState;
pub fn create_router(state: AppState) -> Router {
Router::new()
// Event endpoints
.route("/api/v1/mat/events", get(handlers::list_events).post(handlers::create_event))
.route(
"/api/v1/mat/events",
get(handlers::list_events).post(handlers::create_event),
)
.route("/api/v1/mat/events/:event_id", get(handlers::get_event))
// Zone endpoints
.route("/api/v1/mat/events/:event_id/zones", get(handlers::list_zones).post(handlers::add_zone))
.route(
"/api/v1/mat/events/:event_id/zones",
get(handlers::list_zones).post(handlers::add_zone),
)
// Survivor endpoints
.route("/api/v1/mat/events/:event_id/survivors", get(handlers::list_survivors))
.route(
"/api/v1/mat/events/:event_id/survivors",
get(handlers::list_survivors),
)
// Alert endpoints
.route("/api/v1/mat/events/:event_id/alerts", get(handlers::list_alerts))
.route("/api/v1/mat/alerts/:alert_id/acknowledge", post(handlers::acknowledge_alert))
.route(
"/api/v1/mat/events/:event_id/alerts",
get(handlers::list_alerts),
)
.route(
"/api/v1/mat/alerts/:alert_id/acknowledge",
post(handlers::acknowledge_alert),
)
// Scan control endpoints (ADR-001: CSI data ingestion + pipeline control)
.route("/api/v1/mat/scan/csi", post(handlers::push_csi_data))
.route("/api/v1/mat/scan/control", post(handlers::scan_control))
.route("/api/v1/mat/scan/status", get(handlers::pipeline_status))
// Domain event store endpoint
.route("/api/v1/mat/events/domain", get(handlers::list_domain_events))
.route(
"/api/v1/mat/events/domain",
get(handlers::list_domain_events),
)
// WebSocket endpoint
.route("/ws/mat/stream", get(websocket::ws_handler))
.with_state(state)
+15 -14
View File
@@ -2,6 +2,7 @@
//!
//! This module provides the shared state that is passed to all API handlers.
//! It contains repositories, services, and real-time event broadcasting.
#![allow(missing_docs)]
use std::collections::HashMap;
use std::sync::Arc;
@@ -10,12 +11,12 @@ use parking_lot::RwLock;
use tokio::sync::broadcast;
use uuid::Uuid;
use crate::domain::{
DisasterEvent, Alert,
events::{EventStore, InMemoryEventStore},
};
use crate::detection::{DetectionPipeline, DetectionConfig};
use super::dto::WebSocketMessage;
use crate::detection::{DetectionConfig, DetectionPipeline};
use crate::domain::{
events::{EventStore, InMemoryEventStore},
Alert, DisasterEvent,
};
/// Shared application state for the API.
///
@@ -109,12 +110,16 @@ impl AppState {
/// Get scanning state.
pub fn is_scanning(&self) -> bool {
self.inner.scanning.load(std::sync::atomic::Ordering::SeqCst)
self.inner
.scanning
.load(std::sync::atomic::Ordering::SeqCst)
}
/// Set scanning state.
pub fn set_scanning(&self, state: bool) {
self.inner.scanning.store(state, std::sync::atomic::Ordering::SeqCst);
self.inner
.scanning
.store(state, std::sync::atomic::Ordering::SeqCst);
}
// ========================================================================
@@ -235,7 +240,7 @@ impl Default for AppState {
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::{DisasterType, DisasterEvent};
use crate::domain::{DisasterEvent, DisasterType};
use geo::Point;
#[test]
@@ -258,11 +263,7 @@ mod tests {
#[test]
fn test_update_event() {
let state = AppState::new();
let event = DisasterEvent::new(
DisasterType::Earthquake,
Point::new(0.0, 0.0),
"Test",
);
let event = DisasterEvent::new(DisasterType::Earthquake, Point::new(0.0, 0.0), "Test");
let id = *event.id().as_uuid();
state.store_event(event);
@@ -279,7 +280,7 @@ mod tests {
#[test]
fn test_broadcast_subscribe() {
let state = AppState::new();
let mut rx = state.subscribe();
let _rx = state.subscribe();
state.broadcast(WebSocketMessage::Heartbeat {
timestamp: chrono::Utc::now(),
@@ -76,10 +76,7 @@ use super::state::AppState;
/// description: WebSocket connection established
/// ```
#[tracing::instrument(skip(state, ws))]
pub async fn ws_handler(
State(state): State<AppState>,
ws: WebSocketUpgrade,
) -> Response {
pub async fn ws_handler(State(state): State<AppState>, ws: WebSocketUpgrade) -> Response {
ws.on_upgrade(move |socket| handle_socket(socket, state))
}
@@ -88,7 +85,8 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
let (mut sender, mut receiver) = socket.split();
// Subscription state for this connection
let subscriptions: Arc<Mutex<SubscriptionState>> = Arc::new(Mutex::new(SubscriptionState::new()));
let subscriptions: Arc<Mutex<SubscriptionState>> =
Arc::new(Mutex::new(SubscriptionState::new()));
// Subscribe to broadcast channel
let mut broadcast_rx = state.subscribe();
@@ -260,7 +258,7 @@ impl SubscriptionState {
WebSocketMessage::ZoneScanComplete { event_id, .. } => Some(*event_id),
WebSocketMessage::EventStatusChanged { event_id, .. } => Some(*event_id),
WebSocketMessage::Heartbeat { .. } => None, // Always receive
WebSocketMessage::Error { .. } => None, // Always receive
WebSocketMessage::Error { .. } => None, // Always receive
};
match event_id {