WP-20/21: contract and failing tests for vulnerability management
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
169
backend/crates/api/tests/vulnerabilities.rs
Normal file
169
backend/crates/api/tests/vulnerabilities.rs
Normal file
@ -0,0 +1,169 @@
|
||||
//! WP-20/21: /api/vulnerabilities and notification settings.
|
||||
mod common;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use common::{get, post, send_json, test_app_with_admin};
|
||||
use serde_json::json;
|
||||
|
||||
const ADMIN: &str = "admin@example.com";
|
||||
const PW: &str = "admin-password-123";
|
||||
|
||||
async fn run_scan(app: &axum::Router, token: &str) -> serde_json::Value {
|
||||
let run = post(
|
||||
app,
|
||||
"/api/jobs/run",
|
||||
json!({"kind": "vulnerability_scan"}),
|
||||
Some(token),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(run.status, StatusCode::ACCEPTED, "{}", run.json);
|
||||
let id = run.json["id"].as_str().unwrap().to_string();
|
||||
for _ in 0..100 {
|
||||
let r = get(app, &format!("/api/jobs/{id}"), Some(token)).await;
|
||||
if r.json["status"] != "running" {
|
||||
return r.json;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(30)).await;
|
||||
}
|
||||
panic!("scan did not finish");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn scan_populates_findings_summary_and_filters() {
|
||||
let app = test_app_with_admin().await;
|
||||
let token = common::login(&app, ADMIN, PW).await.access;
|
||||
|
||||
let empty = get(&app, "/api/vulnerabilities/summary", Some(&token)).await;
|
||||
assert_eq!(empty.status, StatusCode::OK);
|
||||
assert_eq!(empty.json["total"]["critical"], 0);
|
||||
assert!(empty.json["last_scan"].is_null());
|
||||
assert!(empty.json["scanner"].as_str().unwrap().contains("fake"));
|
||||
|
||||
let run = run_scan(&app, &token).await;
|
||||
assert_eq!(run["status"], "success", "{}", run["log"]);
|
||||
assert!(run["log"].as_str().unwrap().contains("new"));
|
||||
|
||||
let s = get(&app, "/api/vulnerabilities/summary", Some(&token)).await;
|
||||
assert!(s.json["total"]["critical"].as_u64().unwrap() >= 2);
|
||||
assert!(s.json["os"]["high"].as_u64().unwrap() >= 1);
|
||||
assert!(s.json["last_scan"].is_string());
|
||||
|
||||
let all = get(&app, "/api/vulnerabilities", Some(&token)).await;
|
||||
let list = all.json.as_array().unwrap();
|
||||
assert!(list.len() >= 5);
|
||||
assert_eq!(list[0]["severity"], "critical", "sorted by severity");
|
||||
let crit = get(
|
||||
&app,
|
||||
"/api/vulnerabilities?min_severity=critical",
|
||||
Some(&token),
|
||||
)
|
||||
.await;
|
||||
assert!(crit
|
||||
.json
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|f| f["severity"] == "critical"));
|
||||
let os = get(&app, "/api/vulnerabilities?target=os", Some(&token)).await;
|
||||
assert!(os
|
||||
.json
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|f| f["target"] == "os"));
|
||||
let targets = get(&app, "/api/vulnerabilities/targets", Some(&token)).await;
|
||||
assert!(targets.json.as_array().unwrap().iter().any(|t| t == "os"));
|
||||
|
||||
// acknowledge one
|
||||
let id = list[0]["id"].as_str().unwrap();
|
||||
let res = post(
|
||||
&app,
|
||||
&format!("/api/vulnerabilities/{id}/status"),
|
||||
json!({"status": "acknowledged"}),
|
||||
Some(&token),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(res.status, StatusCode::OK, "{}", res.json);
|
||||
assert_eq!(res.json["status"], "acknowledged");
|
||||
assert_eq!(
|
||||
post(
|
||||
&app,
|
||||
&format!("/api/vulnerabilities/{id}/status"),
|
||||
json!({"status": "fixed"}),
|
||||
Some(&token)
|
||||
)
|
||||
.await
|
||||
.status,
|
||||
StatusCode::UNPROCESSABLE_ENTITY
|
||||
);
|
||||
|
||||
// second scan reports nothing new
|
||||
let run = run_scan(&app, &token).await;
|
||||
assert!(
|
||||
run["log"].as_str().unwrap().contains("0 new"),
|
||||
"{}",
|
||||
run["log"]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn notification_threshold_setting_and_permissions() {
|
||||
let app = test_app_with_admin().await;
|
||||
let admin = common::login(&app, ADMIN, PW).await.access;
|
||||
let res = get(&app, "/api/settings/notifications", Some(&admin)).await;
|
||||
assert_eq!(res.json["min_severity"], "high");
|
||||
assert_eq!(
|
||||
send_json(
|
||||
&app,
|
||||
"PUT",
|
||||
"/api/settings/notifications",
|
||||
json!({"min_severity": "medium"}),
|
||||
Some(&admin)
|
||||
)
|
||||
.await
|
||||
.status,
|
||||
StatusCode::NO_CONTENT
|
||||
);
|
||||
assert_eq!(
|
||||
get(&app, "/api/settings/notifications", Some(&admin))
|
||||
.await
|
||||
.json["min_severity"],
|
||||
"medium"
|
||||
);
|
||||
|
||||
post(&app, "/api/users", json!({"email": "u@x.de", "display_name": "U", "password": "user-password-123", "role": "user"}), Some(&admin)).await;
|
||||
let user = common::login(&app, "u@x.de", "user-password-123")
|
||||
.await
|
||||
.access;
|
||||
assert_eq!(
|
||||
get(&app, "/api/vulnerabilities", Some(&user)).await.status,
|
||||
StatusCode::OK
|
||||
);
|
||||
assert_eq!(
|
||||
send_json(
|
||||
&app,
|
||||
"PUT",
|
||||
"/api/settings/notifications",
|
||||
json!({"min_severity": "low"}),
|
||||
Some(&user)
|
||||
)
|
||||
.await
|
||||
.status,
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
assert_eq!(
|
||||
post(
|
||||
&app,
|
||||
"/api/vulnerabilities/00000000-0000-0000-0000-000000000000/status",
|
||||
json!({"status": "acknowledged"}),
|
||||
Some(&user)
|
||||
)
|
||||
.await
|
||||
.status,
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
assert_eq!(
|
||||
get(&app, "/api/vulnerabilities", None).await.status,
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user