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:
Sharang Parnerkar
2026-03-02 13:30:17 +01:00
commit 0867e401bc
97 changed files with 11750 additions and 0 deletions

View 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..." }
},
}
}

View 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..." }
},
}
}
}

View 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..." }
},
}
}
}

View 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;

View 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}"
}
}
}
}

View 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..." }
},
}
}
}

View 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..." }
},
}
}
}

View 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."
}
}
}
}