Skip to main content

ploit_malper/engine/
parser.rs

1use 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
9/// Parse a VulnMalper scan report into normalized records plus the injectable
10/// endpoints the scanner flagged on each host.
11pub 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    // Ensure every record carries its extracted CVE ids before deduplication.
29    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
95/// Collect `hosts[].injectable` endpoint lists, associating each endpoint with
96/// the host that reported it.
97fn 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}