All checks were successful
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 4m19s
CI / Security Audit (push) Successful in 1m44s
CI / Detect Changes (push) Successful in 5s
CI / Tests (push) Successful in 5m15s
CI / Deploy Agent (push) Successful in 2s
CI / Deploy Dashboard (push) Successful in 2s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Successful in 2s
173 lines
5.0 KiB
Rust
173 lines
5.0 KiB
Rust
use axum::extract::{Extension, Path, Query};
|
|
use axum::http::StatusCode;
|
|
use axum::Json;
|
|
use mongodb::bson::doc;
|
|
|
|
use super::dto::*;
|
|
use compliance_core::models::Finding;
|
|
|
|
#[tracing::instrument(skip_all, fields(repo_id = ?filter.repo_id, severity = ?filter.severity, scan_type = ?filter.scan_type))]
|
|
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);
|
|
}
|
|
// Text search across title, description, file_path, rule_id
|
|
if let Some(q) = &filter.q {
|
|
if !q.is_empty() {
|
|
let regex = doc! { "$regex": q, "$options": "i" };
|
|
query.insert(
|
|
"$or",
|
|
mongodb::bson::bson!([
|
|
{ "title": regex.clone() },
|
|
{ "description": regex.clone() },
|
|
{ "file_path": regex.clone() },
|
|
{ "rule_id": regex },
|
|
]),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Dynamic sort
|
|
let sort_field = filter.sort_by.as_deref().unwrap_or("created_at");
|
|
let sort_dir: i32 = match filter.sort_order.as_deref() {
|
|
Some("asc") => 1,
|
|
_ => -1,
|
|
};
|
|
let sort_doc = doc! { sort_field: sort_dir };
|
|
|
|
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(sort_doc)
|
|
.skip(skip)
|
|
.limit(filter.limit)
|
|
.await
|
|
{
|
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
|
Err(e) => {
|
|
tracing::warn!("Failed to fetch findings: {e}");
|
|
Vec::new()
|
|
}
|
|
};
|
|
|
|
Ok(Json(ApiResponse {
|
|
data: findings,
|
|
total: Some(total),
|
|
page: Some(filter.page),
|
|
}))
|
|
}
|
|
|
|
#[tracing::instrument(skip_all, fields(finding_id = %id))]
|
|
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,
|
|
}))
|
|
}
|
|
|
|
#[tracing::instrument(skip_all, fields(finding_id = %id))]
|
|
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" })))
|
|
}
|
|
|
|
#[tracing::instrument(skip_all)]
|
|
pub async fn bulk_update_finding_status(
|
|
Extension(agent): AgentExt,
|
|
Json(req): Json<BulkUpdateStatusRequest>,
|
|
) -> Result<Json<serde_json::Value>, StatusCode> {
|
|
let oids: Vec<mongodb::bson::oid::ObjectId> = req
|
|
.ids
|
|
.iter()
|
|
.filter_map(|id| mongodb::bson::oid::ObjectId::parse_str(id).ok())
|
|
.collect();
|
|
|
|
if oids.is_empty() {
|
|
return Err(StatusCode::BAD_REQUEST);
|
|
}
|
|
|
|
let result = agent
|
|
.db
|
|
.findings()
|
|
.update_many(
|
|
doc! { "_id": { "$in": oids } },
|
|
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", "modified_count": result.modified_count }),
|
|
))
|
|
}
|
|
|
|
#[tracing::instrument(skip_all)]
|
|
pub async fn update_finding_feedback(
|
|
Extension(agent): AgentExt,
|
|
Path(id): Path<String>,
|
|
Json(req): Json<UpdateFeedbackRequest>,
|
|
) -> 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": { "developer_feedback": &req.feedback, "updated_at": mongodb::bson::DateTime::now() } },
|
|
)
|
|
.await
|
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
|
|
Ok(Json(serde_json::json!({ "status": "updated" })))
|
|
}
|