|
|
|
@@ -0,0 +1,334 @@
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
|
|
|
|
#[allow(unused_imports)]
|
|
|
|
|
use axum::extract::{Extension, Path, Query};
|
|
|
|
|
use axum::http::StatusCode;
|
|
|
|
|
use axum::Json;
|
|
|
|
|
use mongodb::bson::doc;
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
|
|
|
|
|
use compliance_core::models::*;
|
|
|
|
|
|
|
|
|
|
use crate::agent::ComplianceAgent;
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct PaginationParams {
|
|
|
|
|
#[serde(default = "default_page")]
|
|
|
|
|
pub page: u64,
|
|
|
|
|
#[serde(default = "default_limit")]
|
|
|
|
|
pub limit: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_page() -> u64 { 1 }
|
|
|
|
|
fn default_limit() -> i64 { 50 }
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct FindingsFilter {
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub repo_id: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub severity: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub scan_type: Option<String>,
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub status: Option<String>,
|
|
|
|
|
#[serde(default = "default_page")]
|
|
|
|
|
pub page: u64,
|
|
|
|
|
#[serde(default = "default_limit")]
|
|
|
|
|
pub limit: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct ApiResponse<T: Serialize> {
|
|
|
|
|
pub data: T,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub total: Option<u64>,
|
|
|
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
|
|
|
pub page: Option<u64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
pub struct OverviewStats {
|
|
|
|
|
pub total_repositories: u64,
|
|
|
|
|
pub total_findings: u64,
|
|
|
|
|
pub critical_findings: u64,
|
|
|
|
|
pub high_findings: u64,
|
|
|
|
|
pub medium_findings: u64,
|
|
|
|
|
pub low_findings: u64,
|
|
|
|
|
pub total_sbom_entries: u64,
|
|
|
|
|
pub total_cve_alerts: u64,
|
|
|
|
|
pub total_issues: u64,
|
|
|
|
|
pub recent_scans: Vec<ScanRun>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct AddRepositoryRequest {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub git_url: String,
|
|
|
|
|
#[serde(default = "default_branch")]
|
|
|
|
|
pub default_branch: String,
|
|
|
|
|
pub tracker_type: Option<TrackerType>,
|
|
|
|
|
pub tracker_owner: Option<String>,
|
|
|
|
|
pub tracker_repo: Option<String>,
|
|
|
|
|
pub scan_schedule: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn default_branch() -> String { "main".to_string() }
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct UpdateStatusRequest {
|
|
|
|
|
pub status: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type AgentExt = Extension<Arc<ComplianceAgent>>;
|
|
|
|
|
type ApiResult<T> = Result<Json<ApiResponse<T>>, StatusCode>;
|
|
|
|
|
|
|
|
|
|
pub async fn health() -> Json<serde_json::Value> {
|
|
|
|
|
Json(serde_json::json!({ "status": "ok" }))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn stats_overview(Extension(agent): AgentExt) -> ApiResult<OverviewStats> {
|
|
|
|
|
let db = &agent.db;
|
|
|
|
|
|
|
|
|
|
let total_repositories = db.repositories().count_documents(doc! {}).await.unwrap_or(0);
|
|
|
|
|
let total_findings = db.findings().count_documents(doc! {}).await.unwrap_or(0);
|
|
|
|
|
let critical_findings = db.findings().count_documents(doc! { "severity": "critical" }).await.unwrap_or(0);
|
|
|
|
|
let high_findings = db.findings().count_documents(doc! { "severity": "high" }).await.unwrap_or(0);
|
|
|
|
|
let medium_findings = db.findings().count_documents(doc! { "severity": "medium" }).await.unwrap_or(0);
|
|
|
|
|
let low_findings = db.findings().count_documents(doc! { "severity": "low" }).await.unwrap_or(0);
|
|
|
|
|
let total_sbom_entries = db.sbom_entries().count_documents(doc! {}).await.unwrap_or(0);
|
|
|
|
|
let total_cve_alerts = db.cve_alerts().count_documents(doc! {}).await.unwrap_or(0);
|
|
|
|
|
let total_issues = db.tracker_issues().count_documents(doc! {}).await.unwrap_or(0);
|
|
|
|
|
|
|
|
|
|
let recent_scans: Vec<ScanRun> = match db
|
|
|
|
|
.scan_runs()
|
|
|
|
|
.find(doc! {})
|
|
|
|
|
.sort(doc! { "started_at": -1 })
|
|
|
|
|
.limit(10)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
|
|
|
|
Err(_) => Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse {
|
|
|
|
|
data: OverviewStats {
|
|
|
|
|
total_repositories,
|
|
|
|
|
total_findings,
|
|
|
|
|
critical_findings,
|
|
|
|
|
high_findings,
|
|
|
|
|
medium_findings,
|
|
|
|
|
low_findings,
|
|
|
|
|
total_sbom_entries,
|
|
|
|
|
total_cve_alerts,
|
|
|
|
|
total_issues,
|
|
|
|
|
recent_scans,
|
|
|
|
|
},
|
|
|
|
|
total: None,
|
|
|
|
|
page: None,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn list_repositories(
|
|
|
|
|
Extension(agent): AgentExt,
|
|
|
|
|
Query(params): Query<PaginationParams>,
|
|
|
|
|
) -> ApiResult<Vec<TrackedRepository>> {
|
|
|
|
|
let db = &agent.db;
|
|
|
|
|
let skip = (params.page.saturating_sub(1)) * params.limit as u64;
|
|
|
|
|
let total = db.repositories().count_documents(doc! {}).await.unwrap_or(0);
|
|
|
|
|
|
|
|
|
|
let repos = match db.repositories().find(doc! {}).skip(skip).limit(params.limit).await {
|
|
|
|
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
|
|
|
|
Err(_) => Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse {
|
|
|
|
|
data: repos,
|
|
|
|
|
total: Some(total),
|
|
|
|
|
page: Some(params.page),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn add_repository(
|
|
|
|
|
Extension(agent): AgentExt,
|
|
|
|
|
Json(req): Json<AddRepositoryRequest>,
|
|
|
|
|
) -> Result<Json<ApiResponse<TrackedRepository>>, StatusCode> {
|
|
|
|
|
let mut repo = TrackedRepository::new(req.name, req.git_url);
|
|
|
|
|
repo.default_branch = req.default_branch;
|
|
|
|
|
repo.tracker_type = req.tracker_type;
|
|
|
|
|
repo.tracker_owner = req.tracker_owner;
|
|
|
|
|
repo.tracker_repo = req.tracker_repo;
|
|
|
|
|
repo.scan_schedule = req.scan_schedule;
|
|
|
|
|
|
|
|
|
|
agent
|
|
|
|
|
.db
|
|
|
|
|
.repositories()
|
|
|
|
|
.insert_one(&repo)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|_| StatusCode::CONFLICT)?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse {
|
|
|
|
|
data: repo,
|
|
|
|
|
total: None,
|
|
|
|
|
page: None,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn trigger_scan(
|
|
|
|
|
Extension(agent): AgentExt,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
) -> Result<Json<serde_json::Value>, StatusCode> {
|
|
|
|
|
let agent_clone = (*agent).clone();
|
|
|
|
|
tokio::spawn(async move {
|
|
|
|
|
if let Err(e) = agent_clone.run_scan(&id, ScanTrigger::Manual).await {
|
|
|
|
|
tracing::error!("Manual scan failed for {id}: {e}");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
Ok(Json(serde_json::json!({ "status": "scan_triggered" })))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn list_findings(
|
|
|
|
|
Extension(agent): AgentExt,
|
|
|
|
|
Query(filter): Query<FindingsFilter>,
|
|
|
|
|
) -> ApiResult<Vec<Finding>> {
|
|
|
|
|
let db = &agent.db;
|
|
|
|
|
let mut query = doc! {};
|
|
|
|
|
if let Some(repo_id) = &filter.repo_id {
|
|
|
|
|
query.insert("repo_id", repo_id);
|
|
|
|
|
}
|
|
|
|
|
if let Some(severity) = &filter.severity {
|
|
|
|
|
query.insert("severity", severity);
|
|
|
|
|
}
|
|
|
|
|
if let Some(scan_type) = &filter.scan_type {
|
|
|
|
|
query.insert("scan_type", scan_type);
|
|
|
|
|
}
|
|
|
|
|
if let Some(status) = &filter.status {
|
|
|
|
|
query.insert("status", status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let skip = (filter.page.saturating_sub(1)) * filter.limit as u64;
|
|
|
|
|
let total = db.findings().count_documents(query.clone()).await.unwrap_or(0);
|
|
|
|
|
|
|
|
|
|
let findings = match db.findings().find(query).sort(doc! { "created_at": -1 }).skip(skip).limit(filter.limit).await {
|
|
|
|
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
|
|
|
|
Err(_) => Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse {
|
|
|
|
|
data: findings,
|
|
|
|
|
total: Some(total),
|
|
|
|
|
page: Some(filter.page),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_finding(
|
|
|
|
|
Extension(agent): AgentExt,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
) -> Result<Json<ApiResponse<Finding>>, StatusCode> {
|
|
|
|
|
let oid = mongodb::bson::oid::ObjectId::parse_str(&id).map_err(|_| StatusCode::BAD_REQUEST)?;
|
|
|
|
|
let finding = agent
|
|
|
|
|
.db
|
|
|
|
|
.findings()
|
|
|
|
|
.find_one(doc! { "_id": oid })
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
|
|
|
|
.ok_or(StatusCode::NOT_FOUND)?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse {
|
|
|
|
|
data: finding,
|
|
|
|
|
total: None,
|
|
|
|
|
page: None,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn update_finding_status(
|
|
|
|
|
Extension(agent): AgentExt,
|
|
|
|
|
Path(id): Path<String>,
|
|
|
|
|
Json(req): Json<UpdateStatusRequest>,
|
|
|
|
|
) -> Result<Json<serde_json::Value>, StatusCode> {
|
|
|
|
|
let oid = mongodb::bson::oid::ObjectId::parse_str(&id).map_err(|_| StatusCode::BAD_REQUEST)?;
|
|
|
|
|
|
|
|
|
|
agent
|
|
|
|
|
.db
|
|
|
|
|
.findings()
|
|
|
|
|
.update_one(
|
|
|
|
|
doc! { "_id": oid },
|
|
|
|
|
doc! { "$set": { "status": &req.status, "updated_at": mongodb::bson::DateTime::now() } },
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
|
|
|
|
|
|
|
|
Ok(Json(serde_json::json!({ "status": "updated" })))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn list_sbom(
|
|
|
|
|
Extension(agent): AgentExt,
|
|
|
|
|
Query(params): Query<PaginationParams>,
|
|
|
|
|
) -> ApiResult<Vec<SbomEntry>> {
|
|
|
|
|
let db = &agent.db;
|
|
|
|
|
let skip = (params.page.saturating_sub(1)) * params.limit as u64;
|
|
|
|
|
let total = db.sbom_entries().count_documents(doc! {}).await.unwrap_or(0);
|
|
|
|
|
|
|
|
|
|
let entries = match db.sbom_entries().find(doc! {}).skip(skip).limit(params.limit).await {
|
|
|
|
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
|
|
|
|
Err(_) => Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse {
|
|
|
|
|
data: entries,
|
|
|
|
|
total: Some(total),
|
|
|
|
|
page: Some(params.page),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn list_issues(
|
|
|
|
|
Extension(agent): AgentExt,
|
|
|
|
|
Query(params): Query<PaginationParams>,
|
|
|
|
|
) -> ApiResult<Vec<TrackerIssue>> {
|
|
|
|
|
let db = &agent.db;
|
|
|
|
|
let skip = (params.page.saturating_sub(1)) * params.limit as u64;
|
|
|
|
|
let total = db.tracker_issues().count_documents(doc! {}).await.unwrap_or(0);
|
|
|
|
|
|
|
|
|
|
let issues = match db.tracker_issues().find(doc! {}).sort(doc! { "created_at": -1 }).skip(skip).limit(params.limit).await {
|
|
|
|
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
|
|
|
|
Err(_) => Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse {
|
|
|
|
|
data: issues,
|
|
|
|
|
total: Some(total),
|
|
|
|
|
page: Some(params.page),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn list_scan_runs(
|
|
|
|
|
Extension(agent): AgentExt,
|
|
|
|
|
Query(params): Query<PaginationParams>,
|
|
|
|
|
) -> ApiResult<Vec<ScanRun>> {
|
|
|
|
|
let db = &agent.db;
|
|
|
|
|
let skip = (params.page.saturating_sub(1)) * params.limit as u64;
|
|
|
|
|
let total = db.scan_runs().count_documents(doc! {}).await.unwrap_or(0);
|
|
|
|
|
|
|
|
|
|
let scans = match db.scan_runs().find(doc! {}).sort(doc! { "started_at": -1 }).skip(skip).limit(params.limit).await {
|
|
|
|
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
|
|
|
|
Err(_) => Vec::new(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(Json(ApiResponse {
|
|
|
|
|
data: scans,
|
|
|
|
|
total: Some(total),
|
|
|
|
|
page: Some(params.page),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn collect_cursor_async<T: serde::de::DeserializeOwned + Unpin + Send>(
|
|
|
|
|
mut cursor: mongodb::Cursor<T>,
|
|
|
|
|
) -> Vec<T> {
|
|
|
|
|
use futures_util::StreamExt;
|
|
|
|
|
let mut items = Vec::new();
|
|
|
|
|
while let Some(Ok(item)) = cursor.next().await {
|
|
|
|
|
items.push(item);
|
|
|
|
|
}
|
|
|
|
|
items
|
|
|
|
|
}
|