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:
@@ -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 }
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
186
compliance-core/src/models/graph.rs
Normal file
186
compliance-core/src/models/graph.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
47
compliance-core/src/traits/dast_agent.rs
Normal file
47
compliance-core/src/traits/dast_agent.rs
Normal 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>;
|
||||
}
|
||||
30
compliance-core/src/traits/graph_builder.rs
Normal file
30
compliance-core/src/traits/graph_builder.rs
Normal 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>;
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user