feat: enhance tracing with field attributes and warn logging across all handlers
All checks were successful
CI / Tests (push) Successful in 5m17s
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 3s
CI / Deploy Dashboard (push) Has been skipped
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Has been skipped
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 4m38s
CI / Security Audit (push) Successful in 1m50s
All checks were successful
CI / Tests (push) Successful in 5m17s
CI / Detect Changes (push) Successful in 3s
CI / Deploy Agent (push) Successful in 3s
CI / Deploy Dashboard (push) Has been skipped
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Has been skipped
CI / Format (push) Successful in 4s
CI / Clippy (push) Successful in 4m38s
CI / Security Audit (push) Successful in 1m50s
Add repo_id, finding_id, and filter fields to tracing::instrument attributes for better trace correlation in SigNoz. Replace all silently swallowed errors (Err(_) => Vec::new()) with tracing::warn! logging across mod.rs, dast.rs, graph.rs handlers. Add stage-level spans with .instrument() to pipeline orchestrator for visibility into scan phases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ use super::ApiResponse;
|
|||||||
type AgentExt = Extension<Arc<ComplianceAgent>>;
|
type AgentExt = Extension<Arc<ComplianceAgent>>;
|
||||||
|
|
||||||
/// POST /api/v1/chat/:repo_id — Send a chat message with RAG context
|
/// POST /api/v1/chat/:repo_id — Send a chat message with RAG context
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id))]
|
||||||
pub async fn chat(
|
pub async fn chat(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(repo_id): Path<String>,
|
Path(repo_id): Path<String>,
|
||||||
@@ -127,7 +127,7 @@ pub async fn chat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// POST /api/v1/chat/:repo_id/build-embeddings — Trigger embedding build
|
/// POST /api/v1/chat/:repo_id/build-embeddings — Trigger embedding build
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id))]
|
||||||
pub async fn build_embeddings(
|
pub async fn build_embeddings(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(repo_id): Path<String>,
|
Path(repo_id): Path<String>,
|
||||||
@@ -228,7 +228,7 @@ pub async fn build_embeddings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/v1/chat/:repo_id/status — Get latest embedding build status
|
/// GET /api/v1/chat/:repo_id/status — Get latest embedding build status
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id))]
|
||||||
pub async fn embedding_status(
|
pub async fn embedding_status(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(repo_id): Path<String>,
|
Path(repo_id): Path<String>,
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ pub async fn list_targets(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch DAST targets: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(ApiResponse {
|
Ok(Json(ApiResponse {
|
||||||
@@ -101,7 +104,7 @@ pub async fn add_target(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// POST /api/v1/dast/targets/:id/scan — Trigger DAST scan
|
/// POST /api/v1/dast/targets/:id/scan — Trigger DAST scan
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(target_id = %id))]
|
||||||
pub async fn trigger_scan(
|
pub async fn trigger_scan(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
@@ -163,7 +166,10 @@ pub async fn list_scan_runs(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch DAST scan runs: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(ApiResponse {
|
Ok(Json(ApiResponse {
|
||||||
@@ -196,7 +202,10 @@ pub async fn list_findings(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch DAST findings: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(ApiResponse {
|
Ok(Json(ApiResponse {
|
||||||
@@ -207,7 +216,7 @@ pub async fn list_findings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/v1/dast/findings/:id — Finding detail with evidence
|
/// GET /api/v1/dast/findings/:id — Finding detail with evidence
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(finding_id = %id))]
|
||||||
pub async fn get_finding(
|
pub async fn get_finding(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ fn default_search_limit() -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/v1/graph/:repo_id — Full graph data
|
/// GET /api/v1/graph/:repo_id — Full graph data
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id))]
|
||||||
pub async fn get_graph(
|
pub async fn get_graph(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(repo_id): Path<String>,
|
Path(repo_id): Path<String>,
|
||||||
@@ -55,11 +55,17 @@ pub async fn get_graph(
|
|||||||
|
|
||||||
let all_nodes: Vec<CodeNode> = match db.graph_nodes().find(filter.clone()).await {
|
let all_nodes: Vec<CodeNode> = match db.graph_nodes().find(filter.clone()).await {
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch graph nodes: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let edges: Vec<CodeEdge> = match db.graph_edges().find(filter).await {
|
let edges: Vec<CodeEdge> = match db.graph_edges().find(filter).await {
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch graph edges: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove disconnected nodes (no edges) to keep the graph clean
|
// Remove disconnected nodes (no edges) to keep the graph clean
|
||||||
@@ -89,7 +95,7 @@ pub async fn get_graph(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/v1/graph/:repo_id/nodes — List nodes (paginated)
|
/// GET /api/v1/graph/:repo_id/nodes — List nodes (paginated)
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id))]
|
||||||
pub async fn get_nodes(
|
pub async fn get_nodes(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(repo_id): Path<String>,
|
Path(repo_id): Path<String>,
|
||||||
@@ -99,7 +105,10 @@ pub async fn get_nodes(
|
|||||||
|
|
||||||
let nodes: Vec<CodeNode> = match db.graph_nodes().find(filter).await {
|
let nodes: Vec<CodeNode> = match db.graph_nodes().find(filter).await {
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch graph nodes: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let total = nodes.len() as u64;
|
let total = nodes.len() as u64;
|
||||||
@@ -111,7 +120,7 @@ pub async fn get_nodes(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/v1/graph/:repo_id/communities — List detected communities
|
/// GET /api/v1/graph/:repo_id/communities — List detected communities
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id))]
|
||||||
pub async fn get_communities(
|
pub async fn get_communities(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(repo_id): Path<String>,
|
Path(repo_id): Path<String>,
|
||||||
@@ -121,7 +130,10 @@ pub async fn get_communities(
|
|||||||
|
|
||||||
let nodes: Vec<CodeNode> = match db.graph_nodes().find(filter).await {
|
let nodes: Vec<CodeNode> = match db.graph_nodes().find(filter).await {
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch graph nodes for communities: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut communities: std::collections::HashMap<u32, Vec<String>> =
|
let mut communities: std::collections::HashMap<u32, Vec<String>> =
|
||||||
@@ -161,7 +173,7 @@ pub struct CommunityInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/v1/graph/:repo_id/impact/:finding_id — Impact analysis
|
/// GET /api/v1/graph/:repo_id/impact/:finding_id — Impact analysis
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id, finding_id = %finding_id))]
|
||||||
pub async fn get_impact(
|
pub async fn get_impact(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path((repo_id, finding_id)): Path<(String, String)>,
|
Path((repo_id, finding_id)): Path<(String, String)>,
|
||||||
@@ -183,7 +195,7 @@ pub async fn get_impact(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/v1/graph/:repo_id/search — BM25 symbol search
|
/// GET /api/v1/graph/:repo_id/search — BM25 symbol search
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id, query = %params.q))]
|
||||||
pub async fn search_symbols(
|
pub async fn search_symbols(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(repo_id): Path<String>,
|
Path(repo_id): Path<String>,
|
||||||
@@ -204,7 +216,10 @@ pub async fn search_symbols(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to search graph nodes: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let total = nodes.len() as u64;
|
let total = nodes.len() as u64;
|
||||||
@@ -216,7 +231,7 @@ pub async fn search_symbols(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// GET /api/v1/graph/:repo_id/file-content — Read source file from cloned repo
|
/// GET /api/v1/graph/:repo_id/file-content — Read source file from cloned repo
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id))]
|
||||||
pub async fn get_file_content(
|
pub async fn get_file_content(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(repo_id): Path<String>,
|
Path(repo_id): Path<String>,
|
||||||
@@ -278,7 +293,7 @@ pub struct FileContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// POST /api/v1/graph/:repo_id/build — Trigger graph rebuild
|
/// POST /api/v1/graph/:repo_id/build — Trigger graph rebuild
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id))]
|
||||||
pub async fn trigger_build(
|
pub async fn trigger_build(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(repo_id): Path<String>,
|
Path(repo_id): Path<String>,
|
||||||
|
|||||||
@@ -234,7 +234,10 @@ pub async fn stats_overview(Extension(agent): AgentExt) -> ApiResult<OverviewSta
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch recent scans: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(ApiResponse {
|
Ok(Json(ApiResponse {
|
||||||
@@ -276,7 +279,10 @@ pub async fn list_repositories(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch repositories: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(ApiResponse {
|
Ok(Json(ApiResponse {
|
||||||
@@ -342,7 +348,7 @@ pub async fn get_ssh_public_key(
|
|||||||
Ok(Json(serde_json::json!({ "public_key": public_key.trim() })))
|
Ok(Json(serde_json::json!({ "public_key": public_key.trim() })))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %id))]
|
||||||
pub async fn trigger_scan(
|
pub async fn trigger_scan(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
@@ -357,7 +363,7 @@ pub async fn trigger_scan(
|
|||||||
Ok(Json(serde_json::json!({ "status": "scan_triggered" })))
|
Ok(Json(serde_json::json!({ "status": "scan_triggered" })))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %id))]
|
||||||
pub async fn delete_repository(
|
pub async fn delete_repository(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
@@ -404,7 +410,7 @@ pub async fn delete_repository(
|
|||||||
Ok(Json(serde_json::json!({ "status": "deleted" })))
|
Ok(Json(serde_json::json!({ "status": "deleted" })))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = ?filter.repo_id, severity = ?filter.severity, scan_type = ?filter.scan_type))]
|
||||||
pub async fn list_findings(
|
pub async fn list_findings(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Query(filter): Query<FindingsFilter>,
|
Query(filter): Query<FindingsFilter>,
|
||||||
@@ -463,7 +469,10 @@ pub async fn list_findings(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch findings: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(ApiResponse {
|
Ok(Json(ApiResponse {
|
||||||
@@ -473,7 +482,7 @@ pub async fn list_findings(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(finding_id = %id))]
|
||||||
pub async fn get_finding(
|
pub async fn get_finding(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
@@ -494,7 +503,7 @@ pub async fn get_finding(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(finding_id = %id))]
|
||||||
pub async fn update_finding_status(
|
pub async fn update_finding_status(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Path(id): Path<String>,
|
Path(id): Path<String>,
|
||||||
@@ -598,7 +607,7 @@ pub async fn sbom_filters(
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = ?filter.repo_id, package_manager = ?filter.package_manager))]
|
||||||
pub async fn list_sbom(
|
pub async fn list_sbom(
|
||||||
Extension(agent): AgentExt,
|
Extension(agent): AgentExt,
|
||||||
Query(filter): Query<SbomFilter>,
|
Query(filter): Query<SbomFilter>,
|
||||||
@@ -644,7 +653,10 @@ pub async fn list_sbom(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch SBOM entries: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(ApiResponse {
|
Ok(Json(ApiResponse {
|
||||||
@@ -666,7 +678,10 @@ pub async fn export_sbom(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch SBOM entries for export: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let body = if params.format == "spdx" {
|
let body = if params.format == "spdx" {
|
||||||
@@ -799,7 +814,10 @@ pub async fn license_summary(
|
|||||||
|
|
||||||
let entries: Vec<SbomEntry> = match db.sbom_entries().find(query).await {
|
let entries: Vec<SbomEntry> = match db.sbom_entries().find(query).await {
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch SBOM entries for license summary: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut license_map: std::collections::HashMap<String, Vec<String>> =
|
let mut license_map: std::collections::HashMap<String, Vec<String>> =
|
||||||
@@ -845,7 +863,10 @@ pub async fn sbom_diff(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch SBOM entries for repo_a: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let entries_b: Vec<SbomEntry> = match db
|
let entries_b: Vec<SbomEntry> = match db
|
||||||
@@ -854,7 +875,10 @@ pub async fn sbom_diff(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch SBOM entries for repo_b: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build maps by (name, package_manager) -> version
|
// Build maps by (name, package_manager) -> version
|
||||||
@@ -944,7 +968,10 @@ pub async fn list_issues(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch tracker issues: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(ApiResponse {
|
Ok(Json(ApiResponse {
|
||||||
@@ -972,7 +999,10 @@ pub async fn list_scan_runs(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(cursor) => collect_cursor_async(cursor).await,
|
Ok(cursor) => collect_cursor_async(cursor).await,
|
||||||
Err(_) => Vec::new(),
|
Err(e) => {
|
||||||
|
tracing::warn!("Failed to fetch scan runs: {e}");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Json(ApiResponse {
|
Ok(Json(ApiResponse {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ impl CveScanner {
|
|||||||
repo_id: &str,
|
repo_id: &str,
|
||||||
entries: &mut [SbomEntry],
|
entries: &mut [SbomEntry],
|
||||||
) -> Result<Vec<CveAlert>, CoreError> {
|
) -> Result<Vec<CveAlert>, CoreError> {
|
||||||
|
tracing::info!("scanning {} SBOM entries for known CVEs", entries.len());
|
||||||
let mut alerts = Vec::new();
|
let mut alerts = Vec::new();
|
||||||
|
|
||||||
// Batch query OSV.dev
|
// Batch query OSV.dev
|
||||||
@@ -93,7 +94,10 @@ impl CveScanner {
|
|||||||
.json(&body)
|
.json(&body)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| CoreError::Http(format!("OSV.dev request failed: {e}")))?;
|
.map_err(|e| {
|
||||||
|
tracing::warn!("OSV.dev API call failed: {e}");
|
||||||
|
CoreError::Http(format!("OSV.dev request failed: {e}"))
|
||||||
|
})?;
|
||||||
|
|
||||||
if !resp.status().is_success() {
|
if !resp.status().is_success() {
|
||||||
let status = resp.status();
|
let status = resp.status();
|
||||||
@@ -104,10 +108,10 @@ impl CveScanner {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: OsvBatchResponse = resp
|
let result: OsvBatchResponse = resp.json().await.map_err(|e| {
|
||||||
.json()
|
tracing::warn!("failed to parse OSV.dev response: {e}");
|
||||||
.await
|
CoreError::Http(format!("Failed to parse OSV.dev response: {e}"))
|
||||||
.map_err(|e| CoreError::Http(format!("Failed to parse OSV.dev response: {e}")))?;
|
})?;
|
||||||
|
|
||||||
let chunk_vulns = result.results.into_iter().map(|r| {
|
let chunk_vulns = result.results.into_iter().map(|r| {
|
||||||
r.vulns
|
r.vulns
|
||||||
|
|||||||
@@ -78,10 +78,12 @@ impl GitOps {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(repo_name = %repo_name))]
|
||||||
pub fn clone_or_fetch(&self, git_url: &str, repo_name: &str) -> Result<PathBuf, AgentError> {
|
pub fn clone_or_fetch(&self, git_url: &str, repo_name: &str) -> Result<PathBuf, AgentError> {
|
||||||
let repo_path = self.base_path.join(repo_name);
|
let repo_path = self.base_path.join(repo_name);
|
||||||
|
|
||||||
if repo_path.exists() {
|
if repo_path.exists() {
|
||||||
|
tracing::info!("fetching updates for existing repo");
|
||||||
self.fetch(&repo_path)?;
|
self.fetch(&repo_path)?;
|
||||||
} else {
|
} else {
|
||||||
std::fs::create_dir_all(&repo_path)?;
|
std::fs::create_dir_all(&repo_path)?;
|
||||||
@@ -92,7 +94,9 @@ impl GitOps {
|
|||||||
Ok(repo_path)
|
Ok(repo_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
fn clone_repo(&self, git_url: &str, repo_path: &Path) -> Result<(), AgentError> {
|
fn clone_repo(&self, git_url: &str, repo_path: &Path) -> Result<(), AgentError> {
|
||||||
|
tracing::info!("cloning repo from {}", git_url);
|
||||||
let mut builder = git2::build::RepoBuilder::new();
|
let mut builder = git2::build::RepoBuilder::new();
|
||||||
let fetch_opts = self.credentials.fetch_options();
|
let fetch_opts = self.credentials.fetch_options();
|
||||||
builder.fetch_options(fetch_opts);
|
builder.fetch_options(fetch_opts);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use mongodb::bson::doc;
|
use mongodb::bson::doc;
|
||||||
|
use tracing::Instrument;
|
||||||
|
|
||||||
use compliance_core::models::*;
|
use compliance_core::models::*;
|
||||||
use compliance_core::traits::Scanner;
|
use compliance_core::traits::Scanner;
|
||||||
@@ -50,7 +51,7 @@ impl PipelineOrchestrator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id, trigger = ?trigger))]
|
||||||
pub async fn run(&self, repo_id: &str, trigger: ScanTrigger) -> Result<(), AgentError> {
|
pub async fn run(&self, repo_id: &str, trigger: ScanTrigger) -> Result<(), AgentError> {
|
||||||
// Look up the repository
|
// Look up the repository
|
||||||
let repo = self
|
let repo = self
|
||||||
@@ -90,6 +91,7 @@ impl PipelineOrchestrator {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
tracing::error!(repo_id, error = %e, "Scan pipeline failed");
|
||||||
self.db
|
self.db
|
||||||
.scan_runs()
|
.scan_runs()
|
||||||
.update_one(
|
.update_one(
|
||||||
@@ -109,7 +111,7 @@ impl PipelineOrchestrator {
|
|||||||
result.map(|_| ())
|
result.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all, fields(repo_id = repo.name.as_str()))]
|
||||||
async fn run_pipeline(
|
async fn run_pipeline(
|
||||||
&self,
|
&self,
|
||||||
repo: &TrackedRepository,
|
repo: &TrackedRepository,
|
||||||
@@ -138,8 +140,13 @@ impl PipelineOrchestrator {
|
|||||||
// Stage 1: Semgrep SAST
|
// Stage 1: Semgrep SAST
|
||||||
tracing::info!("[{repo_id}] Stage 1: Semgrep SAST");
|
tracing::info!("[{repo_id}] Stage 1: Semgrep SAST");
|
||||||
self.update_phase(scan_run_id, "sast").await;
|
self.update_phase(scan_run_id, "sast").await;
|
||||||
let semgrep = SemgrepScanner;
|
match async {
|
||||||
match semgrep.scan(&repo_path, &repo_id).await {
|
let semgrep = SemgrepScanner;
|
||||||
|
semgrep.scan(&repo_path, &repo_id).await
|
||||||
|
}
|
||||||
|
.instrument(tracing::info_span!("stage_sast"))
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(output) => all_findings.extend(output.findings),
|
Ok(output) => all_findings.extend(output.findings),
|
||||||
Err(e) => tracing::warn!("[{repo_id}] Semgrep failed: {e}"),
|
Err(e) => tracing::warn!("[{repo_id}] Semgrep failed: {e}"),
|
||||||
}
|
}
|
||||||
@@ -147,8 +154,13 @@ impl PipelineOrchestrator {
|
|||||||
// Stage 2: SBOM Generation
|
// Stage 2: SBOM Generation
|
||||||
tracing::info!("[{repo_id}] Stage 2: SBOM Generation");
|
tracing::info!("[{repo_id}] Stage 2: SBOM Generation");
|
||||||
self.update_phase(scan_run_id, "sbom_generation").await;
|
self.update_phase(scan_run_id, "sbom_generation").await;
|
||||||
let sbom_scanner = SbomScanner;
|
let mut sbom_entries = match async {
|
||||||
let mut sbom_entries = match sbom_scanner.scan(&repo_path, &repo_id).await {
|
let sbom_scanner = SbomScanner;
|
||||||
|
sbom_scanner.scan(&repo_path, &repo_id).await
|
||||||
|
}
|
||||||
|
.instrument(tracing::info_span!("stage_sbom_generation"))
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(output) => output.sbom_entries,
|
Ok(output) => output.sbom_entries,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("[{repo_id}] SBOM generation failed: {e}");
|
tracing::warn!("[{repo_id}] SBOM generation failed: {e}");
|
||||||
@@ -167,9 +179,13 @@ impl PipelineOrchestrator {
|
|||||||
k.expose_secret().to_string()
|
k.expose_secret().to_string()
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
let cve_alerts = match cve_scanner
|
let cve_alerts = match async {
|
||||||
.scan_dependencies(&repo_id, &mut sbom_entries)
|
cve_scanner
|
||||||
.await
|
.scan_dependencies(&repo_id, &mut sbom_entries)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.instrument(tracing::info_span!("stage_cve_scanning"))
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(alerts) => alerts,
|
Ok(alerts) => alerts,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -181,22 +197,36 @@ impl PipelineOrchestrator {
|
|||||||
// Stage 4: Pattern Scanning (GDPR + OAuth)
|
// Stage 4: Pattern Scanning (GDPR + OAuth)
|
||||||
tracing::info!("[{repo_id}] Stage 4: Pattern Scanning");
|
tracing::info!("[{repo_id}] Stage 4: Pattern Scanning");
|
||||||
self.update_phase(scan_run_id, "pattern_scanning").await;
|
self.update_phase(scan_run_id, "pattern_scanning").await;
|
||||||
let gdpr = GdprPatternScanner::new();
|
{
|
||||||
match gdpr.scan(&repo_path, &repo_id).await {
|
let pattern_findings = async {
|
||||||
Ok(output) => all_findings.extend(output.findings),
|
let mut findings = Vec::new();
|
||||||
Err(e) => tracing::warn!("[{repo_id}] GDPR pattern scan failed: {e}"),
|
let gdpr = GdprPatternScanner::new();
|
||||||
}
|
match gdpr.scan(&repo_path, &repo_id).await {
|
||||||
let oauth = OAuthPatternScanner::new();
|
Ok(output) => findings.extend(output.findings),
|
||||||
match oauth.scan(&repo_path, &repo_id).await {
|
Err(e) => tracing::warn!("[{repo_id}] GDPR pattern scan failed: {e}"),
|
||||||
Ok(output) => all_findings.extend(output.findings),
|
}
|
||||||
Err(e) => tracing::warn!("[{repo_id}] OAuth pattern scan failed: {e}"),
|
let oauth = OAuthPatternScanner::new();
|
||||||
|
match oauth.scan(&repo_path, &repo_id).await {
|
||||||
|
Ok(output) => findings.extend(output.findings),
|
||||||
|
Err(e) => tracing::warn!("[{repo_id}] OAuth pattern scan failed: {e}"),
|
||||||
|
}
|
||||||
|
findings
|
||||||
|
}
|
||||||
|
.instrument(tracing::info_span!("stage_pattern_scanning"))
|
||||||
|
.await;
|
||||||
|
all_findings.extend(pattern_findings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 4a: Secret Detection (Gitleaks)
|
// Stage 4a: Secret Detection (Gitleaks)
|
||||||
tracing::info!("[{repo_id}] Stage 4a: Secret Detection");
|
tracing::info!("[{repo_id}] Stage 4a: Secret Detection");
|
||||||
self.update_phase(scan_run_id, "secret_detection").await;
|
self.update_phase(scan_run_id, "secret_detection").await;
|
||||||
let gitleaks = GitleaksScanner;
|
match async {
|
||||||
match gitleaks.scan(&repo_path, &repo_id).await {
|
let gitleaks = GitleaksScanner;
|
||||||
|
gitleaks.scan(&repo_path, &repo_id).await
|
||||||
|
}
|
||||||
|
.instrument(tracing::info_span!("stage_secret_detection"))
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(output) => all_findings.extend(output.findings),
|
Ok(output) => all_findings.extend(output.findings),
|
||||||
Err(e) => tracing::warn!("[{repo_id}] Gitleaks failed: {e}"),
|
Err(e) => tracing::warn!("[{repo_id}] Gitleaks failed: {e}"),
|
||||||
}
|
}
|
||||||
@@ -204,8 +234,13 @@ impl PipelineOrchestrator {
|
|||||||
// Stage 4b: Lint Scanning
|
// Stage 4b: Lint Scanning
|
||||||
tracing::info!("[{repo_id}] Stage 4b: Lint Scanning");
|
tracing::info!("[{repo_id}] Stage 4b: Lint Scanning");
|
||||||
self.update_phase(scan_run_id, "lint_scanning").await;
|
self.update_phase(scan_run_id, "lint_scanning").await;
|
||||||
let lint = LintScanner;
|
match async {
|
||||||
match lint.scan(&repo_path, &repo_id).await {
|
let lint = LintScanner;
|
||||||
|
lint.scan(&repo_path, &repo_id).await
|
||||||
|
}
|
||||||
|
.instrument(tracing::info_span!("stage_lint_scanning"))
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(output) => all_findings.extend(output.findings),
|
Ok(output) => all_findings.extend(output.findings),
|
||||||
Err(e) => tracing::warn!("[{repo_id}] Lint scanning failed: {e}"),
|
Err(e) => tracing::warn!("[{repo_id}] Lint scanning failed: {e}"),
|
||||||
}
|
}
|
||||||
@@ -214,19 +249,26 @@ impl PipelineOrchestrator {
|
|||||||
if let Some(old_sha) = &repo.last_scanned_commit {
|
if let Some(old_sha) = &repo.last_scanned_commit {
|
||||||
tracing::info!("[{repo_id}] Stage 4c: LLM Code Review");
|
tracing::info!("[{repo_id}] Stage 4c: LLM Code Review");
|
||||||
self.update_phase(scan_run_id, "code_review").await;
|
self.update_phase(scan_run_id, "code_review").await;
|
||||||
let reviewer = CodeReviewScanner::new(self.llm.clone());
|
let review_output = async {
|
||||||
let review_output = reviewer
|
let reviewer = CodeReviewScanner::new(self.llm.clone());
|
||||||
.review_diff(&repo_path, &repo_id, old_sha, ¤t_sha)
|
reviewer
|
||||||
.await;
|
.review_diff(&repo_path, &repo_id, old_sha, ¤t_sha)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.instrument(tracing::info_span!("stage_code_review"))
|
||||||
|
.await;
|
||||||
all_findings.extend(review_output.findings);
|
all_findings.extend(review_output.findings);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage 4.5: Graph Building
|
// Stage 4.5: Graph Building
|
||||||
tracing::info!("[{repo_id}] Stage 4.5: Graph Building");
|
tracing::info!("[{repo_id}] Stage 4.5: Graph Building");
|
||||||
self.update_phase(scan_run_id, "graph_building").await;
|
self.update_phase(scan_run_id, "graph_building").await;
|
||||||
let graph_context = match self
|
let graph_context = match async {
|
||||||
.build_code_graph(&repo_path, &repo_id, &all_findings)
|
self.build_code_graph(&repo_path, &repo_id, &all_findings)
|
||||||
.await
|
.await
|
||||||
|
}
|
||||||
|
.instrument(tracing::info_span!("stage_graph_building"))
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
Ok(ctx) => Some(ctx),
|
Ok(ctx) => Some(ctx),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ impl Scanner for SbomScanner {
|
|||||||
|
|
||||||
/// Generate missing lock files so Syft can resolve the full dependency tree.
|
/// Generate missing lock files so Syft can resolve the full dependency tree.
|
||||||
/// This handles repos that gitignore their lock files (common for Rust libraries).
|
/// This handles repos that gitignore their lock files (common for Rust libraries).
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
async fn generate_lockfiles(repo_path: &Path) {
|
async fn generate_lockfiles(repo_path: &Path) {
|
||||||
// Cargo: generate Cargo.lock if Cargo.toml exists without it
|
// Cargo: generate Cargo.lock if Cargo.toml exists without it
|
||||||
if repo_path.join("Cargo.toml").exists() && !repo_path.join("Cargo.lock").exists() {
|
if repo_path.join("Cargo.toml").exists() && !repo_path.join("Cargo.lock").exists() {
|
||||||
@@ -122,6 +123,7 @@ async fn generate_lockfiles(repo_path: &Path) {
|
|||||||
|
|
||||||
/// Enrich Cargo SBOM entries with license info from `cargo metadata`.
|
/// Enrich Cargo SBOM entries with license info from `cargo metadata`.
|
||||||
/// Syft doesn't read license data from Cargo.lock, so we fill it in.
|
/// Syft doesn't read license data from Cargo.lock, so we fill it in.
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
async fn enrich_cargo_licenses(repo_path: &Path, entries: &mut [SbomEntry]) {
|
async fn enrich_cargo_licenses(repo_path: &Path, entries: &mut [SbomEntry]) {
|
||||||
if !repo_path.join("Cargo.toml").exists() {
|
if !repo_path.join("Cargo.toml").exists() {
|
||||||
return;
|
return;
|
||||||
@@ -182,6 +184,7 @@ async fn enrich_cargo_licenses(repo_path: &Path, entries: &mut [SbomEntry]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(repo_id = %repo_id))]
|
||||||
async fn run_syft(repo_path: &Path, repo_id: &str) -> Result<Vec<SbomEntry>, CoreError> {
|
async fn run_syft(repo_path: &Path, repo_id: &str) -> Result<Vec<SbomEntry>, CoreError> {
|
||||||
let output = tokio::process::Command::new("syft")
|
let output = tokio::process::Command::new("syft")
|
||||||
.arg(repo_path)
|
.arg(repo_path)
|
||||||
@@ -232,6 +235,7 @@ async fn run_syft(repo_path: &Path, repo_id: &str) -> Result<Vec<SbomEntry>, Cor
|
|||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
async fn run_cargo_audit(repo_path: &Path, _repo_id: &str) -> Result<Vec<AuditVuln>, CoreError> {
|
async fn run_cargo_audit(repo_path: &Path, _repo_id: &str) -> Result<Vec<AuditVuln>, CoreError> {
|
||||||
let cargo_lock = repo_path.join("Cargo.lock");
|
let cargo_lock = repo_path.join("Cargo.lock");
|
||||||
if !cargo_lock.exists() {
|
if !cargo_lock.exists() {
|
||||||
|
|||||||
Reference in New Issue
Block a user