feat: findings refinement, new scanners, and deployment tooling (#6)
Some checks failed
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 4m3s
CI / Security Audit (push) Successful in 1m38s
CI / Tests (push) Successful in 4m44s
CI / Detect Changes (push) Successful in 2s
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) Failing after 2s
Some checks failed
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 4m3s
CI / Security Audit (push) Successful in 1m38s
CI / Tests (push) Successful in 4m44s
CI / Detect Changes (push) Successful in 2s
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) Failing after 2s
This commit was merged in pull request #6.
This commit is contained in:
@@ -187,7 +187,13 @@ pub async fn build_embeddings(
|
||||
}
|
||||
};
|
||||
|
||||
let git_ops = crate::pipeline::git::GitOps::new(&agent_clone.config.git_clone_base_path);
|
||||
let creds = crate::pipeline::git::RepoCredentials {
|
||||
ssh_key_path: Some(agent_clone.config.ssh_key_path.clone()),
|
||||
auth_token: repo.auth_token.clone(),
|
||||
auth_username: repo.auth_username.clone(),
|
||||
};
|
||||
let git_ops =
|
||||
crate::pipeline::git::GitOps::new(&agent_clone.config.git_clone_base_path, creds);
|
||||
let repo_path = match git_ops.clone_or_fetch(&repo.git_url, &repo.name) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
|
||||
@@ -291,7 +291,13 @@ pub async fn trigger_build(
|
||||
}
|
||||
};
|
||||
|
||||
let git_ops = crate::pipeline::git::GitOps::new(&agent_clone.config.git_clone_base_path);
|
||||
let creds = crate::pipeline::git::RepoCredentials {
|
||||
ssh_key_path: Some(agent_clone.config.ssh_key_path.clone()),
|
||||
auth_token: repo.auth_token.clone(),
|
||||
auth_username: repo.auth_username.clone(),
|
||||
};
|
||||
let git_ops =
|
||||
crate::pipeline::git::GitOps::new(&agent_clone.config.git_clone_base_path, creds);
|
||||
let repo_path = match git_ops.clone_or_fetch(&repo.git_url, &repo.name) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
|
||||
@@ -41,6 +41,12 @@ pub struct FindingsFilter {
|
||||
pub scan_type: Option<String>,
|
||||
#[serde(default)]
|
||||
pub status: Option<String>,
|
||||
#[serde(default)]
|
||||
pub q: Option<String>,
|
||||
#[serde(default)]
|
||||
pub sort_by: Option<String>,
|
||||
#[serde(default)]
|
||||
pub sort_order: Option<String>,
|
||||
#[serde(default = "default_page")]
|
||||
pub page: u64,
|
||||
#[serde(default = "default_limit")]
|
||||
@@ -76,6 +82,8 @@ pub struct AddRepositoryRequest {
|
||||
pub git_url: String,
|
||||
#[serde(default = "default_branch")]
|
||||
pub default_branch: String,
|
||||
pub auth_token: Option<String>,
|
||||
pub auth_username: Option<String>,
|
||||
pub tracker_type: Option<TrackerType>,
|
||||
pub tracker_owner: Option<String>,
|
||||
pub tracker_repo: Option<String>,
|
||||
@@ -91,6 +99,17 @@ pub struct UpdateStatusRequest {
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct BulkUpdateStatusRequest {
|
||||
pub ids: Vec<String>,
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct UpdateFeedbackRequest {
|
||||
pub feedback: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SbomFilter {
|
||||
#[serde(default)]
|
||||
@@ -267,9 +286,25 @@ pub async fn list_repositories(
|
||||
pub async fn add_repository(
|
||||
Extension(agent): AgentExt,
|
||||
Json(req): Json<AddRepositoryRequest>,
|
||||
) -> Result<Json<ApiResponse<TrackedRepository>>, StatusCode> {
|
||||
) -> Result<Json<ApiResponse<TrackedRepository>>, (StatusCode, String)> {
|
||||
// Validate repository access before saving
|
||||
let creds = crate::pipeline::git::RepoCredentials {
|
||||
ssh_key_path: Some(agent.config.ssh_key_path.clone()),
|
||||
auth_token: req.auth_token.clone(),
|
||||
auth_username: req.auth_username.clone(),
|
||||
};
|
||||
|
||||
if let Err(e) = crate::pipeline::git::GitOps::test_access(&req.git_url, &creds) {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
format!("Cannot access repository: {e}"),
|
||||
));
|
||||
}
|
||||
|
||||
let mut repo = TrackedRepository::new(req.name, req.git_url);
|
||||
repo.default_branch = req.default_branch;
|
||||
repo.auth_token = req.auth_token;
|
||||
repo.auth_username = req.auth_username;
|
||||
repo.tracker_type = req.tracker_type;
|
||||
repo.tracker_owner = req.tracker_owner;
|
||||
repo.tracker_repo = req.tracker_repo;
|
||||
@@ -280,7 +315,12 @@ pub async fn add_repository(
|
||||
.repositories()
|
||||
.insert_one(&repo)
|
||||
.await
|
||||
.map_err(|_| StatusCode::CONFLICT)?;
|
||||
.map_err(|_| {
|
||||
(
|
||||
StatusCode::CONFLICT,
|
||||
"Repository already exists".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(Json(ApiResponse {
|
||||
data: repo,
|
||||
@@ -289,6 +329,14 @@ pub async fn add_repository(
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_ssh_public_key(
|
||||
Extension(agent): AgentExt,
|
||||
) -> Result<Json<serde_json::Value>, StatusCode> {
|
||||
let public_path = format!("{}.pub", agent.config.ssh_key_path);
|
||||
let public_key = std::fs::read_to_string(&public_path).map_err(|_| StatusCode::NOT_FOUND)?;
|
||||
Ok(Json(serde_json::json!({ "public_key": public_key.trim() })))
|
||||
}
|
||||
|
||||
pub async fn trigger_scan(
|
||||
Extension(agent): AgentExt,
|
||||
Path(id): Path<String>,
|
||||
@@ -367,6 +415,29 @@ pub async fn list_findings(
|
||||
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
|
||||
@@ -378,7 +449,7 @@ pub async fn list_findings(
|
||||
let findings = match db
|
||||
.findings()
|
||||
.find(query)
|
||||
.sort(doc! { "created_at": -1 })
|
||||
.sort(sort_doc)
|
||||
.skip(skip)
|
||||
.limit(filter.limit)
|
||||
.await
|
||||
@@ -434,6 +505,55 @@ pub async fn update_finding_status(
|
||||
Ok(Json(serde_json::json!({ "status": "updated" })))
|
||||
}
|
||||
|
||||
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 }),
|
||||
))
|
||||
}
|
||||
|
||||
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" })))
|
||||
}
|
||||
|
||||
pub async fn list_sbom(
|
||||
Extension(agent): AgentExt,
|
||||
Query(filter): Query<SbomFilter>,
|
||||
|
||||
@@ -7,6 +7,10 @@ pub fn build_router() -> Router {
|
||||
Router::new()
|
||||
.route("/api/v1/health", get(handlers::health))
|
||||
.route("/api/v1/stats/overview", get(handlers::stats_overview))
|
||||
.route(
|
||||
"/api/v1/settings/ssh-public-key",
|
||||
get(handlers::get_ssh_public_key),
|
||||
)
|
||||
.route("/api/v1/repositories", get(handlers::list_repositories))
|
||||
.route("/api/v1/repositories", post(handlers::add_repository))
|
||||
.route(
|
||||
@@ -23,6 +27,14 @@ pub fn build_router() -> Router {
|
||||
"/api/v1/findings/{id}/status",
|
||||
patch(handlers::update_finding_status),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/findings/bulk-status",
|
||||
patch(handlers::bulk_update_finding_status),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/findings/{id}/feedback",
|
||||
patch(handlers::update_finding_feedback),
|
||||
)
|
||||
.route("/api/v1/sbom", get(handlers::list_sbom))
|
||||
.route("/api/v1/sbom/export", get(handlers::export_sbom))
|
||||
.route("/api/v1/sbom/licenses", get(handlers::license_summary))
|
||||
|
||||
Reference in New Issue
Block a user