Add DAST, graph modules, toast notifications, and dashboard enhancements

Add DAST scanning and code knowledge graph features across the stack:
- compliance-dast and compliance-graph workspace crates
- Agent API handlers and routes for DAST targets/scans and graph builds
- Core models and traits for DAST and graph domains
- Dashboard pages for DAST targets/findings/overview and graph explorer/impact
- Toast notification system with auto-dismiss for async action feedback
- Button click animations and disabled states for better UX

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-03-04 13:53:50 +01:00
parent 03ee69834d
commit cea8f59e10
69 changed files with 8745 additions and 54 deletions

View File

@@ -19,5 +19,5 @@ sha2 = { workspace = true }
hex = { workspace = true }
uuid = { workspace = true }
secrecy = { workspace = true }
bson = "2"
bson = { version = "2", features = ["chrono-0_4"] }
mongodb = { workspace = true, optional = true }

View File

@@ -38,6 +38,12 @@ pub enum CoreError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Graph error: {0}")]
Graph(String),
#[error("DAST error: {0}")]
Dast(String),
#[error("Not found: {0}")]
NotFound(String),

View File

@@ -0,0 +1,276 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::finding::Severity;
/// Type of DAST target application
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DastTargetType {
WebApp,
RestApi,
GraphQl,
}
impl std::fmt::Display for DastTargetType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::WebApp => write!(f, "webapp"),
Self::RestApi => write!(f, "rest_api"),
Self::GraphQl => write!(f, "graphql"),
}
}
}
/// Authentication configuration for DAST target
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DastAuthConfig {
/// Authentication method: "none", "basic", "bearer", "cookie", "form"
pub method: String,
/// Login URL for form-based auth
pub login_url: Option<String>,
/// Username or token
pub username: Option<String>,
/// Password (stored encrypted in practice)
pub password: Option<String>,
/// Bearer token
pub token: Option<String>,
/// Custom headers for auth
pub headers: Option<std::collections::HashMap<String, String>>,
}
/// A target for DAST scanning
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DastTarget {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<bson::oid::ObjectId>,
pub name: String,
pub base_url: String,
pub target_type: DastTargetType,
pub auth_config: Option<DastAuthConfig>,
/// Linked repository ID (for SAST correlation)
pub repo_id: Option<String>,
/// URL paths to exclude from scanning
pub excluded_paths: Vec<String>,
/// Maximum crawl depth
pub max_crawl_depth: u32,
/// Rate limit (requests per second)
pub rate_limit: u32,
/// Whether destructive tests (DELETE, PUT) are allowed
pub allow_destructive: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl DastTarget {
pub fn new(name: String, base_url: String, target_type: DastTargetType) -> Self {
let now = Utc::now();
Self {
id: None,
name,
base_url,
target_type,
auth_config: None,
repo_id: None,
excluded_paths: Vec::new(),
max_crawl_depth: 5,
rate_limit: 10,
allow_destructive: false,
created_at: now,
updated_at: now,
}
}
}
/// Phase of a DAST scan
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DastScanPhase {
Reconnaissance,
Crawling,
VulnerabilityAnalysis,
Exploitation,
Reporting,
Completed,
}
impl std::fmt::Display for DastScanPhase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Reconnaissance => write!(f, "reconnaissance"),
Self::Crawling => write!(f, "crawling"),
Self::VulnerabilityAnalysis => write!(f, "vulnerability_analysis"),
Self::Exploitation => write!(f, "exploitation"),
Self::Reporting => write!(f, "reporting"),
Self::Completed => write!(f, "completed"),
}
}
}
/// Status of a DAST scan run
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DastScanStatus {
Running,
Completed,
Failed,
Cancelled,
}
/// A DAST scan run
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DastScanRun {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<bson::oid::ObjectId>,
pub target_id: String,
pub status: DastScanStatus,
pub current_phase: DastScanPhase,
pub phases_completed: Vec<DastScanPhase>,
/// Number of endpoints discovered during crawling
pub endpoints_discovered: u32,
/// Number of findings
pub findings_count: u32,
/// Number of confirmed exploitable findings
pub exploitable_count: u32,
pub error_message: Option<String>,
/// Linked SAST scan run ID (if triggered as part of pipeline)
pub sast_scan_run_id: Option<String>,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
}
impl DastScanRun {
pub fn new(target_id: String) -> Self {
Self {
id: None,
target_id,
status: DastScanStatus::Running,
current_phase: DastScanPhase::Reconnaissance,
phases_completed: Vec::new(),
endpoints_discovered: 0,
findings_count: 0,
exploitable_count: 0,
error_message: None,
sast_scan_run_id: None,
started_at: Utc::now(),
completed_at: None,
}
}
}
/// Type of DAST vulnerability
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DastVulnType {
SqlInjection,
Xss,
AuthBypass,
Ssrf,
ApiMisconfiguration,
OpenRedirect,
Idor,
InformationDisclosure,
SecurityMisconfiguration,
BrokenAuth,
Other,
}
impl std::fmt::Display for DastVulnType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::SqlInjection => write!(f, "sql_injection"),
Self::Xss => write!(f, "xss"),
Self::AuthBypass => write!(f, "auth_bypass"),
Self::Ssrf => write!(f, "ssrf"),
Self::ApiMisconfiguration => write!(f, "api_misconfiguration"),
Self::OpenRedirect => write!(f, "open_redirect"),
Self::Idor => write!(f, "idor"),
Self::InformationDisclosure => write!(f, "information_disclosure"),
Self::SecurityMisconfiguration => write!(f, "security_misconfiguration"),
Self::BrokenAuth => write!(f, "broken_auth"),
Self::Other => write!(f, "other"),
}
}
}
/// Evidence collected during DAST testing
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DastEvidence {
/// HTTP request that triggered the finding
pub request_method: String,
pub request_url: String,
pub request_headers: Option<std::collections::HashMap<String, String>>,
pub request_body: Option<String>,
/// HTTP response
pub response_status: u16,
pub response_headers: Option<std::collections::HashMap<String, String>>,
/// Relevant snippet of response body
pub response_snippet: Option<String>,
/// Path to screenshot file (if captured)
pub screenshot_path: Option<String>,
/// The payload that triggered the vulnerability
pub payload: Option<String>,
/// Timing information (for timing-based attacks)
pub response_time_ms: Option<u64>,
}
/// A finding from DAST scanning
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DastFinding {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<bson::oid::ObjectId>,
pub scan_run_id: String,
pub target_id: String,
pub vuln_type: DastVulnType,
pub title: String,
pub description: String,
pub severity: Severity,
pub cwe: Option<String>,
/// The URL endpoint where the vulnerability was found
pub endpoint: String,
/// HTTP method
pub method: String,
/// Parameter that is vulnerable
pub parameter: Option<String>,
/// Whether exploitability was confirmed with a working payload
pub exploitable: bool,
/// Evidence chain
pub evidence: Vec<DastEvidence>,
/// Remediation guidance
pub remediation: Option<String>,
/// Linked SAST finding ID (if correlated)
pub linked_sast_finding_id: Option<String>,
pub created_at: DateTime<Utc>,
}
impl DastFinding {
pub fn new(
scan_run_id: String,
target_id: String,
vuln_type: DastVulnType,
title: String,
description: String,
severity: Severity,
endpoint: String,
method: String,
) -> Self {
Self {
id: None,
scan_run_id,
target_id,
vuln_type,
title,
description,
severity,
cwe: None,
endpoint,
method,
parameter: None,
exploitable: false,
evidence: Vec::new(),
remediation: None,
linked_sast_finding_id: None,
created_at: Utc::now(),
}
}
}

