Skip to content

Commit f594539

Browse files
checkmarx_xml (#49)
1 parent f03f259 commit f594539

File tree

6 files changed

+358
-113
lines changed

6 files changed

+358
-113
lines changed

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod setup_hooks;
99
mod scanners {
1010
pub mod fortify;
1111
pub mod blast;
12+
pub mod parsers;
1213
}
1314
mod utils {
1415
pub mod terminal;

src/scan.rs

Lines changed: 18 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use std::collections::HashSet;
2-
use serde_json::Value;
32
use std::io::{self, Read};
43
use crate::Config;
54
use uuid::Uuid;
@@ -8,6 +7,8 @@ use std::process::Command;
87
use crate::cicd::{*};
98
use crate::log::debug;
109
use reqwest::header;
10+
use crate::scanners::parsers::ScanParserFactory;
11+
use serde_json::Value;
1112

1213
pub fn run_command(base_cmd: &String, mut command: Command) -> String {
1314
match which::which(base_cmd) {
@@ -91,123 +92,27 @@ pub fn read_file_report(config: &Config, file_path: &str) {
9192
}
9293

9394
pub fn parse_scan(config: &Config, input: String, save_to_file: bool) {
94-
debug("Parsing the scan report json");
95-
96-
let mut paths: Vec<String> = Vec::new();
97-
let mut scanner = String::new();
98-
let data: std::result::Result<Value, _> = serde_json::from_str(&input);
99-
100-
match data {
101-
Ok(data) => {
102-
let schema = data.get("$schema").and_then(|v| v.as_str()).unwrap_or("unknown");
103-
104-
if input.contains("semgrep.dev") {
105-
debug("Detected semgrep schema");
106-
scanner = "semgrep".to_string();
107-
if let Some(results) = data.get("results").and_then(|v| v.as_array()) {
108-
for result in results {
109-
if let Some(path) = result.get("path").and_then(|v| v.as_str()) {
110-
paths.push(path.to_string());
111-
}
112-
}
113-
}
114-
} else if schema.contains("sarif") {
115-
debug("Detected sarif schema");
116-
let run = data.get("runs").and_then(|v| v.as_array()).and_then(|v| v.get(0));
117-
let driver = run.and_then(|v| v.get("tool")).and_then(|v| v.get("driver")).and_then(|v| v.get("name"));
118-
let tool = driver.and_then(|v| v.as_str()).unwrap_or("unknown");
119-
120-
if tool == "SnykCode" {
121-
debug("Detected snyk version of sarif schema");
122-
scanner = "snyk".to_string();
123-
} else if tool == "CodeQL" {
124-
debug("Detected codeql version of sarif schema");
125-
scanner = "codeql".to_string();
126-
} else {
127-
eprintln!("{} is not supported as this time.", tool);
128-
std::process::exit(1);
129-
}
130-
131-
if let Some(runs) = data.get("runs").and_then(|v| v.as_array()) {
132-
for run in runs {
133-
if let Some(results) = run.get("results").and_then(|v| v.as_array()) {
134-
for result in results {
135-
if let Some(locations) = result.get("locations").and_then(|v| v.as_array()) {
136-
for location in locations {
137-
if let Some(uri) = location.get("physicalLocation").and_then(|v| v.get("artifactLocation")).and_then(|v| v.get("uri")).and_then(|v| v.as_str()) {
138-
paths.push(uri.to_string());
139-
}
140-
}
141-
}
142-
}
143-
}
144-
}
145-
}
146-
// checkmarx report generated by CLI
147-
} else if data.get("totalCount").is_some() && data.get("results").is_some() && data.get("scanID").is_some() {
148-
debug("Detected checkmarx cli schema");
149-
scanner = "checkmarx".to_string();
150-
if let Some(results) = data.get("results").and_then(|v| v.as_array()) {
151-
for result in results {
152-
if let Some(data) = result.get("data") {
153-
if let Some(nodes) = data.get("nodes").and_then(|v| v.as_array()) {
154-
for node in nodes {
155-
if let Some(path) = node.get("fileName") {
156-
if let Some(truncated_path) = path.as_str() {
157-
paths.push(truncated_path.get(1..).unwrap_or("").to_string());
158-
}
159-
}
160-
}
161-
}
162-
}
163-
}
164-
}
165-
// for checkmarx report generated by web
166-
} else if data.get("scanResults").is_some() && data.get("reportId").is_some() {
167-
debug("Detected checkmarx web schema");
168-
scanner = "checkmarx".to_string();
169-
if let Some(scan_results) = data.get("scanResults") {
170-
if let Some(sast) = scan_results.get("sast") {
171-
if let Some(languges) = sast.get("languages").and_then(|v| v.as_array()) {
172-
for language in languges {
173-
if let Some(queries) = language.get("queries").and_then(|v| v.as_array()) {
174-
for query in queries {
175-
if let Some(vulns) = query.get("vulnerabilities").and_then(|v| v.as_array()) {
176-
for vuln in vulns {
177-
if let Some(nodes) = vuln.get("nodes").and_then(|v| v.as_array()) {
178-
for node in nodes {
179-
if let Some(path) = node.get("fileName") {
180-
if let Some(truncated_path) = path.as_str() {
181-
paths.push(truncated_path.get(1..).unwrap_or("").to_string());
182-
}
183-
}
184-
}
185-
}
186-
}
187-
}
188-
}
189-
}
190-
}
191-
}
192-
}
193-
}
194-
} else {
195-
debug("Couldn't detect what kind of report this is.")
95+
debug("Parsing the scan report");
96+
97+
// Remove BOM (Byte Order Mark) if present
98+
let cleaned_input = input.trim_start_matches('\u{feff}').trim();
99+
100+
let parser_factory = ScanParserFactory::new();
101+
102+
match parser_factory.parse_scan_data(cleaned_input) {
103+
Ok(parse_result) => {
104+
if parse_result.paths.is_empty() {
105+
eprintln!("No issues found in scan report, exiting.");
106+
std::process::exit(0);
196107
}
108+
109+
upload_scan(config, parse_result.paths, parse_result.scanner, cleaned_input.to_string(), save_to_file);
197110
}
198-
Err(e) => {
199-
eprintln!("Failed to parse JSON report: {}", e);
200-
eprintln!("Only reports in JSON format are supported.");
111+
Err(error_message) => {
112+
eprintln!("{}", error_message);
201113
std::process::exit(1);
202114
}
203115
}
204-
205-
if paths.len() == 0 {
206-
eprintln!("No issues found in scan report, exiting.");
207-
std::process::exit(0);
208-
}
209-
210-
upload_scan(config, paths, scanner, input, save_to_file);
211116
}
212117

213118
pub fn upload_scan(config: &Config, paths: Vec<String>, scanner: String, input: String, save_to_file: bool) {

src/scanners/parsers/checkmarx.rs

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
use serde_json::Value;
2+
use crate::log::debug;
3+
use super::{ScanParser, ParseResult};
4+
use quick_xml::Reader;
5+
use quick_xml::events::Event;
6+
7+
pub struct CheckmarxCliParser;
8+
9+
impl ScanParser for CheckmarxCliParser {
10+
fn detect(&self, input: &str) -> bool {
11+
if let Ok(data) = serde_json::from_str::<Value>(input) {
12+
data.get("totalCount").is_some()
13+
&& data.get("results").is_some()
14+
&& data.get("scanID").is_some()
15+
} else {
16+
false
17+
}
18+
}
19+
20+
fn parse(&self, input: &str) -> Option<ParseResult> {
21+
debug("Detected checkmarx cli schema");
22+
23+
let data: Value = match serde_json::from_str(input) {
24+
Ok(data) => data,
25+
Err(_) => return None,
26+
};
27+
28+
let mut paths = Vec::new();
29+
if let Some(results) = data.get("results").and_then(|v| v.as_array()) {
30+
for result in results {
31+
if let Some(data) = result.get("data") {
32+
if let Some(nodes) = data.get("nodes").and_then(|v| v.as_array()) {
33+
for node in nodes {
34+
if let Some(path) = node.get("fileName") {
35+
if let Some(truncated_path) = path.as_str() {
36+
paths.push(truncated_path.get(1..).unwrap_or("").to_string());
37+
}
38+
}
39+
}
40+
}
41+
}
42+
}
43+
}
44+
45+
Some(ParseResult {
46+
paths,
47+
scanner: "checkmarx".to_string(),
48+
})
49+
}
50+
51+
fn scanner_name(&self) -> &str {
52+
"checkmarx-cli"
53+
}
54+
}
55+
56+
pub struct CheckmarxWebParser;
57+
58+
impl ScanParser for CheckmarxWebParser {
59+
fn detect(&self, input: &str) -> bool {
60+
if let Ok(data) = serde_json::from_str::<Value>(input) {
61+
data.get("scanResults").is_some() && data.get("reportId").is_some()
62+
} else {
63+
false
64+
}
65+
}
66+
67+
fn parse(&self, input: &str) -> Option<ParseResult> {
68+
debug("Detected checkmarx web schema");
69+
70+
let data: Value = match serde_json::from_str(input) {
71+
Ok(data) => data,
72+
Err(_) => return None,
73+
};
74+
75+
let mut paths = Vec::new();
76+
if let Some(scan_results) = data.get("scanResults") {
77+
if let Some(sast) = scan_results.get("sast") {
78+
if let Some(languages) = sast.get("languages").and_then(|v| v.as_array()) {
79+
for language in languages {
80+
if let Some(queries) = language.get("queries").and_then(|v| v.as_array()) {
81+
for query in queries {
82+
if let Some(vulns) = query.get("vulnerabilities").and_then(|v| v.as_array()) {
83+
for vuln in vulns {
84+
if let Some(nodes) = vuln.get("nodes").and_then(|v| v.as_array()) {
85+
for node in nodes {
86+
if let Some(path) = node.get("fileName") {
87+
if let Some(truncated_path) = path.as_str() {
88+
paths.push(truncated_path.get(1..).unwrap_or("").to_string());
89+
}
90+
}
91+
}
92+
}
93+
}
94+
}
95+
}
96+
}
97+
}
98+
}
99+
}
100+
}
101+
102+
Some(ParseResult {
103+
paths,
104+
scanner: "checkmarx".to_string(),
105+
})
106+
}
107+
108+
fn scanner_name(&self) -> &str {
109+
"checkmarx-web"
110+
}
111+
}
112+
113+
pub struct CheckmarxXmlParser;
114+
115+
impl CheckmarxXmlParser {
116+
fn parse_xml_content(&self, input: &str) -> Option<ParseResult> {
117+
debug("Detected checkmarx xml schema");
118+
let mut paths = Vec::new();
119+
let mut reader = Reader::from_str(input);
120+
121+
let mut buf = Vec::new();
122+
123+
loop {
124+
match reader.read_event_into(&mut buf) {
125+
Ok(Event::Start(ref e)) | Ok(Event::Empty(ref e)) => {
126+
if e.name().as_ref() == b"Result" {
127+
for attr in e.attributes() {
128+
if let Ok(attr) = attr {
129+
if attr.key.as_ref() == b"FileName" {
130+
if let Ok(file_name) = std::str::from_utf8(&attr.value) {
131+
let clean_path = file_name.trim_start_matches('/').trim_start_matches('\\');
132+
if !clean_path.is_empty() {
133+
paths.push(clean_path.to_string());
134+
}
135+
}
136+
}
137+
}
138+
}
139+
}
140+
}
141+
Ok(Event::Eof) => break,
142+
Err(e) => {
143+
eprintln!("Error parsing XML: {}", e);
144+
return None;
145+
}
146+
_ => {}
147+
}
148+
buf.clear();
149+
}
150+
151+
Some(ParseResult {
152+
paths,
153+
scanner: "checkmarx".to_string(),
154+
})
155+
}
156+
}
157+
158+
impl ScanParser for CheckmarxXmlParser {
159+
fn detect(&self, input: &str) -> bool {
160+
input.trim().starts_with("<?xml") && input.contains("<CxXMLResults")
161+
}
162+
163+
fn parse(&self, input: &str) -> Option<ParseResult> {
164+
self.parse_xml_content(input)
165+
}
166+
167+
fn scanner_name(&self) -> &str {
168+
"checkmarx-xml"
169+
}
170+
}
171+
172+

src/scanners/parsers/mod.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
2+
3+
#[derive(Debug)]
4+
pub struct ParseResult {
5+
pub paths: Vec<String>,
6+
pub scanner: String,
7+
}
8+
9+
pub trait ScanParser {
10+
fn detect(&self, input: &str) -> bool;
11+
12+
fn parse(&self, input: &str) -> Option<ParseResult>;
13+
14+
#[allow(dead_code)]
15+
fn scanner_name(&self) -> &str;
16+
}
17+
18+
pub struct ScanParserFactory {
19+
parsers: Vec<Box<dyn ScanParser>>,
20+
}
21+
22+
impl ScanParserFactory {
23+
pub fn new() -> Self {
24+
let parsers: Vec<Box<dyn ScanParser>> = vec![
25+
Box::new(semgrep::SemgrepParser),
26+
Box::new(sarif::SarifParser),
27+
Box::new(checkmarx::CheckmarxCliParser),
28+
Box::new(checkmarx::CheckmarxWebParser),
29+
Box::new(checkmarx::CheckmarxXmlParser),
30+
];
31+
32+
Self { parsers }
33+
}
34+
35+
#[allow(dead_code)]
36+
pub fn find_parser(&self, input: &str) -> Option<&Box<dyn ScanParser>> {
37+
self.parsers.iter().find(|parser| parser.detect(input))
38+
}
39+
40+
pub fn parse_scan_data(&self, input: &str) -> Result<ParseResult, String> {
41+
for parser in &self.parsers {
42+
if parser.detect(input) {
43+
match parser.parse(input) {
44+
Some(result) => return Ok(result),
45+
None => continue,
46+
}
47+
}
48+
}
49+
50+
crate::log::debug("Couldn't detect what kind of report this is.");
51+
Err("Unsupported scan report format. Please check if your scanner is supported. Supported formats: JSON (Semgrep, SARIF, Checkmarx), XML (Checkmarx).".to_string())
52+
}
53+
}
54+
55+
pub mod semgrep;
56+
pub mod sarif;
57+
pub mod checkmarx;
58+
59+

0 commit comments

Comments
 (0)