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,18 @@
[package]
name = "compliance-core"
version = "0.1.0"
edition = "2021"
[lints]
workspace = true
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true }
thiserror = { workspace = true }
sha2 = { workspace = true }
hex = { workspace = true }
uuid = { workspace = true }
secrecy = { workspace = true }
mongodb = { workspace = true }

View File

@@ -0,0 +1,34 @@
use secrecy::SecretString;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug)]
pub struct AgentConfig {
pub mongodb_uri: String,
pub mongodb_database: String,
pub litellm_url: String,
pub litellm_api_key: SecretString,
pub litellm_model: String,
pub github_token: Option<SecretString>,
pub github_webhook_secret: Option<SecretString>,
pub gitlab_url: Option<String>,
pub gitlab_token: Option<SecretString>,
pub gitlab_webhook_secret: Option<SecretString>,
pub jira_url: Option<String>,
pub jira_email: Option<String>,
pub jira_api_token: Option<SecretString>,
pub jira_project_key: Option<String>,
pub searxng_url: Option<String>,
pub nvd_api_key: Option<SecretString>,
pub agent_port: u16,
pub scan_schedule: String,
pub cve_monitor_schedule: String,
pub git_clone_base_path: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DashboardConfig {
pub mongodb_uri: String,
pub mongodb_database: String,
pub agent_api_url: String,
pub dashboard_port: u16,
}

View File

@@ -0,0 +1,41 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum CoreError {
#[error("Database error: {0}")]
Database(#[from] mongodb::error::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Git error: {0}")]
Git(String),
#[error("Scanner error: {source}")]
Scanner {
scanner: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("LLM error: {0}")]
Llm(String),
#[error("Issue tracker error: {0}")]
IssueTracker(String),
#[error("HTTP error: {0}")]
Http(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Not found: {0}")]
NotFound(String),
#[error("{0}")]
Other(String),
}

View File

@@ -0,0 +1,7 @@
pub mod config;
pub mod error;
pub mod models;
pub mod traits;
pub use config::{AgentConfig, DashboardConfig};
pub use error::CoreError;

View File

@@ -0,0 +1,46 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CveSource {
Osv,
Nvd,
SearXNG,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CveAlert {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<mongodb::bson::oid::ObjectId>,
pub cve_id: String,
pub repo_id: String,
pub affected_package: String,
pub affected_version: String,
pub source: CveSource,
pub severity: Option<String>,
pub cvss_score: Option<f64>,
pub summary: Option<String>,
pub llm_impact_summary: Option<String>,
pub references: Vec<String>,
pub created_at: DateTime<Utc>,
}
impl CveAlert {
pub fn new(cve_id: String, repo_id: String, affected_package: String, affected_version: String, source: CveSource) -> Self {
Self {
id: None,
cve_id,
repo_id,
affected_package,
affected_version,
source,
severity: None,
cvss_score: None,
summary: None,
llm_impact_summary: None,
references: Vec::new(),
created_at: Utc::now(),
}
}
}

View File

@@ -0,0 +1,115 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::scan::ScanType;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
Info,
Low,
Medium,
High,
Critical,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Info => write!(f, "info"),
Self::Low => write!(f, "low"),
Self::Medium => write!(f, "medium"),
Self::High => write!(f, "high"),
Self::Critical => write!(f, "critical"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum FindingStatus {
Open,
Triaged,
FalsePositive,
Resolved,
Ignored,
}
impl std::fmt::Display for FindingStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Open => write!(f, "open"),
Self::Triaged => write!(f, "triaged"),
Self::FalsePositive => write!(f, "false_positive"),
Self::Resolved => write!(f, "resolved"),
Self::Ignored => write!(f, "ignored"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Finding {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<mongodb::bson::oid::ObjectId>,
pub repo_id: String,
pub fingerprint: String,
pub scanner: String,
pub scan_type: ScanType,
pub rule_id: Option<String>,
pub title: String,
pub description: String,
pub severity: Severity,
pub confidence: Option<f64>,
pub cwe: Option<String>,
pub cve: Option<String>,
pub cvss_score: Option<f64>,
pub file_path: Option<String>,
pub line_number: Option<u32>,
pub code_snippet: Option<String>,
pub remediation: Option<String>,
pub suggested_fix: Option<String>,
pub status: FindingStatus,
pub tracker_issue_url: Option<String>,
pub scan_run_id: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Finding {
pub fn new(
repo_id: String,
fingerprint: String,
scanner: String,
scan_type: ScanType,
title: String,
description: String,
severity: Severity,
) -> Self {
let now = Utc::now();
Self {
id: None,
repo_id,
fingerprint,
scanner,
scan_type,
rule_id: None,
title,
description,
severity,
confidence: None,
cwe: None,
cve: None,
cvss_score: None,
file_path: None,
line_number: None,
code_snippet: None,
remediation: None,
suggested_fix: None,
status: FindingStatus::Open,
tracker_issue_url: None,
scan_run_id: None,
created_at: now,
updated_at: now,
}
}
}

View File

@@ -0,0 +1,77 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TrackerType {
GitHub,
GitLab,
Jira,
}
impl std::fmt::Display for TrackerType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::GitHub => write!(f, "github"),
Self::GitLab => write!(f, "gitlab"),
Self::Jira => write!(f, "jira"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum IssueStatus {
Open,
InProgress,
Closed,
Resolved,
}
impl std::fmt::Display for IssueStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Open => write!(f, "open"),
Self::InProgress => write!(f, "in_progress"),
Self::Closed => write!(f, "closed"),
Self::Resolved => write!(f, "resolved"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackerIssue {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<mongodb::bson::oid::ObjectId>,
pub finding_id: String,
pub tracker_type: TrackerType,
pub external_id: String,
pub external_url: String,
pub title: String,
pub status: IssueStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl TrackerIssue {
pub fn new(
finding_id: String,
tracker_type: TrackerType,
external_id: String,
external_url: String,
title: String,
) -> Self {
let now = Utc::now();
Self {
id: None,
finding_id,
tracker_type,
external_id,
external_url,
title,
status: IssueStatus::Open,
created_at: now,
updated_at: now,
}
}
}

View File

@@ -0,0 +1,13 @@
pub mod cve;
pub mod finding;
pub mod issue;
pub mod repository;
pub mod sbom;
pub mod scan;
pub use cve::{CveAlert, CveSource};
pub use finding::{Finding, FindingStatus, Severity};
pub use issue::{IssueStatus, TrackerIssue, TrackerType};
pub use repository::{ScanTrigger, TrackedRepository};
pub use sbom::{SbomEntry, VulnRef};
pub use scan::{ScanPhase, ScanRun, ScanRunStatus, ScanType};

View File

@@ -0,0 +1,53 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::issue::TrackerType;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ScanTrigger {
Scheduled,
Webhook,
Manual,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrackedRepository {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<mongodb::bson::oid::ObjectId>,
pub name: String,
pub git_url: String,
pub default_branch: String,
pub local_path: Option<String>,
pub scan_schedule: Option<String>,
pub webhook_enabled: bool,
pub tracker_type: Option<TrackerType>,
pub tracker_owner: Option<String>,
pub tracker_repo: Option<String>,
pub last_scanned_commit: Option<String>,
pub findings_count: u32,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl TrackedRepository {
pub fn new(name: String, git_url: String) -> Self {
let now = Utc::now();
Self {
id: None,
name,
git_url,
default_branch: "main".to_string(),
local_path: None,
scan_schedule: None,
webhook_enabled: false,
tracker_type: None,
tracker_owner: None,
tracker_repo: None,
last_scanned_commit: None,
findings_count: 0,
created_at: now,
updated_at: now,
}
}
}

View File

@@ -0,0 +1,43 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VulnRef {
pub id: String,
pub source: String,
pub severity: Option<String>,
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SbomEntry {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<mongodb::bson::oid::ObjectId>,
pub repo_id: String,
pub name: String,
pub version: String,
pub package_manager: String,
pub license: Option<String>,
pub purl: Option<String>,
pub known_vulnerabilities: Vec<VulnRef>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl SbomEntry {
pub fn new(repo_id: String, name: String, version: String, package_manager: String) -> Self {
let now = Utc::now();
Self {
id: None,
repo_id,
name,
version,
package_manager,
license: None,
purl: None,
known_vulnerabilities: Vec::new(),
created_at: now,
updated_at: now,
}
}
}

View File

@@ -0,0 +1,81 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::repository::ScanTrigger;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ScanType {
Sast,
Sbom,
Cve,
Gdpr,
OAuth,
}
impl std::fmt::Display for ScanType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Sast => write!(f, "sast"),
Self::Sbom => write!(f, "sbom"),
Self::Cve => write!(f, "cve"),
Self::Gdpr => write!(f, "gdpr"),
Self::OAuth => write!(f, "oauth"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ScanRunStatus {
Running,
Completed,
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ScanPhase {
ChangeDetection,
Sast,
SbomGeneration,
CveScanning,
PatternScanning,
LlmTriage,
IssueCreation,
Completed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScanRun {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<mongodb::bson::oid::ObjectId>,
pub repo_id: String,
pub trigger: ScanTrigger,
pub commit_sha: Option<String>,
pub status: ScanRunStatus,
pub current_phase: ScanPhase,
pub phases_completed: Vec<ScanPhase>,
pub new_findings_count: u32,
pub error_message: Option<String>,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
}
impl ScanRun {
pub fn new(repo_id: String, trigger: ScanTrigger) -> Self {
Self {
id: None,
repo_id,
trigger,
commit_sha: None,
status: ScanRunStatus::Running,
current_phase: ScanPhase::ChangeDetection,
phases_completed: Vec::new(),
new_findings_count: 0,
error_message: None,
started_at: Utc::now(),
completed_at: None,
}
}
}

View File

@@ -0,0 +1,55 @@
use crate::error::CoreError;
use crate::models::TrackerIssue;
#[allow(async_fn_in_trait)]
pub trait IssueTracker: Send + Sync {
fn name(&self) -> &str;
async fn create_issue(
&self,
owner: &str,
repo: &str,
title: &str,
body: &str,
labels: &[String],
) -> Result<TrackerIssue, CoreError>;
async fn update_issue_status(
&self,
owner: &str,
repo: &str,
external_id: &str,
status: &str,
) -> Result<(), CoreError>;
async fn add_comment(
&self,
owner: &str,
repo: &str,
external_id: &str,
body: &str,
) -> Result<(), CoreError>;
async fn create_pr_review(
&self,
owner: &str,
repo: &str,
pr_number: u64,
body: &str,
comments: Vec<ReviewComment>,
) -> Result<(), CoreError>;
async fn find_existing_issue(
&self,
owner: &str,
repo: &str,
fingerprint: &str,
) -> Result<Option<TrackerIssue>, CoreError>;
}
#[derive(Debug, Clone)]
pub struct ReviewComment {
pub path: String,
pub line: u32,
pub body: String,
}

View File

@@ -0,0 +1,5 @@
pub mod issue_tracker;
pub mod scanner;
pub use issue_tracker::IssueTracker;
pub use scanner::{ScanOutput, Scanner};

View File

@@ -0,0 +1,17 @@
use std::path::Path;
use crate::error::CoreError;
use crate::models::{Finding, SbomEntry, ScanType};
#[derive(Debug, Default)]
pub struct ScanOutput {
pub findings: Vec<Finding>,
pub sbom_entries: Vec<SbomEntry>,
}
#[allow(async_fn_in_trait)]
pub trait Scanner: Send + Sync {
fn name(&self) -> &str;
fn scan_type(&self) -> ScanType;
async fn scan(&self, repo_path: &Path, repo_id: &str) -> Result<ScanOutput, CoreError>;
}