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:
18
compliance-dashboard/src/infrastructure/config.rs
Normal file
18
compliance-dashboard/src/infrastructure/config.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use compliance_core::DashboardConfig;
|
||||
|
||||
use super::error::DashboardError;
|
||||
|
||||
pub fn load_config() -> Result<DashboardConfig, DashboardError> {
|
||||
Ok(DashboardConfig {
|
||||
mongodb_uri: std::env::var("MONGODB_URI")
|
||||
.map_err(|_| DashboardError::Config("Missing MONGODB_URI".to_string()))?,
|
||||
mongodb_database: std::env::var("MONGODB_DATABASE")
|
||||
.unwrap_or_else(|_| "compliance_scanner".to_string()),
|
||||
agent_api_url: std::env::var("AGENT_API_URL")
|
||||
.unwrap_or_else(|_| "http://localhost:3001".to_string()),
|
||||
dashboard_port: std::env::var("DASHBOARD_PORT")
|
||||
.ok()
|
||||
.and_then(|p| p.parse().ok())
|
||||
.unwrap_or(8080),
|
||||
})
|
||||
}
|
||||
45
compliance-dashboard/src/infrastructure/database.rs
Normal file
45
compliance-dashboard/src/infrastructure/database.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use mongodb::bson::doc;
|
||||
use mongodb::{Client, Collection};
|
||||
|
||||
use compliance_core::models::*;
|
||||
|
||||
use super::error::DashboardError;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Database {
|
||||
inner: mongodb::Database,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn connect(uri: &str, db_name: &str) -> Result<Self, DashboardError> {
|
||||
let client = Client::with_uri_str(uri).await?;
|
||||
let db = client.database(db_name);
|
||||
db.run_command(doc! { "ping": 1 }).await?;
|
||||
tracing::info!("Dashboard connected to MongoDB '{db_name}'");
|
||||
Ok(Self { inner: db })
|
||||
}
|
||||
|
||||
pub fn repositories(&self) -> Collection<TrackedRepository> {
|
||||
self.inner.collection("repositories")
|
||||
}
|
||||
|
||||
pub fn findings(&self) -> Collection<Finding> {
|
||||
self.inner.collection("findings")
|
||||
}
|
||||
|
||||
pub fn scan_runs(&self) -> Collection<ScanRun> {
|
||||
self.inner.collection("scan_runs")
|
||||
}
|
||||
|
||||
pub fn sbom_entries(&self) -> Collection<SbomEntry> {
|
||||
self.inner.collection("sbom_entries")
|
||||
}
|
||||
|
||||
pub fn cve_alerts(&self) -> Collection<CveAlert> {
|
||||
self.inner.collection("cve_alerts")
|
||||
}
|
||||
|
||||
pub fn tracker_issues(&self) -> Collection<TrackerIssue> {
|
||||
self.inner.collection("tracker_issues")
|
||||
}
|
||||
}
|
||||
26
compliance-dashboard/src/infrastructure/error.rs
Normal file
26
compliance-dashboard/src/infrastructure/error.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use dioxus::prelude::*;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DashboardError {
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] mongodb::error::Error),
|
||||
|
||||
#[error("HTTP error: {0}")]
|
||||
Http(#[from] reqwest::Error),
|
||||
|
||||
#[error("JSON error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(String),
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<DashboardError> for ServerFnError {
|
||||
fn from(err: DashboardError) -> Self {
|
||||
ServerFnError::new(err.to_string())
|
||||
}
|
||||
}
|
||||
71
compliance-dashboard/src/infrastructure/findings.rs
Normal file
71
compliance-dashboard/src/infrastructure/findings.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use compliance_core::models::Finding;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct FindingsListResponse {
|
||||
pub data: Vec<Finding>,
|
||||
pub total: Option<u64>,
|
||||
pub page: Option<u64>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_findings(
|
||||
page: u64,
|
||||
severity: String,
|
||||
scan_type: String,
|
||||
status: String,
|
||||
repo_id: String,
|
||||
) -> Result<FindingsListResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
|
||||
let mut url = format!("{}/api/v1/findings?page={page}&limit=20", state.agent_api_url);
|
||||
if !severity.is_empty() {
|
||||
url.push_str(&format!("&severity={severity}"));
|
||||
}
|
||||
if !scan_type.is_empty() {
|
||||
url.push_str(&format!("&scan_type={scan_type}"));
|
||||
}
|
||||
if !status.is_empty() {
|
||||
url.push_str(&format!("&status={status}"));
|
||||
}
|
||||
if !repo_id.is_empty() {
|
||||
url.push_str(&format!("&repo_id={repo_id}"));
|
||||
}
|
||||
|
||||
let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: FindingsListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_finding_detail(id: String) -> Result<Finding, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/findings/{id}", state.agent_api_url);
|
||||
|
||||
let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: serde_json::Value = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let finding: Finding = serde_json::from_value(body["data"].clone())
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(finding)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn update_finding_status(id: String, status: String) -> Result<(), ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/findings/{id}/status", state.agent_api_url);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.patch(&url)
|
||||
.json(&serde_json::json!({ "status": status }))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
22
compliance-dashboard/src/infrastructure/issues.rs
Normal file
22
compliance-dashboard/src/infrastructure/issues.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use compliance_core::models::TrackerIssue;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct IssuesListResponse {
|
||||
pub data: Vec<TrackerIssue>,
|
||||
pub total: Option<u64>,
|
||||
pub page: Option<u64>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_issues(page: u64) -> Result<IssuesListResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/issues?page={page}&limit=20", state.agent_api_url);
|
||||
|
||||
let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: IssuesListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
13
compliance-dashboard/src/infrastructure/mod.rs
Normal file
13
compliance-dashboard/src/infrastructure/mod.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
pub mod config;
|
||||
pub mod database;
|
||||
pub mod error;
|
||||
pub mod findings;
|
||||
pub mod issues;
|
||||
pub mod repositories;
|
||||
pub mod sbom;
|
||||
pub mod scans;
|
||||
pub mod server;
|
||||
pub mod server_state;
|
||||
pub mod stats;
|
||||
|
||||
pub use server::server_start;
|
||||
64
compliance-dashboard/src/infrastructure/repositories.rs
Normal file
64
compliance-dashboard/src/infrastructure/repositories.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use compliance_core::models::TrackedRepository;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct RepositoryListResponse {
|
||||
pub data: Vec<TrackedRepository>,
|
||||
pub total: Option<u64>,
|
||||
pub page: Option<u64>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_repositories(page: u64) -> Result<RepositoryListResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/repositories?page={page}&limit=20", state.agent_api_url);
|
||||
|
||||
let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: RepositoryListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn add_repository(name: String, git_url: String, default_branch: String) -> Result<(), ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/repositories", state.agent_api_url);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client
|
||||
.post(&url)
|
||||
.json(&serde_json::json!({
|
||||
"name": name,
|
||||
"git_url": git_url,
|
||||
"default_branch": default_branch,
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(ServerFnError::new(format!("Failed to add repository: {body}")));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn trigger_repo_scan(repo_id: String) -> Result<(), ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/repositories/{repo_id}/scan", state.agent_api_url);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
client
|
||||
.post(&url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
22
compliance-dashboard/src/infrastructure/sbom.rs
Normal file
22
compliance-dashboard/src/infrastructure/sbom.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use compliance_core::models::SbomEntry;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct SbomListResponse {
|
||||
pub data: Vec<SbomEntry>,
|
||||
pub total: Option<u64>,
|
||||
pub page: Option<u64>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_sbom(page: u64) -> Result<SbomListResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/sbom?page={page}&limit=50", state.agent_api_url);
|
||||
|
||||
let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: SbomListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
22
compliance-dashboard/src/infrastructure/scans.rs
Normal file
22
compliance-dashboard/src/infrastructure/scans.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use compliance_core::models::ScanRun;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ScansListResponse {
|
||||
pub data: Vec<ScanRun>,
|
||||
pub total: Option<u64>,
|
||||
pub page: Option<u64>,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_scan_runs(page: u64) -> Result<ScansListResponse, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/scan-runs?page={page}&limit=20", state.agent_api_url);
|
||||
|
||||
let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: ScansListResponse = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
Ok(body)
|
||||
}
|
||||
41
compliance-dashboard/src/infrastructure/server.rs
Normal file
41
compliance-dashboard/src/infrastructure/server.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use dioxus::prelude::*;
|
||||
|
||||
use super::config;
|
||||
use super::database::Database;
|
||||
use super::error::DashboardError;
|
||||
use super::server_state::{ServerState, ServerStateInner};
|
||||
|
||||
pub fn server_start(app: fn() -> Element) -> Result<(), DashboardError> {
|
||||
tokio::runtime::Runtime::new()
|
||||
.map_err(|e| DashboardError::Other(e.to_string()))?
|
||||
.block_on(async move {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
let config = config::load_config()?;
|
||||
let db = Database::connect(&config.mongodb_uri, &config.mongodb_database).await?;
|
||||
|
||||
let server_state: ServerState = ServerStateInner {
|
||||
agent_api_url: config.agent_api_url.clone(),
|
||||
db,
|
||||
config,
|
||||
}
|
||||
.into();
|
||||
|
||||
let addr = dioxus_cli_config::fullstack_address_or_localhost();
|
||||
let listener = tokio::net::TcpListener::bind(addr)
|
||||
.await
|
||||
.map_err(|e| DashboardError::Other(format!("Failed to bind: {e}")))?;
|
||||
|
||||
tracing::info!("Dashboard server listening on {addr}");
|
||||
|
||||
let router = axum::Router::new()
|
||||
.serve_dioxus_application(ServeConfig::new(), app)
|
||||
.layer(axum::Extension(server_state));
|
||||
|
||||
axum::serve(listener, router.into_make_service())
|
||||
.await
|
||||
.map_err(|e| DashboardError::Other(format!("Server error: {e}")))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
46
compliance-dashboard/src/infrastructure/server_state.rs
Normal file
46
compliance-dashboard/src/infrastructure/server_state.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use compliance_core::DashboardConfig;
|
||||
|
||||
use super::database::Database;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerState(Arc<ServerStateInner>);
|
||||
|
||||
impl Deref for ServerState {
|
||||
type Target = ServerStateInner;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ServerStateInner {
|
||||
pub db: Database,
|
||||
pub config: DashboardConfig,
|
||||
pub agent_api_url: String,
|
||||
}
|
||||
|
||||
impl From<ServerStateInner> for ServerState {
|
||||
fn from(inner: ServerStateInner) -> Self {
|
||||
Self(Arc::new(inner))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> axum::extract::FromRequestParts<S> for ServerState
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = axum::http::StatusCode;
|
||||
|
||||
async fn from_request_parts(
|
||||
parts: &mut axum::http::request::Parts,
|
||||
_state: &S,
|
||||
) -> Result<Self, Self::Rejection> {
|
||||
parts
|
||||
.extensions
|
||||
.get::<ServerState>()
|
||||
.cloned()
|
||||
.ok_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
}
|
||||
27
compliance-dashboard/src/infrastructure/stats.rs
Normal file
27
compliance-dashboard/src/infrastructure/stats.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use dioxus::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct OverviewStats {
|
||||
pub total_repositories: u64,
|
||||
pub total_findings: u64,
|
||||
pub critical_findings: u64,
|
||||
pub high_findings: u64,
|
||||
pub medium_findings: u64,
|
||||
pub low_findings: u64,
|
||||
pub total_sbom_entries: u64,
|
||||
pub total_cve_alerts: u64,
|
||||
pub total_issues: u64,
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub async fn fetch_overview_stats() -> Result<OverviewStats, ServerFnError> {
|
||||
let state: super::server_state::ServerState =
|
||||
dioxus_fullstack::FullstackContext::extract().await?;
|
||||
let url = format!("{}/api/v1/stats/overview", state.agent_api_url);
|
||||
|
||||
let resp = reqwest::get(&url).await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let body: serde_json::Value = resp.json().await.map_err(|e| ServerFnError::new(e.to_string()))?;
|
||||
let stats: OverviewStats = serde_json::from_value(body["data"].clone()).unwrap_or_default();
|
||||
Ok(stats)
|
||||
}
|
||||
Reference in New Issue
Block a user