use anyhow::Result;
use serde_json::{Value, json};
use crate::engine::{ReviewIssue, ReviewResponse, ScanResponse, Severity};
use crate::formatters::Formatter;
pub struct SarifFormatter;
impl Formatter for SarifFormatter {
fn format_review(&self, response: &ReviewResponse) -> Result<String> {
let sarif = build_sarif(&response.issues);
let json = serde_json::to_string_pretty(&sarif)?;
Ok(json)
}
fn format_scan(&self, response: &ScanResponse) -> Result<String> {
let sarif = build_sarif(&response.issues);
let json = serde_json::to_string_pretty(&sarif)?;
Ok(json)
}
}
fn build_sarif(issues: &[ReviewIssue]) -> Value {
let mut rule_map = serde_json::Map::new();
for issue in issues {
let rule_id = issue
.issue_type
.clone()
.unwrap_or_else(|| "unknown".to_string());
if !rule_map.contains_key(&rule_id) {
rule_map.insert(
rule_id.clone(),
json!({
"id": rule_id,
"shortDescription": {
"text": issue.title.clone()
},
"fullDescription": {
"text": issue.body.clone()
},
"defaultConfiguration": {
"level": severity_to_sarif_level(&issue.severity)
}
}),
);
}
}
let rules: Vec<Value> = rule_map.into_values().collect();
let results: Vec<Value> = issues
.iter()
.map(|issue| {
let mut result = json!({
"ruleId": issue.issue_type.clone().unwrap_or_else(|| "unknown".to_string()),
"level": severity_to_sarif_level(&issue.severity),
"message": {
"text": issue.body.clone()
},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": issue.file.clone()
}
}
}]
});
if let Some(ref id) = issue.rule_id {
result["properties"] = json!({ "coraRuleId": id });
}
if let Some(line) = issue.line {
result["locations"][0]["physicalLocation"]["region"] = json!({
"startLine": line
});
}
if let Some(ref fix) = issue.suggested_fix {
result["fixes"] = json!([{
"description": {
"text": fix.clone()
},
"artifactChanges": [{
"artifactLocation": {
"uri": issue.file.clone()
},
"replacements": [{
"deletedRegion": {
"startLine": issue.line.unwrap_or(1)
},
"insertedContent": {
"text": fix.clone()
}
}]
}]
}]);
}
result
})
.collect();
let invocations = if issues.is_empty() {
json!([])
} else {
json!([{
"executionSuccessful": true,
"properties": {
"cora.watermark": format!("Reviewed by CodeCora v{}", env!("CARGO_PKG_VERSION"))
}
}])
};
json!({
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "CodeCora",
"fullName": "codecoradev/cora-code",
"version": env!("CARGO_PKG_VERSION"),
"informationUri": env!("CARGO_PKG_REPOSITORY"),
"rules": rules
}
},
"results": results,
"invocations": invocations
}]
})
}
fn severity_to_sarif_level(severity: &Severity) -> &str {
match severity {
Severity::Critical | Severity::Major => "error",
Severity::Minor => "warning",
Severity::Info => "note",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::{ReviewIssue, Severity};
fn sample_issue() -> ReviewIssue {
ReviewIssue::new("src/main.rs", Some(42), Severity::Critical, "SQL Injection")
.with_type("security")
.with_body("User input concatenated into query.")
.with_fix("Use parameterized queries.")
}
fn sample_response() -> ReviewResponse {
ReviewResponse {
issues: vec![sample_issue()],
summary: "Found 1 critical issue.".to_string(),
tokens_used: None,
should_block: false,
}
}
fn empty_response() -> ReviewResponse {
ReviewResponse {
issues: vec![],
summary: String::new(),
tokens_used: None,
should_block: false,
}
}
#[test]
fn sarif_output_is_valid_json() {
let fmt = SarifFormatter;
let output = fmt.format_review(&sample_response()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert!(parsed.is_object());
}
#[test]
fn sarif_has_required_schema_field() {
let fmt = SarifFormatter;
let output = fmt.format_review(&sample_response()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert!(parsed["$schema"].as_str().unwrap().contains("sarif-schema"));
}
#[test]
fn sarif_has_version() {
let fmt = SarifFormatter;
let output = fmt.format_review(&sample_response()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["version"].as_str().unwrap(), "2.1.0");
}
#[test]
fn sarif_has_runs() {
let fmt = SarifFormatter;
let output = fmt.format_review(&sample_response()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
let runs = parsed["runs"].as_array().unwrap();
assert_eq!(runs.len(), 1);
}
#[test]
fn sarif_tool_driver_name_is_codecora() {
let fmt = SarifFormatter;
let output = fmt.format_review(&sample_response()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(
parsed["runs"][0]["tool"]["driver"]["name"]
.as_str()
.unwrap(),
"CodeCora"
);
}
#[test]
fn sarif_tool_driver_has_fullname_codecoradev() {
let fmt = SarifFormatter;
let output = fmt.format_review(&sample_response()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(
parsed["runs"][0]["tool"]["driver"]["fullName"]
.as_str()
.unwrap(),
"codecoradev/cora-code"
);
}
#[test]
fn sarif_results_contain_issue() {
let fmt = SarifFormatter;
let output = fmt.format_review(&sample_response()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
let results = parsed["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0]["level"].as_str().unwrap(), "error"); }
#[test]
fn sarif_empty_issues_produces_valid_output() {
let fmt = SarifFormatter;
let output = fmt.format_review(&empty_response()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
let results = parsed["runs"][0]["results"].as_array().unwrap();
assert!(results.is_empty());
let invocations = parsed["runs"][0]["invocations"].as_array().unwrap();
assert!(invocations.is_empty());
}
#[test]
fn sarif_with_issues_has_watermark_invocation() {
let fmt = SarifFormatter;
let output = fmt.format_review(&sample_response()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
let invocations = parsed["runs"][0]["invocations"].as_array().unwrap();
assert_eq!(invocations.len(), 1);
let wm = &invocations[0]["properties"]["cora.watermark"];
let wm_str = wm.as_str().unwrap();
assert!(wm_str.contains("Cora"));
assert!(wm_str.contains('v'));
}
#[test]
fn sarif_location_includes_line() {
let fmt = SarifFormatter;
let output = fmt.format_review(&sample_response()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
let region = &parsed["runs"][0]["results"][0]["locations"][0]["physicalLocation"]["region"];
assert_eq!(region["startLine"].as_u64().unwrap(), 42);
}
#[test]
fn sarif_scan_format_also_valid() {
let fmt = SarifFormatter;
let scan = ScanResponse {
issues: vec![sample_issue()],
summary: "Done.".to_string(),
files_scanned: 5,
lines_scanned: 200,
tokens_used: None,
should_block: false,
};
let output = fmt.format_scan(&scan).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert!(parsed["$schema"].as_str().is_some());
assert_eq!(parsed["runs"][0]["results"].as_array().unwrap().len(), 1);
}
#[test]
fn sarif_severity_to_level_critical_is_error() {
assert_eq!(severity_to_sarif_level(&Severity::Critical), "error");
}
#[test]
fn sarif_severity_to_level_major_is_error() {
assert_eq!(severity_to_sarif_level(&Severity::Major), "error");
}
#[test]
fn sarif_severity_to_level_minor_is_warning() {
assert_eq!(severity_to_sarif_level(&Severity::Minor), "warning");
}
#[test]
fn sarif_severity_to_level_info_is_note() {
assert_eq!(severity_to_sarif_level(&Severity::Info), "note");
}
#[test]
fn sarif_issue_without_line_has_no_region() {
let issue = ReviewIssue::new("src/lib.rs", None, Severity::Info, "No line")
.with_body("No line info.");
let response = ReviewResponse {
issues: vec![issue],
summary: String::new(),
tokens_used: None,
should_block: false,
};
let fmt = SarifFormatter;
let output = fmt.format_review(&response).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
let result = &parsed["runs"][0]["results"][0];
let loc = &result["locations"][0]["physicalLocation"];
assert!(loc.get("region").is_none());
}
#[test]
fn sarif_issue_with_fix() {
let fmt = SarifFormatter;
let output = fmt.format_review(&sample_response()).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
let result = &parsed["runs"][0]["results"][0];
assert!(result.get("fixes").is_some());
}
}