1use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
19pub enum EntityType {
20 #[serde(rename = "scan")]
21 Scan,
22 #[serde(rename = "finding")]
23 Finding,
24 #[serde(rename = "endpoint")]
25 Endpoint,
26 #[serde(rename = "vulnerability")]
27 Vulnerability,
28 #[serde(rename = "scan_result")]
29 ScanResult,
30}
31
32impl EntityType {
33 pub fn as_str(self) -> &'static str {
34 match self {
35 Self::Scan => "scan",
36 Self::Finding => "finding",
37 Self::Endpoint => "endpoint",
38 Self::Vulnerability => "vulnerability",
39 Self::ScanResult => "scan_result",
40 }
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub struct ScanRecord {
47 pub scan_id: String,
48 pub target_url: String,
49 pub status: String,
50 pub started_at: u64,
51 pub completed_at: Option<u64>,
52 pub duration_ms: Option<u64>,
53 pub findings_count: u32,
54 pub critical_count: u32,
55 pub high_count: u32,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub struct FindingRecord {
61 pub finding_id: String,
62 pub scan_id: String,
63 pub phase: u8,
64 pub module_name: String,
65 pub severity: String,
66 pub description: String,
67 pub evidence: String,
68 pub discovered_at: u64,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct EndpointRecord {
74 pub endpoint_id: String,
75 pub scan_id: String,
76 pub url: String,
77 pub method: String,
78 pub status_code: u32,
79 pub response_time_ms: u32,
80 pub discovered_at: u64,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct TableSchema {
86 pub table_name: String,
87 pub columns: Vec<ColumnDef>,
88 pub indexes: Vec<IndexDef>,
89 pub primary_key: String,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct ColumnDef {
95 pub name: String,
96 pub data_type: String,
97 pub nullable: bool,
98 pub indexed: bool,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct IndexDef {
104 pub name: String,
105 pub columns: Vec<String>,
106 pub unique: bool,
107}
108
109#[derive(Debug, Clone, Default)]
111pub struct SchemaManager {
112 schemas: HashMap<String, TableSchema>,
113}
114
115impl SchemaManager {
116 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn register_schema(&mut self, schema: TableSchema) -> Option<TableSchema> {
124 self.schemas.insert(schema.table_name.clone(), schema)
125 }
126
127 pub fn get_schema(&self, table_name: &str) -> Option<&TableSchema> {
128 self.schemas.get(table_name)
129 }
130
131 pub fn schema_count(&self) -> usize {
132 self.schemas.len()
133 }
134
135 pub fn is_empty(&self) -> bool {
136 self.schemas.is_empty()
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn schema(name: &str, data_type: &str) -> TableSchema {
145 TableSchema {
146 table_name: name.to_string(),
147 columns: vec![ColumnDef {
148 name: "scan_id".to_string(),
149 data_type: data_type.to_string(),
150 nullable: false,
151 indexed: true,
152 }],
153 indexes: Vec::new(),
154 primary_key: "scan_id".to_string(),
155 }
156 }
157
158 #[test]
159 fn entity_type_is_metadata_without_table_mapping() {
160 assert_eq!(EntityType::Finding.as_str(), "finding");
161 }
162
163 #[test]
164 fn schema_catalog_is_empty_until_the_caller_records_metadata() {
165 let catalog = SchemaManager::new();
166 assert!(catalog.is_empty());
167 assert!(catalog.get_schema("scans").is_none());
168 }
169
170 #[test]
171 fn duplicate_schema_names_replace_and_return_the_previous_record() {
172 let mut catalog = SchemaManager::new();
173 assert!(catalog.register_schema(schema("scans", "TEXT")).is_none());
174
175 let previous = catalog
176 .register_schema(schema("scans", "opaque-not-sql"))
177 .expect("the original metadata must be returned");
178
179 assert_eq!(previous.columns[0].data_type, "TEXT");
180 assert_eq!(catalog.schema_count(), 1);
181 assert_eq!(
182 catalog.get_schema("scans").unwrap().columns[0].data_type,
183 "opaque-not-sql"
184 );
185 }
186
187 #[test]
188 fn schema_values_remain_opaque_metadata() {
189 let suspicious = "TEXT); DROP TABLE scans; --";
190 let mut catalog = SchemaManager::new();
191 let _ = catalog.register_schema(schema("scans", suspicious));
192
193 assert_eq!(
194 catalog.get_schema("scans").unwrap().columns[0].data_type,
195 suspicious
196 );
197 }
198}