Initial commit: Compliance Scanner Agent
Autonomous security and compliance scanning agent for git repositories. Features: SAST (Semgrep), SBOM (Syft), CVE monitoring (OSV.dev/NVD), GDPR/OAuth pattern detection, LLM triage, issue creation (GitHub/GitLab/Jira), PR reviews, and Dioxus fullstack dashboard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
117
compliance-dashboard/src/pages/finding_detail.rs
Normal file
117
compliance-dashboard/src/pages/finding_detail.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
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 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();
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: f.title.clone(),
|
||||
description: format!("{} | {} | {}", f.scanner, f.scan_type, f.status),
|
||||
}
|
||||
|
||||
div { style: "display: flex; gap: 8px; margin-bottom: 16px;",
|
||||
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}" }
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
div { class: "card-header", "Description" }
|
||||
p { style: "line-height: 1.6;", "{f.description}" }
|
||||
}
|
||||
|
||||
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 { style: "line-height: 1.6;", "{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 { style: "display: flex; gap: 8px;",
|
||||
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;
|
||||
});
|
||||
},
|
||||
"{status}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! {
|
||||
div { class: "card", p { "Finding not found." } }
|
||||
},
|
||||
None => rsx! {
|
||||
div { class: "loading", "Loading finding..." }
|
||||
},
|
||||
}
|
||||
}
|
||||
124
compliance-dashboard/src/pages/findings.rs
Normal file
124
compliance-dashboard/src/pages/findings.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::app::Route;
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::components::pagination::Pagination;
|
||||
use crate::components::severity_badge::SeverityBadge;
|
||||
|
||||
#[component]
|
||||
pub fn FindingsPage() -> Element {
|
||||
let mut page = use_signal(|| 1u64);
|
||||
let mut severity_filter = use_signal(String::new);
|
||||
let mut type_filter = use_signal(String::new);
|
||||
let mut status_filter = use_signal(String::new);
|
||||
|
||||
let findings = use_resource(move || {
|
||||
let p = page();
|
||||
let sev = severity_filter();
|
||||
let typ = type_filter();
|
||||
let stat = status_filter();
|
||||
async move {
|
||||
crate::infrastructure::findings::fetch_findings(p, sev, typ, stat, String::new()).await.ok()
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "Findings",
|
||||
description: "Security and compliance findings across all repositories",
|
||||
}
|
||||
|
||||
div { class: "filter-bar",
|
||||
select {
|
||||
onchange: move |e| { severity_filter.set(e.value()); page.set(1); },
|
||||
option { value: "", "All Severities" }
|
||||
option { value: "critical", "Critical" }
|
||||
option { value: "high", "High" }
|
||||
option { value: "medium", "Medium" }
|
||||
option { value: "low", "Low" }
|
||||
option { value: "info", "Info" }
|
||||
}
|
||||
select {
|
||||
onchange: move |e| { type_filter.set(e.value()); page.set(1); },
|
||||
option { value: "", "All Types" }
|
||||
option { value: "sast", "SAST" }
|
||||
option { value: "sbom", "SBOM" }
|
||||
option { value: "cve", "CVE" }
|
||||
option { value: "gdpr", "GDPR" }
|
||||
option { value: "oauth", "OAuth" }
|
||||
}
|
||||
select {
|
||||
onchange: move |e| { status_filter.set(e.value()); page.set(1); },
|
||||
option { value: "", "All Statuses" }
|
||||
option { value: "open", "Open" }
|
||||
option { value: "triaged", "Triaged" }
|
||||
option { value: "resolved", "Resolved" }
|
||||
option { value: "false_positive", "False Positive" }
|
||||
option { value: "ignored", "Ignored" }
|
||||
}
|
||||
}
|
||||
|
||||
match &*findings.read() {
|
||||
Some(Some(resp)) => {
|
||||
let total_pages = resp.total.unwrap_or(0).div_ceil(20).max(1);
|
||||
rsx! {
|
||||
div { class: "card",
|
||||
div { class: "table-wrapper",
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th { "Severity" }
|
||||
th { "Title" }
|
||||
th { "Type" }
|
||||
th { "Scanner" }
|
||||
th { "File" }
|
||||
th { "Status" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for finding in &resp.data {
|
||||
{
|
||||
let id = finding.id.as_ref().map(|id| id.to_hex()).unwrap_or_default();
|
||||
rsx! {
|
||||
tr {
|
||||
td { SeverityBadge { severity: finding.severity.to_string() } }
|
||||
td {
|
||||
Link {
|
||||
to: Route::FindingDetailPage { id: id },
|
||||
style: "color: var(--accent); text-decoration: none;",
|
||||
"{finding.title}"
|
||||
}
|
||||
}
|
||||
td { "{finding.scan_type}" }
|
||||
td { "{finding.scanner}" }
|
||||
td {
|
||||
style: "font-family: monospace; font-size: 12px;",
|
||||
"{finding.file_path.as_deref().unwrap_or(\"-\")}"
|
||||
}
|
||||
td {
|
||||
span { class: "badge badge-info", "{finding.status}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Pagination {
|
||||
current_page: page(),
|
||||
total_pages: total_pages,
|
||||
on_page_change: move |p| page.set(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! {
|
||||
div { class: "card", p { "Failed to load findings." } }
|
||||
},
|
||||
None => rsx! {
|
||||
div { class: "loading", "Loading findings..." }
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
87
compliance-dashboard/src/pages/issues.rs
Normal file
87
compliance-dashboard/src/pages/issues.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::components::pagination::Pagination;
|
||||
|
||||
#[component]
|
||||
pub fn IssuesPage() -> Element {
|
||||
let mut page = use_signal(|| 1u64);
|
||||
|
||||
let issues = use_resource(move || {
|
||||
let p = page();
|
||||
async move {
|
||||
crate::infrastructure::issues::fetch_issues(p).await.ok()
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "Issues",
|
||||
description: "Cross-tracker issue view - GitHub, GitLab, and Jira",
|
||||
}
|
||||
|
||||
match &*issues.read() {
|
||||
Some(Some(resp)) => {
|
||||
let total_pages = resp.total.unwrap_or(0).div_ceil(20).max(1);
|
||||
rsx! {
|
||||
div { class: "card",
|
||||
div { class: "table-wrapper",
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th { "Tracker" }
|
||||
th { "ID" }
|
||||
th { "Title" }
|
||||
th { "Status" }
|
||||
th { "Created" }
|
||||
th { "Link" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for issue in &resp.data {
|
||||
tr {
|
||||
td {
|
||||
span { class: "badge badge-info", "{issue.tracker_type}" }
|
||||
}
|
||||
td {
|
||||
style: "font-family: monospace;",
|
||||
"{issue.external_id}"
|
||||
}
|
||||
td { "{issue.title}" }
|
||||
td {
|
||||
span { class: "badge badge-info", "{issue.status}" }
|
||||
}
|
||||
td {
|
||||
style: "font-size: 12px; color: var(--text-secondary);",
|
||||
{issue.created_at.format("%Y-%m-%d %H:%M").to_string()}
|
||||
}
|
||||
td {
|
||||
a {
|
||||
href: "{issue.external_url}",
|
||||
target: "_blank",
|
||||
style: "color: var(--accent); text-decoration: none;",
|
||||
"Open"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Pagination {
|
||||
current_page: page(),
|
||||
total_pages: total_pages,
|
||||
on_page_change: move |p| page.set(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! {
|
||||
div { class: "card", p { "Failed to load issues." } }
|
||||
},
|
||||
None => rsx! {
|
||||
div { class: "loading", "Loading issues..." }
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
15
compliance-dashboard/src/pages/mod.rs
Normal file
15
compliance-dashboard/src/pages/mod.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
pub mod finding_detail;
|
||||
pub mod findings;
|
||||
pub mod issues;
|
||||
pub mod overview;
|
||||
pub mod repositories;
|
||||
pub mod sbom;
|
||||
pub mod settings;
|
||||
|
||||
pub use finding_detail::FindingDetailPage;
|
||||
pub use findings::FindingsPage;
|
||||
pub use issues::IssuesPage;
|
||||
pub use overview::OverviewPage;
|
||||
pub use repositories::RepositoriesPage;
|
||||
pub use sbom::SbomPage;
|
||||
pub use settings::SettingsPage;
|
||||
104
compliance-dashboard/src/pages/overview.rs
Normal file
104
compliance-dashboard/src/pages/overview.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::components::stat_card::StatCard;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use crate::infrastructure::stats::fetch_overview_stats;
|
||||
|
||||
#[component]
|
||||
pub fn OverviewPage() -> Element {
|
||||
let stats = use_resource(move || async move {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
fetch_overview_stats().await.ok()
|
||||
}
|
||||
#[cfg(not(feature = "server"))]
|
||||
{
|
||||
crate::infrastructure::stats::fetch_overview_stats().await.ok()
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "Overview",
|
||||
description: "Security and compliance scanning dashboard",
|
||||
}
|
||||
|
||||
match &*stats.read() {
|
||||
Some(Some(s)) => rsx! {
|
||||
div { class: "stat-cards",
|
||||
StatCard { label: "Repositories", value: s.total_repositories.to_string() }
|
||||
StatCard { label: "Total Findings", value: s.total_findings.to_string() }
|
||||
StatCard {
|
||||
label: "Critical",
|
||||
value: s.critical_findings.to_string(),
|
||||
color: "var(--danger)",
|
||||
}
|
||||
StatCard {
|
||||
label: "High",
|
||||
value: s.high_findings.to_string(),
|
||||
color: "#f97316",
|
||||
}
|
||||
StatCard {
|
||||
label: "Medium",
|
||||
value: s.medium_findings.to_string(),
|
||||
color: "var(--warning)",
|
||||
}
|
||||
StatCard {
|
||||
label: "Low",
|
||||
value: s.low_findings.to_string(),
|
||||
color: "var(--success)",
|
||||
}
|
||||
StatCard { label: "Dependencies", value: s.total_sbom_entries.to_string() }
|
||||
StatCard { label: "CVE Alerts", value: s.total_cve_alerts.to_string() }
|
||||
StatCard { label: "Tracker Issues", value: s.total_issues.to_string() }
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
div { class: "card-header", "Severity Distribution" }
|
||||
div {
|
||||
style: "display: flex; gap: 8px; align-items: flex-end; height: 200px; padding: 16px;",
|
||||
SeverityBar { label: "Critical", count: s.critical_findings, max: s.total_findings, color: "var(--danger)" }
|
||||
SeverityBar { label: "High", count: s.high_findings, max: s.total_findings, color: "#f97316" }
|
||||
SeverityBar { label: "Medium", count: s.medium_findings, max: s.total_findings, color: "var(--warning)" }
|
||||
SeverityBar { label: "Low", count: s.low_findings, max: s.total_findings, color: "var(--success)" }
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! {
|
||||
div { class: "card",
|
||||
p { style: "color: var(--text-secondary);",
|
||||
"Unable to load stats. Make sure the agent API is running."
|
||||
}
|
||||
}
|
||||
},
|
||||
None => rsx! {
|
||||
div { class: "loading", "Loading overview..." }
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn SeverityBar(label: String, count: u64, max: u64, color: String) -> Element {
|
||||
let height_pct = if max > 0 { (count as f64 / max as f64) * 100.0 } else { 0.0 };
|
||||
let height = format!("{}%", height_pct.max(2.0));
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
style: "flex: 1; display: flex; flex-direction: column; align-items: center; gap: 4px;",
|
||||
div {
|
||||
style: "font-size: 14px; font-weight: 600;",
|
||||
"{count}"
|
||||
}
|
||||
div {
|
||||
style: "width: 100%; background: {color}; border-radius: 4px 4px 0 0; height: {height}; min-height: 4px; transition: height 0.3s;",
|
||||
}
|
||||
div {
|
||||
style: "font-size: 11px; color: var(--text-secondary);",
|
||||
"{label}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
155
compliance-dashboard/src/pages/repositories.rs
Normal file
155
compliance-dashboard/src/pages/repositories.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::components::pagination::Pagination;
|
||||
|
||||
#[component]
|
||||
pub fn RepositoriesPage() -> Element {
|
||||
let mut page = use_signal(|| 1u64);
|
||||
let mut show_add_form = use_signal(|| false);
|
||||
let mut name = use_signal(String::new);
|
||||
let mut git_url = use_signal(String::new);
|
||||
let mut branch = use_signal(|| "main".to_string());
|
||||
|
||||
let repos = use_resource(move || {
|
||||
let p = page();
|
||||
async move {
|
||||
crate::infrastructure::repositories::fetch_repositories(p).await.ok()
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "Repositories",
|
||||
description: "Tracked git repositories",
|
||||
}
|
||||
|
||||
div { style: "margin-bottom: 16px;",
|
||||
button {
|
||||
class: "btn btn-primary",
|
||||
onclick: move |_| show_add_form.toggle(),
|
||||
if show_add_form() { "Cancel" } else { "+ Add Repository" }
|
||||
}
|
||||
}
|
||||
|
||||
if show_add_form() {
|
||||
div { class: "card",
|
||||
div { class: "card-header", "Add Repository" }
|
||||
div { class: "form-group",
|
||||
label { "Name" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "my-project",
|
||||
value: "{name}",
|
||||
oninput: move |e| name.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "form-group",
|
||||
label { "Git URL" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "https://github.com/org/repo.git",
|
||||
value: "{git_url}",
|
||||
oninput: move |e| git_url.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "form-group",
|
||||
label { "Default Branch" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "main",
|
||||
value: "{branch}",
|
||||
oninput: move |e| branch.set(e.value()),
|
||||
}
|
||||
}
|
||||
button {
|
||||
class: "btn btn-primary",
|
||||
onclick: move |_| {
|
||||
let n = name();
|
||||
let u = git_url();
|
||||
let b = branch();
|
||||
spawn(async move {
|
||||
let _ = crate::infrastructure::repositories::add_repository(n, u, b).await;
|
||||
});
|
||||
show_add_form.set(false);
|
||||
name.set(String::new());
|
||||
git_url.set(String::new());
|
||||
},
|
||||
"Add"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match &*repos.read() {
|
||||
Some(Some(resp)) => {
|
||||
let total_pages = resp.total.unwrap_or(0).div_ceil(20).max(1);
|
||||
rsx! {
|
||||
div { class: "card",
|
||||
div { class: "table-wrapper",
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th { "Name" }
|
||||
th { "Git URL" }
|
||||
th { "Branch" }
|
||||
th { "Findings" }
|
||||
th { "Last Scanned" }
|
||||
th { "Actions" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for repo in &resp.data {
|
||||
{
|
||||
let repo_id = repo.id.as_ref().map(|id| id.to_hex()).unwrap_or_default();
|
||||
let repo_id_clone = repo_id.clone();
|
||||
rsx! {
|
||||
tr {
|
||||
td { "{repo.name}" }
|
||||
td {
|
||||
style: "font-size: 12px; font-family: monospace;",
|
||||
"{repo.git_url}"
|
||||
}
|
||||
td { "{repo.default_branch}" }
|
||||
td { "{repo.findings_count}" }
|
||||
td {
|
||||
match &repo.last_scanned_commit {
|
||||
Some(sha) => rsx! { span { style: "font-family: monospace; font-size: 12px;", "{&sha[..7.min(sha.len())]}" } },
|
||||
None => rsx! { span { style: "color: var(--text-secondary);", "Never" } },
|
||||
}
|
||||
}
|
||||
td {
|
||||
button {
|
||||
class: "btn btn-ghost",
|
||||
onclick: move |_| {
|
||||
let id = repo_id_clone.clone();
|
||||
spawn(async move {
|
||||
let _ = crate::infrastructure::repositories::trigger_repo_scan(id).await;
|
||||
});
|
||||
},
|
||||
"Scan"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Pagination {
|
||||
current_page: page(),
|
||||
total_pages: total_pages,
|
||||
on_page_change: move |p| page.set(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! {
|
||||
div { class: "card", p { "Failed to load repositories." } }
|
||||
},
|
||||
None => rsx! {
|
||||
div { class: "loading", "Loading repositories..." }
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
85
compliance-dashboard/src/pages/sbom.rs
Normal file
85
compliance-dashboard/src/pages/sbom.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::page_header::PageHeader;
|
||||
use crate::components::pagination::Pagination;
|
||||
|
||||
#[component]
|
||||
pub fn SbomPage() -> Element {
|
||||
let mut page = use_signal(|| 1u64);
|
||||
|
||||
let sbom = use_resource(move || {
|
||||
let p = page();
|
||||
async move {
|
||||
crate::infrastructure::sbom::fetch_sbom(p).await.ok()
|
||||
}
|
||||
});
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "SBOM",
|
||||
description: "Software Bill of Materials - dependency inventory across all repositories",
|
||||
}
|
||||
|
||||
match &*sbom.read() {
|
||||
Some(Some(resp)) => {
|
||||
let total_pages = resp.total.unwrap_or(0).div_ceil(50).max(1);
|
||||
rsx! {
|
||||
div { class: "card",
|
||||
div { class: "table-wrapper",
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th { "Package" }
|
||||
th { "Version" }
|
||||
th { "Manager" }
|
||||
th { "License" }
|
||||
th { "Vulnerabilities" }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for entry in &resp.data {
|
||||
tr {
|
||||
td {
|
||||
style: "font-weight: 500;",
|
||||
"{entry.name}"
|
||||
}
|
||||
td {
|
||||
style: "font-family: monospace; font-size: 13px;",
|
||||
"{entry.version}"
|
||||
}
|
||||
td { "{entry.package_manager}" }
|
||||
td { "{entry.license.as_deref().unwrap_or(\"-\")}" }
|
||||
td {
|
||||
if entry.known_vulnerabilities.is_empty() {
|
||||
span {
|
||||
style: "color: var(--success);",
|
||||
"None"
|
||||
}
|
||||
} else {
|
||||
span { class: "badge badge-high",
|
||||
"{entry.known_vulnerabilities.len()} vuln(s)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Pagination {
|
||||
current_page: page(),
|
||||
total_pages: total_pages,
|
||||
on_page_change: move |p| page.set(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(None) => rsx! {
|
||||
div { class: "card", p { "Failed to load SBOM." } }
|
||||
},
|
||||
None => rsx! {
|
||||
div { class: "loading", "Loading SBOM..." }
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
142
compliance-dashboard/src/pages/settings.rs
Normal file
142
compliance-dashboard/src/pages/settings.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use crate::components::page_header::PageHeader;
|
||||
|
||||
#[component]
|
||||
pub fn SettingsPage() -> Element {
|
||||
let mut litellm_url = use_signal(|| "http://localhost:4000".to_string());
|
||||
let mut litellm_model = use_signal(|| "gpt-4o".to_string());
|
||||
let mut github_token = use_signal(String::new);
|
||||
let mut gitlab_url = use_signal(|| "https://gitlab.com".to_string());
|
||||
let mut gitlab_token = use_signal(String::new);
|
||||
let mut jira_url = use_signal(String::new);
|
||||
let mut jira_email = use_signal(String::new);
|
||||
let mut jira_token = use_signal(String::new);
|
||||
let mut jira_project = use_signal(String::new);
|
||||
let mut searxng_url = use_signal(|| "http://localhost:8888".to_string());
|
||||
|
||||
rsx! {
|
||||
PageHeader {
|
||||
title: "Settings",
|
||||
description: "Configure integrations and scanning parameters",
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
div { class: "card-header", "LiteLLM Configuration" }
|
||||
div { class: "form-group",
|
||||
label { "LiteLLM URL" }
|
||||
input {
|
||||
r#type: "text",
|
||||
value: "{litellm_url}",
|
||||
oninput: move |e| litellm_url.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "form-group",
|
||||
label { "Model" }
|
||||
input {
|
||||
r#type: "text",
|
||||
value: "{litellm_model}",
|
||||
oninput: move |e| litellm_model.set(e.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
div { class: "card-header", "GitHub Integration" }
|
||||
div { class: "form-group",
|
||||
label { "Personal Access Token" }
|
||||
input {
|
||||
r#type: "password",
|
||||
placeholder: "ghp_...",
|
||||
value: "{github_token}",
|
||||
oninput: move |e| github_token.set(e.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
div { class: "card-header", "GitLab Integration" }
|
||||
div { class: "form-group",
|
||||
label { "GitLab URL" }
|
||||
input {
|
||||
r#type: "text",
|
||||
value: "{gitlab_url}",
|
||||
oninput: move |e| gitlab_url.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "form-group",
|
||||
label { "Access Token" }
|
||||
input {
|
||||
r#type: "password",
|
||||
placeholder: "glpat-...",
|
||||
value: "{gitlab_token}",
|
||||
oninput: move |e| gitlab_token.set(e.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
div { class: "card-header", "Jira Integration" }
|
||||
div { class: "form-group",
|
||||
label { "Jira URL" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "https://your-org.atlassian.net",
|
||||
value: "{jira_url}",
|
||||
oninput: move |e| jira_url.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "form-group",
|
||||
label { "Email" }
|
||||
input {
|
||||
r#type: "email",
|
||||
value: "{jira_email}",
|
||||
oninput: move |e| jira_email.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "form-group",
|
||||
label { "API Token" }
|
||||
input {
|
||||
r#type: "password",
|
||||
value: "{jira_token}",
|
||||
oninput: move |e| jira_token.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "form-group",
|
||||
label { "Project Key" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "SEC",
|
||||
value: "{jira_project}",
|
||||
oninput: move |e| jira_project.set(e.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { class: "card",
|
||||
div { class: "card-header", "SearXNG" }
|
||||
div { class: "form-group",
|
||||
label { "SearXNG URL" }
|
||||
input {
|
||||
r#type: "text",
|
||||
value: "{searxng_url}",
|
||||
oninput: move |e| searxng_url.set(e.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div { style: "margin-top: 16px;",
|
||||
button {
|
||||
class: "btn btn-primary",
|
||||
onclick: move |_| {
|
||||
tracing::info!("Settings save not yet implemented - settings are managed via .env");
|
||||
},
|
||||
"Save Settings"
|
||||
}
|
||||
p {
|
||||
style: "margin-top: 8px; font-size: 12px; color: var(--text-secondary);",
|
||||
"Note: Settings are currently configured via environment variables (.env file). Dashboard-based settings persistence coming soon."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user