View File

@@ -0,0 +1,186 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// Type of code node in the knowledge graph
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum CodeNodeKind {
Function,
Method,
Class,
Struct,
Enum,
Interface,
Trait,
Module,
File,
}
impl std::fmt::Display for CodeNodeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Function => write!(f, "function"),
Self::Method => write!(f, "method"),
Self::Class => write!(f, "class"),
Self::Struct => write!(f, "struct"),
Self::Enum => write!(f, "enum"),
Self::Interface => write!(f, "interface"),
Self::Trait => write!(f, "trait"),
Self::Module => write!(f, "module"),
Self::File => write!(f, "file"),
}
}
}
/// A node in the code knowledge graph
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeNode {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<bson::oid::ObjectId>,
pub repo_id: String,
pub graph_build_id: String,
/// Unique identifier within the graph (e.g., "src/main.rs::main")
pub qualified_name: String,
pub name: String,
pub kind: CodeNodeKind,
pub file_path: String,
pub start_line: u32,
pub end_line: u32,
/// Language of the source file
pub language: String,
/// Community ID from Louvain clustering
pub community_id: Option<u32>,
/// Whether this is a public entry point (main, exported fn, HTTP handler, etc.)
pub is_entry_point: bool,
/// Internal petgraph node index for fast lookups
#[serde(skip_serializing_if = "Option::is_none")]
pub graph_index: Option<u32>,
}
/// Type of relationship between code nodes
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum CodeEdgeKind {
Calls,
Imports,
Inherits,
Implements,
Contains,
/// A type reference (e.g., function parameter type, return type)
TypeRef,
}
impl std::fmt::Display for CodeEdgeKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Calls => write!(f, "calls"),
Self::Imports => write!(f, "imports"),
Self::Inherits => write!(f, "inherits"),
Self::Implements => write!(f, "implements"),
Self::Contains => write!(f, "contains"),
Self::TypeRef => write!(f, "type_ref"),
}
}
}
/// An edge in the code knowledge graph
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeEdge {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<bson::oid::ObjectId>,
pub repo_id: String,
pub graph_build_id: String,
/// Qualified name of source node
pub source: String,
/// Qualified name of target node
pub target: String,
pub kind: CodeEdgeKind,
/// File where this relationship was found
pub file_path: String,
pub line_number: Option<u32>,
}
/// Status of a graph build operation
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum GraphBuildStatus {
Running,
Completed,
Failed,
}
/// Tracks a graph build operation for a repo/commit
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphBuildRun {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<bson::oid::ObjectId>,
pub repo_id: String,
pub commit_sha: Option<String>,
pub status: GraphBuildStatus,
pub node_count: u32,
pub edge_count: u32,
pub community_count: u32,
pub languages_parsed: Vec<String>,
pub error_message: Option<String>,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
}
impl GraphBuildRun {
pub fn new(repo_id: String) -> Self {
Self {
id: None,
repo_id,
commit_sha: None,
status: GraphBuildStatus::Running,
node_count: 0,
edge_count: 0,
community_count: 0,
languages_parsed: Vec::new(),
error_message: None,
started_at: Utc::now(),
completed_at: None,
}
}
}
/// Impact analysis result for a finding
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImpactAnalysis {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<bson::oid::ObjectId>,
pub repo_id: String,
pub finding_id: String,
pub graph_build_id: String,
/// Number of nodes reachable from the finding location
pub blast_radius: u32,
/// Entry points affected by this finding (via reverse call chain)
pub affected_entry_points: Vec<String>,
/// Call chains from entry points to the finding location
pub call_chains: Vec<Vec<String>>,
/// Community IDs affected
pub affected_communities: Vec<u32>,
/// Direct callers of the affected function
pub direct_callers: Vec<String>,
/// Direct callees of the affected function
pub direct_callees: Vec<String>,
pub created_at: DateTime<Utc>,
}
impl ImpactAnalysis {
pub fn new(repo_id: String, finding_id: String, graph_build_id: String) -> Self {
Self {
id: None,
repo_id,
finding_id,
graph_build_id,
blast_radius: 0,
affected_entry_points: Vec::new(),
call_chains: Vec::new(),
affected_communities: Vec::new(),
direct_callers: Vec::new(),
direct_callees: Vec::new(),
created_at: Utc::now(),
}
}
}

