Some checks failed
CI / Deploy Agent (push) Has been skipped
CI / Deploy Dashboard (push) Has been skipped
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Format (push) Failing after 3s
CI / Clippy (push) Failing after 2m44s
CI / Security Audit (push) Has been skipped
CI / Tests (push) Has been skipped
CI / Format (pull_request) Failing after 3s
CI / Clippy (pull_request) Failing after 2m51s
CI / Security Audit (pull_request) Has been skipped
CI / Tests (pull_request) Has been skipped
CI / Detect Changes (push) Has been skipped
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped
- Add gitleaks secret detection, lint scanning (clippy/eslint/ruff), and LLM code review scanners - Enhance LLM triage with multi-action support (confirm/downgrade/upgrade/dismiss), surrounding code context, and file-path classification confidence adjustment - Add text search, column sorting, and bulk status update to findings dashboard - Fix finding detail page status refresh and add developer feedback field - Fix BSON DateTime deserialization across all models with shared serde helpers - Add scan progress spinner with polling to repositories page - Batch OSV.dev queries to avoid "Too many queries" errors - Add gitleaks, semgrep, and ruff to Dockerfile.agent for deployment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
158 lines
6.5 KiB
Rust
158 lines
6.5 KiB
Rust
use dioxus::prelude::*;
|
|
|
|
use crate::components::code_snippet::CodeSnippet;
|
|
use crate::components::page_header::PageHeader;
|
|
use crate::components::severity_badge::SeverityBadge;
|
|
|
|
#[component]
|
|
pub fn FindingDetailPage(id: String) -> Element {
|
|
let finding_id = id.clone();
|
|
|
|
let mut finding = use_resource(move || {
|
|
let fid = finding_id.clone();
|
|
async move {
|
|
crate::infrastructure::findings::fetch_finding_detail(fid)
|
|
.await
|
|
.ok()
|
|
}
|
|
});
|
|
|
|
let snapshot = finding.read().clone();
|
|
|
|
match snapshot {
|
|
Some(Some(f)) => {
|
|
let finding_id_for_status = id.clone();
|
|
let finding_id_for_feedback = id.clone();
|
|
let existing_feedback = f.developer_feedback.clone().unwrap_or_default();
|
|
rsx! {
|
|
PageHeader {
|
|
title: f.title.clone(),
|
|
description: format!("{} | {} | {}", f.scanner, f.scan_type, f.status),
|
|
}
|
|
|
|
div { class: "flex gap-2 mb-4",
|
|
SeverityBadge { severity: f.severity.to_string() }
|
|
if let Some(cwe) = &f.cwe {
|
|
span { class: "badge badge-info", "{cwe}" }
|
|
}
|
|
if let Some(cve) = &f.cve {
|
|
span { class: "badge badge-high", "{cve}" }
|
|
}
|
|
if let Some(score) = f.cvss_score {
|
|
span { class: "badge badge-medium", "CVSS: {score}" }
|
|
}
|
|
if let Some(confidence) = f.confidence {
|
|
span { class: "badge badge-info", "Confidence: {confidence:.1}" }
|
|
}
|
|
}
|
|
|
|
div { class: "card",
|
|
div { class: "card-header", "Description" }
|
|
p { "{f.description}" }
|
|
}
|
|
|
|
if let Some(rationale) = &f.triage_rationale {
|
|
div { class: "card",
|
|
div { class: "card-header", "Triage Rationale" }
|
|
div {
|
|
style: "display: flex; align-items: center; gap: 8px; margin-bottom: 8px;",
|
|
if let Some(action) = &f.triage_action {
|
|
span { class: "badge badge-info", "{action}" }
|
|
}
|
|
}
|
|
p { style: "color: var(--text-secondary); font-size: 14px;", "{rationale}" }
|
|
}
|
|
}
|
|
|
|
if let Some(code) = &f.code_snippet {
|
|
div { class: "card",
|
|
div { class: "card-header", "Code Evidence" }
|
|
CodeSnippet {
|
|
code: code.clone(),
|
|
file_path: f.file_path.clone().unwrap_or_default(),
|
|
line_number: f.line_number.unwrap_or(0),
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(remediation) = &f.remediation {
|
|
div { class: "card",
|
|
div { class: "card-header", "Remediation" }
|
|
p { "{remediation}" }
|
|
}
|
|
}
|
|
|
|
if let Some(fix) = &f.suggested_fix {
|
|
div { class: "card",
|
|
div { class: "card-header", "Suggested Fix" }
|
|
CodeSnippet { code: fix.clone() }
|
|
}
|
|
}
|
|
|
|
if let Some(url) = &f.tracker_issue_url {
|
|
div { class: "card",
|
|
div { class: "card-header", "Linked Issue" }
|
|
a {
|
|
href: "{url}",
|
|
target: "_blank",
|
|
style: "color: var(--accent);",
|
|
"{url}"
|
|
}
|
|
}
|
|
}
|
|
|
|
div { class: "card",
|
|
div { class: "card-header", "Update Status" }
|
|
div { class: "flex gap-2",
|
|
for status in ["open", "triaged", "resolved", "false_positive", "ignored"] {
|
|
{
|
|
let status_str = status.to_string();
|
|
let id_clone = finding_id_for_status.clone();
|
|
rsx! {
|
|
button {
|
|
class: "btn btn-ghost",
|
|
onclick: move |_| {
|
|
let s = status_str.clone();
|
|
let id = id_clone.clone();
|
|
spawn(async move {
|
|
let _ = crate::infrastructure::findings::update_finding_status(id, s).await;
|
|
});
|
|
finding.restart();
|
|
},
|
|
"{status}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
div { class: "card",
|
|
div { class: "card-header", "Developer Feedback" }
|
|
p {
|
|
style: "font-size: 13px; color: var(--text-secondary); margin-bottom: 8px;",
|
|
"Share your assessment of this finding (e.g. false positive, actionable, needs context)"
|
|
}
|
|
textarea {
|
|
style: "width: 100%; min-height: 80px; background: var(--bg-primary); border: 1px solid var(--border); border-radius: 8px; padding: 10px 14px; color: var(--text-primary); font-size: 14px; resize: vertical;",
|
|
value: "{existing_feedback}",
|
|
oninput: move |e| {
|
|
let feedback = e.value();
|
|
let id = finding_id_for_feedback.clone();
|
|
spawn(async move {
|
|
let _ = crate::infrastructure::findings::update_finding_feedback(id, feedback).await;
|
|
});
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Some(None) => rsx! {
|
|
div { class: "card", p { "Finding not found." } }
|
|
},
|
|
None => rsx! {
|
|
div { class: "loading", "Loading finding..." }
|
|
},
|
|
}
|
|
}
|