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:
276
compliance-core/src/models/dast.rs
Normal file
276
compliance-core/src/models/dast.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user