View File

@@ -1,12 +1,22 @@
pub mod cve;
pub mod dast;
pub mod finding;
pub mod graph;
pub mod issue;
pub mod repository;
pub mod sbom;
pub mod scan;
pub use cve::{CveAlert, CveSource};
pub use dast::{
DastAuthConfig, DastEvidence, DastFinding, DastScanPhase, DastScanRun, DastScanStatus,
DastTarget, DastTargetType, DastVulnType,
};
pub use finding::{Finding, FindingStatus, Severity};
pub use graph::{
CodeEdge, CodeEdgeKind, CodeNode, CodeNodeKind, GraphBuildRun, GraphBuildStatus,
ImpactAnalysis,
};
pub use issue::{IssueStatus, TrackerIssue, TrackerType};
pub use repository::{ScanTrigger, TrackedRepository};
pub use sbom::{SbomEntry, VulnRef};

View File

@@ -1,5 +1,5 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
use super::issue::TrackerType;
@@ -15,21 +15,64 @@ pub enum ScanTrigger {
pub struct TrackedRepository {
#[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
pub id: Option<bson::oid::ObjectId>,
#[serde(default)]
pub name: String,
#[serde(default)]
pub git_url: String,
#[serde(default = "default_branch")]
pub default_branch: String,
pub local_path: Option<String>,
pub scan_schedule: Option<String>,
#[serde(default)]
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>,
#[serde(default, deserialize_with = "deserialize_findings_count")]
pub findings_count: u32,
#[serde(default = "chrono::Utc::now", deserialize_with = "deserialize_datetime")]
pub created_at: DateTime<Utc>,
#[serde(default = "chrono::Utc::now", deserialize_with = "deserialize_datetime")]
pub updated_at: DateTime<Utc>,
}
fn default_branch() -> String {
"main".to_string()
}
/// Handles findings_count stored as either a plain integer or a BSON Int64
/// which the driver may present as a map `{"low": N, "high": N, "unsigned": bool}`.
/// Handles datetime stored as either a BSON DateTime or an RFC 3339 string.
fn deserialize_datetime<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let bson = bson::Bson::deserialize(deserializer)?;
match bson {
bson::Bson::DateTime(dt) => Ok(dt.into()),
bson::Bson::String(s) => s
.parse::<DateTime<Utc>>()
.map_err(serde::de::Error::custom),
other => Err(serde::de::Error::custom(format!(
"expected DateTime or string, got: {other:?}"
))),
}
}
fn deserialize_findings_count<'de, D>(deserializer: D) -> Result<u32, D::Error>
where
D: Deserializer<'de>,
{
let bson = bson::Bson::deserialize(deserializer)?;
match &bson {
bson::Bson::Int32(n) => Ok(*n as u32),
bson::Bson::Int64(n) => Ok(*n as u32),
bson::Bson::Double(n) => Ok(*n as u32),
_ => Ok(0),
}
}
impl TrackedRepository {
pub fn new(name: String, git_url: String) -> Self {
let now = Utc::now();

View File

@@ -11,6 +11,8 @@ pub enum ScanType {
Cve,
Gdpr,
OAuth,
Graph,
Dast,
}
impl std::fmt::Display for ScanType {
@@ -21,6 +23,8 @@ impl std::fmt::Display for ScanType {
Self::Cve => write!(f, "cve"),
Self::Gdpr => write!(f, "gdpr"),
Self::OAuth => write!(f, "oauth"),
Self::Graph => write!(f, "graph"),
Self::Dast => write!(f, "dast"),
}
}
}
@@ -41,8 +45,10 @@ pub enum ScanPhase {
SbomGeneration,
CveScanning,
PatternScanning,
GraphBuilding,
LlmTriage,
IssueCreation,
DastScanning,
Completed,
}

View File

@@ -0,0 +1,47 @@
use crate::error::CoreError;
use crate::models::dast::{DastFinding, DastTarget};
/// Context passed to DAST agents containing discovered information
#[derive(Debug, Clone, Default)]
pub struct DastContext {
/// Discovered endpoints from crawling
pub endpoints: Vec<DiscoveredEndpoint>,
/// Technologies detected during recon
pub technologies: Vec<String>,
/// Existing SAST findings for prioritization
pub sast_hints: Vec<String>,
}
/// An endpoint discovered during crawling
#[derive(Debug, Clone)]
pub struct DiscoveredEndpoint {
pub url: String,
pub method: String,
pub parameters: Vec<EndpointParameter>,
pub content_type: Option<String>,
pub requires_auth: bool,
}
/// A parameter on a discovered endpoint
#[derive(Debug, Clone)]
pub struct EndpointParameter {
pub name: String,
/// "query", "body", "header", "path", "cookie"
pub location: String,
pub param_type: Option<String>,
pub example_value: Option<String>,
}
/// Trait for DAST testing agents (injection, XSS, auth bypass, etc.)
#[allow(async_fn_in_trait)]
pub trait DastAgent: Send + Sync {
/// Agent name (e.g., "sql_injection", "xss", "auth_bypass")
fn name(&self) -> &str;
/// Run the agent against a target with discovered context
async fn run(
&self,
target: &DastTarget,
context: &DastContext,
) -> Result<Vec<DastFinding>, CoreError>;
}

View File

@@ -0,0 +1,30 @@
use std::path::Path;
use crate::error::CoreError;
use crate::models::graph::{CodeEdge, CodeNode};
/// Output from parsing a single file
#[derive(Debug, Default)]
pub struct ParseOutput {
pub nodes: Vec<CodeNode>,
pub edges: Vec<CodeEdge>,
}
/// Trait for language-specific code parsers
#[allow(async_fn_in_trait)]
pub trait LanguageParser: Send + Sync {
/// Language name (e.g., "rust", "python", "javascript")
fn language(&self) -> &str;
/// File extensions this parser handles
fn extensions(&self) -> &[&str];
/// Parse a single file and extract nodes + edges
fn parse_file(
&self,
file_path: &Path,
source: &str,
repo_id: &str,
graph_build_id: &str,
) -> Result<ParseOutput, CoreError>;
}

View File

@@ -1,5 +1,9 @@
pub mod dast_agent;
pub mod graph_builder;
pub mod issue_tracker;
pub mod scanner;
pub use dast_agent::{DastAgent, DastContext, DiscoveredEndpoint, EndpointParameter};
pub use graph_builder::{LanguageParser, ParseOutput};
pub use issue_tracker::IssueTracker;
pub use scanner::{ScanOutput, Scanner};