ploit_malper/engine/
parser.rs1use crate::error::Result;
2use crate::models::{InjectableEndpoint, ParsedScan, ScanRecord};
3use serde_json::Value;
4
5pub fn parse_vulnmalper_json(json_input: &str) -> Result<Vec<ScanRecord>> {
6 Ok(parse_vulnmalper_scan(json_input)?.records)
7}
8
9pub fn parse_vulnmalper_scan(json_input: &str) -> Result<ParsedScan> {
12 let value: Value = serde_json::from_str(json_input)?;
13
14 let mut records = match &value {
15 Value::Array(arr) => parse_array(arr),
16 Value::Object(obj) => {
17 if let Some(arr) = obj.get("results").and_then(Value::as_array) {
18 parse_array(arr)
19 } else if let Some(arr) = obj.get("findings").and_then(Value::as_array) {
20 parse_array(arr)
21 } else {
22 Vec::new()
23 }
24 }
25 _ => Vec::new(),
26 };
27
28 for record in &mut records {
30 crate::cve::extract_into_record(record);
31 }
32
33 let injectable_endpoints = extract_injectable_endpoints(&value);
34
35 Ok(ParsedScan {
36 records,
37 injectable_endpoints,
38 })
39}
40
41fn parse_array(arr: &[Value]) -> Vec<ScanRecord> {
42 arr.iter()
43 .filter_map(|v| {
44 let target = v.get("target")?.as_str()?.to_string();
45 let tool = v.get("tool")?.as_str()?.to_string();
46 let title = v.get("title")?.as_str()?.to_string();
47 let severity = v
48 .get("severity")
49 .and_then(Value::as_str)
50 .unwrap_or("unknown")
51 .to_string();
52 let port = v
53 .get("port")
54 .and_then(Value::as_u64)
55 .and_then(|port| u16::try_from(port).ok());
56 let cve = v.get("cve").and_then(Value::as_str).map(ToOwned::to_owned);
57 let service = v
58 .get("service")
59 .and_then(Value::as_str)
60 .map(ToOwned::to_owned);
61 let detail = v
62 .get("detail")
63 .and_then(Value::as_str)
64 .unwrap_or("")
65 .to_string();
66 let reference = v
67 .get("reference")
68 .and_then(Value::as_str)
69 .unwrap_or("")
70 .to_string();
71
72 let mut record = ScanRecord {
73 target,
74 tool,
75 title,
76 severity,
77 port,
78 cve,
79 service,
80 raw: v.clone(),
81 detail,
82 reference,
83 ..Default::default()
84 };
85 if record.affected_endpoints.is_empty() {
86 record.affected_endpoints = crate::analyze::extract_endpoint(&record)
87 .into_iter()
88 .collect();
89 }
90 Some(record)
91 })
92 .collect()
93}
94
95fn extract_injectable_endpoints(value: &Value) -> Vec<InjectableEndpoint> {
98 let mut endpoints = Vec::new();
99 let hosts = match value.as_object().and_then(|obj| obj.get("hosts")) {
100 Some(Value::Array(hosts)) => hosts,
101 _ => return endpoints,
102 };
103
104 for host in hosts {
105 let target = host
106 .get("url")
107 .and_then(Value::as_str)
108 .or_else(|| host.get("host").and_then(Value::as_str))
109 .unwrap_or("unknown")
110 .to_string();
111 let injectable = match host.get("injectable") {
112 Some(Value::Array(list)) => list,
113 _ => continue,
114 };
115 for entry in injectable {
116 if let Some(endpoint) = entry.as_str() {
117 let endpoint = endpoint.trim().to_string();
118 if endpoint.is_empty() {
119 continue;
120 }
121 endpoints.push(InjectableEndpoint {
122 target: target.clone(),
123 endpoint,
124 });
125 }
126 }
127 }
128 endpoints
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn parses_results_array() {
137 let input = r#"{"results":[{"target":"10.0.0.1","tool":"nmap","title":"OpenSSH","severity":"high"}]}"#;
138 let records = parse_vulnmalper_json(input).unwrap();
139 assert_eq!(records.len(), 1);
140 assert_eq!(records[0].target, "10.0.0.1");
141 }
142
143 #[test]
144 fn captures_detail_reference_and_extracts_cves() {
145 let input = r#"{
146 "findings": [{
147 "target": "http://10.0.0.1/",
148 "tool": "nikto",
149 "title": "GET /: Server leaks inodes. See: CVE-2003-1418:",
150 "severity": "info",
151 "detail": "GET /: Server leaks inodes. See: CVE-2003-1418",
152 "reference": "http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2003-1418"
153 }]
154 }"#;
155 let scan = parse_vulnmalper_scan(input).unwrap();
156 let record = &scan.records[0];
157 assert_eq!(
158 record.detail,
159 "GET /: Server leaks inodes. See: CVE-2003-1418"
160 );
161 assert!(record.reference.contains("cve.mitre.org"));
162 assert!(record.cves.contains(&"CVE-2003-1418".to_string()));
163 }
164
165 #[test]
166 fn extracts_host_injectable_endpoints() {
167 let input = r#"{
168 "hosts": [{
169 "url": "http://192.168.1.2/",
170 "host": "192.168.1.2",
171 "injectable": ["http://192.168.1.2?name=1", "http://192.168.1.2?id=2"]
172 }]
173 }"#;
174 let scan = parse_vulnmalper_scan(input).unwrap();
175 assert_eq!(scan.injectable_endpoints.len(), 2);
176 assert_eq!(scan.injectable_endpoints[0].target, "http://192.168.1.2/");
177 assert_eq!(
178 scan.injectable_endpoints[1].endpoint,
179 "http://192.168.1.2?id=2"
180 );
181 }
182}