Skip to main content

jsdet_cli/
sarif.rs

1/// SARIF output — Static Analysis Results Interchange Format.
2///
3/// SARIF is the industry standard for security findings. GitHub Code Scanning,
4/// VS Code, Semgrep, CodeQL all read SARIF. By outputting SARIF, jsdet
5/// integrates into every existing security workflow.
6///
7use serde_json::{Value, json};
8
9/// A security finding to include in the SARIF report.
10#[derive(Debug, Clone)]
11pub struct Finding {
12    pub rule_id: String,
13    pub level: Level,
14    pub message: String,
15    pub file: String,
16    pub line: usize,
17    pub cwe: Option<String>,
18    /// The taint flow from source to sink.
19    pub flow: Vec<FlowStep>,
20}
21
22/// Severity level for SARIF.
23#[derive(Debug, Clone, Copy)]
24pub enum Level {
25    Error,
26    Warning,
27    Note,
28}
29
30impl Level {
31    pub fn as_str(self) -> &'static str {
32        match self {
33            Self::Error => "error",
34            Self::Warning => "warning",
35            Self::Note => "note",
36        }
37    }
38}
39
40/// One step in a taint flow trace.
41#[derive(Debug, Clone)]
42pub struct FlowStep {
43    pub file: String,
44    pub line: usize,
45    pub message: String,
46}
47
48/// Generate a complete SARIF JSON document.
49pub fn generate_sarif(tool_name: &str, tool_version: &str, findings: &[Finding]) -> Value {
50    let rules: Vec<Value> = findings
51        .iter()
52        .map(|f| &f.rule_id)
53        .collect::<std::collections::HashSet<_>>()
54        .into_iter()
55        .map(|id| {
56            let cwe = findings
57                .iter()
58                .find(|f| f.rule_id == *id)
59                .and_then(|f| f.cwe.as_ref());
60
61            let mut rule = json!({
62                "id": id,
63                "shortDescription": {
64                    "text": id
65                }
66            });
67
68            if let Some(cwe_id) = cwe {
69                rule["properties"] = json!({
70                    "tags": [cwe_id]
71                });
72            }
73
74            rule
75        })
76        .collect();
77
78    let results: Vec<Value> = findings
79        .iter()
80        .map(|f| {
81            let mut result = json!({
82                "ruleId": f.rule_id,
83                "level": f.level.as_str(),
84                "message": {
85                    "text": f.message
86                },
87                "locations": [{
88                    "physicalLocation": {
89                        "artifactLocation": {
90                            "uri": f.file
91                        },
92                        "region": {
93                            "startLine": f.line
94                        }
95                    }
96                }]
97            });
98
99            // Add taint flow if present.
100            if !f.flow.is_empty() {
101                let thread_flows: Vec<Value> = f
102                    .flow
103                    .iter()
104                    .map(|step| {
105                        json!({
106                            "location": {
107                                "physicalLocation": {
108                                    "artifactLocation": {
109                                        "uri": step.file
110                                    },
111                                    "region": {
112                                        "startLine": step.line
113                                    }
114                                },
115                                "message": {
116                                    "text": step.message
117                                }
118                            }
119                        })
120                    })
121                    .collect();
122
123                result["codeFlows"] = json!([{
124                    "threadFlows": [{
125                        "locations": thread_flows
126                    }]
127                }]);
128            }
129
130            result
131        })
132        .collect();
133
134    json!({
135        "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
136        "version": "2.1.0",
137        "runs": [{
138            "tool": {
139                "driver": {
140                    "name": tool_name,
141                    "version": tool_version,
142                    "informationUri": "https://github.com/santh-security/jsdet",
143                    "rules": rules
144                }
145            },
146            "results": results
147        }]
148    })
149}