Split the vulnerability view into host and container categories
The findings of the Debian host (OS packages and applications found in the root filesystem) and of the container images in the cluster were mixed in one flat table. They are now two prominent categories: a card each with its own severity split and target count, acting as the primary selector, and a table that adapts to the selection. Host findings show the package source reported by the scanner (debian, gobinary, node-pkg …) instead of the target, container findings show the image. Backend: findings carry the scanner's package source, the list endpoint takes ?scope=host|container, and the targets endpoint reports each target with its category and open count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@ -1,9 +1,10 @@
|
||||
//! /api/vulnerabilities: findings, summary, status changes.
|
||||
use application::vuln_service::Summary;
|
||||
use application::vuln_service::{Summary, TargetSummary};
|
||||
use axum::extract::{Path, Query, State};
|
||||
use axum::routing::{get, post};
|
||||
use axum::{Json, Router};
|
||||
use domain::vuln::{Finding, FindingFilter, FindingStatus, Severity};
|
||||
use domain::vuln::{Finding, FindingFilter, FindingStatus, Severity, TargetKind};
|
||||
use domain::DomainError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
@ -23,6 +24,8 @@ pub fn router() -> Router<AppState> {
|
||||
#[derive(Deserialize)]
|
||||
pub struct ListQuery {
|
||||
pub min_severity: Option<String>,
|
||||
/// `host` (OS, packages, applications) or `container` (images in the cluster).
|
||||
pub scope: Option<String>,
|
||||
pub target: Option<String>,
|
||||
pub status: Option<String>,
|
||||
#[serde(default)]
|
||||
@ -30,15 +33,24 @@ pub struct ListQuery {
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/vulnerabilities", tag = "vulnerabilities", security(("bearer" = [])),
|
||||
params(("min_severity" = Option<String>, Query), ("target" = Option<String>, Query), ("status" = Option<String>, Query), ("include_fixed" = Option<bool>, Query)),
|
||||
params(("min_severity" = Option<String>, Query), ("scope" = Option<String>, Query), ("target" = Option<String>, Query), ("status" = Option<String>, Query), ("include_fixed" = Option<bool>, Query)),
|
||||
responses((status = 200, body = Vec<Object>)))]
|
||||
async fn list(
|
||||
State(state): State<AppState>,
|
||||
_: AuthUser,
|
||||
Query(q): Query<ListQuery>,
|
||||
) -> Result<Json<Vec<Finding>>, ApiError> {
|
||||
let target_kind = match q.scope.as_deref() {
|
||||
None => None,
|
||||
Some(s) => Some(TargetKind::parse_scope(s).ok_or_else(|| {
|
||||
DomainError::Validation(format!(
|
||||
"unknown scope '{s}', expected 'host' or 'container'"
|
||||
))
|
||||
})?),
|
||||
};
|
||||
let filter = FindingFilter {
|
||||
min_severity: q.min_severity.as_deref().map(Severity::parse),
|
||||
target_kind,
|
||||
target: q.target,
|
||||
status: q.status.as_deref().and_then(FindingStatus::parse),
|
||||
include_fixed: q.include_fixed,
|
||||
@ -70,11 +82,11 @@ async fn summary(
|
||||
}))
|
||||
}
|
||||
|
||||
#[utoipa::path(get, path = "/api/vulnerabilities/targets", tag = "vulnerabilities", security(("bearer" = [])), responses((status = 200, body = Vec<String>)))]
|
||||
#[utoipa::path(get, path = "/api/vulnerabilities/targets", tag = "vulnerabilities", security(("bearer" = [])), responses((status = 200, body = Vec<Object>)))]
|
||||
async fn targets(
|
||||
State(state): State<AppState>,
|
||||
_: AuthUser,
|
||||
) -> Result<Json<Vec<String>>, ApiError> {
|
||||
) -> Result<Json<Vec<TargetSummary>>, ApiError> {
|
||||
Ok(Json(state.vulns.targets().await?))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user