Skip to content

Commit 6163c0e

Browse files
committed
writer-json-sarif: separate module for SarifTreeEncoder
Resolves: #115
1 parent 81638f0 commit 6163c0e

File tree

4 files changed

+418
-370
lines changed

4 files changed

+418
-370
lines changed

Diff for: src/lib/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ add_library(cs STATIC
4545
writer-html.cc
4646
writer-json.cc
4747
writer-json-common.cc
48+
writer-json-sarif.cc
4849
writer-json-simple.cc
4950
)

Diff for: src/lib/writer-json-sarif.cc

+357
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
/*
2+
* Copyright (C) 2011 - 2023 Red Hat, Inc.
3+
*
4+
* This file is part of csdiff.
5+
*
6+
* csdiff is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* any later version.
10+
*
11+
* csdiff is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with csdiff. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
#include "writer-json-sarif.hh"
21+
22+
#include "regex.hh"
23+
#include "version.hh"
24+
#include "writer-json-common.hh"
25+
26+
using namespace boost::json;
27+
28+
void SarifTreeEncoder::initToolVersion()
29+
{
30+
std::string tool;
31+
auto it = scanProps_.find("tool");
32+
if (scanProps_.end() != it)
33+
// read "tool" scan property
34+
tool = it->second;
35+
36+
std::string ver;
37+
it = scanProps_.find("tool-version");
38+
if (scanProps_.end() != it) {
39+
// read "tool-version" scan property
40+
ver = it->second;
41+
42+
if (tool.empty()) {
43+
// try to split the "{tool}-{version}" string by the last '-'
44+
const size_t lastDashAt = ver.rfind('-');
45+
if (std::string::npos != lastDashAt) {
46+
// read tool from the "{tool}-{version}" string
47+
tool = ver.substr(0, lastDashAt);
48+
49+
// remove "{tool}-" from "{tool}-{version}"
50+
ver.erase(0U, lastDashAt);
51+
}
52+
}
53+
else {
54+
// try to find "{tool}-" prefix in the "tool-version" scan property
55+
const std::string prefix = tool + "-";
56+
if (0U == ver.find(prefix))
57+
ver.erase(0U, prefix.size());
58+
}
59+
}
60+
61+
std::string uri;
62+
if (tool.empty()) {
63+
// unable to read tool name --> fallback to csdiff as the tool
64+
tool = "csdiff";
65+
ver = CS_VERSION;
66+
uri = "https://github.com/csutils/csdiff";
67+
}
68+
else if (scanProps_.end() != (it = scanProps_.find("tool-url")))
69+
// read "tool-url" scan property
70+
uri = it->second;
71+
72+
driver_["name"] = std::move(tool);
73+
74+
if (!ver.empty())
75+
driver_["version"] = std::move(ver);
76+
77+
if (!uri.empty())
78+
driver_["informationUri"] = std::move(uri);
79+
}
80+
81+
static void sarifEncodeShellCheckRule(object *rule, const std::string &ruleID)
82+
{
83+
// name
84+
rule->emplace("name", ruleID);
85+
86+
// properties.tags[]
87+
object props = {
88+
{ "tags", { "ShellCheck" } }
89+
};
90+
rule->emplace("properties", std::move(props));
91+
92+
// help.text && help.markdown
93+
auto helpURI = "https://github.com/koalaman/shellcheck/wiki/" + ruleID;
94+
auto helpMarkdown = "Defect reference: [" + ruleID +"](" + helpURI + ")";
95+
96+
object help = {
97+
{ "text", "Defect reference: " + helpURI },
98+
{ "markdown", std::move(helpMarkdown) }
99+
};
100+
101+
rule->emplace("help", std::move(help));
102+
}
103+
104+
static void sarifEncodeCweRule(object *rule, const int cwe, bool append = false)
105+
{
106+
auto cweStr = std::to_string(cwe);
107+
array cweList = { "CWE-" + cweStr };
108+
109+
// properties.cwe[]
110+
if (append) {
111+
object &props = rule->at("properties").as_object();
112+
props["cwe"] = std::move(cweList);
113+
} else {
114+
object props = {
115+
{ "cwe", std::move(cweList) }
116+
};
117+
rule->emplace("properties", std::move(props));
118+
}
119+
120+
// help.text
121+
auto helpText =
122+
"https://cwe.mitre.org/data/definitions/" + cweStr + ".html";
123+
124+
if (append) {
125+
object &help = rule->at("help").as_object();
126+
help["text"].as_string() += '\n' + std::move(helpText);
127+
} else {
128+
object help = {
129+
{ "text", std::move(helpText) }
130+
};
131+
rule->emplace("help", help);
132+
}
133+
}
134+
135+
void SarifTreeEncoder::serializeRules()
136+
{
137+
array ruleList;
138+
for (const auto &item : shellCheckMap_) {
139+
const auto &id = item.first;
140+
object rule = {
141+
{ "id", id }
142+
};
143+
144+
sarifEncodeShellCheckRule(&rule, item.second);
145+
if (1U == cweMap_.count(id))
146+
sarifEncodeCweRule(&rule, cweMap_[id], /*append =*/ true);
147+
148+
ruleList.push_back(std::move(rule));
149+
}
150+
151+
for (const auto &item : cweMap_) {
152+
const auto &id = item.first;
153+
if (1U == shellCheckMap_.count(id))
154+
continue;
155+
156+
object rule = {
157+
{ "id", id }
158+
};
159+
160+
sarifEncodeCweRule(&rule, item.second);
161+
ruleList.push_back(std::move(rule));
162+
}
163+
164+
driver_["rules"] = std::move(ruleList);
165+
}
166+
167+
void SarifTreeEncoder::importScanProps(const TScanProps &scanProps)
168+
{
169+
scanProps_ = scanProps;
170+
}
171+
172+
static void sarifEncodeMsg(object *pDst, const std::string& text)
173+
{
174+
object message = {
175+
{ "text", sanitizeUTF8(text) }
176+
};
177+
178+
pDst->emplace("message", std::move(message) );
179+
}
180+
181+
static void sarifEncodeLevel(object *result, const std::string &event)
182+
{
183+
std::string level = event;
184+
185+
// cut the [...] suffix from event if present
186+
size_t pos = event.find('[');
187+
if (std::string::npos != pos)
188+
level = event.substr(0U, pos);
189+
190+
// go through events that denote warning level
191+
for (const char *str : {"error", "warning", "note"}) {
192+
if (str == level) {
193+
// encode in the output if matched
194+
result->emplace("level", std::move(level));
195+
return;
196+
}
197+
}
198+
}
199+
200+
static void sarifEncodeLoc(object *pLoc, const Defect &def, unsigned idx)
201+
{
202+
// location ID within the result
203+
pLoc->emplace("id", idx);
204+
205+
const DefEvent &evt = def.events[idx];
206+
207+
// file name
208+
object locPhy = {
209+
{ "artifactLocation", {
210+
{ "uri", evt.fileName }
211+
}}
212+
};
213+
214+
// line/col
215+
if (evt.line) {
216+
object reg = {
217+
{ "startLine", evt.line }
218+
};
219+
220+
if (evt.column)
221+
reg["startColumn"] = evt.column;
222+
223+
locPhy["region"] = std::move(reg);
224+
}
225+
226+
// location
227+
pLoc->emplace("physicalLocation", std::move(locPhy));
228+
}
229+
230+
static void sarifEncodeComment(array *pDst, const Defect &def, unsigned idx)
231+
{
232+
object comment;
233+
234+
// needed for Github to see the SARIF data as valid
235+
sarifEncodeLoc(&comment, def, idx);
236+
237+
sarifEncodeMsg(&comment, def.events[idx].msg);
238+
pDst->push_back(std::move(comment));
239+
}
240+
241+
static void sarifEncodeEvt(array *pDst, const Defect &def, unsigned idx)
242+
{
243+
const DefEvent &evt = def.events[idx];
244+
245+
// location + message
246+
object loc;
247+
sarifEncodeLoc(&loc, def, idx);
248+
sarifEncodeMsg(&loc, evt.msg);
249+
250+
// threadFlowLocation
251+
object tfLoc = {
252+
{ "location", std::move(loc) },
253+
// verbosityLevel
254+
{ "nestingLevel", evt.verbosityLevel },
255+
// event
256+
{ "kinds", { evt.event } }
257+
};
258+
259+
// append the threadFlowLocation object to the destination array
260+
pDst->push_back(std::move(tfLoc));
261+
}
262+
263+
void SarifTreeEncoder::appendDef(const Defect &def)
264+
{
265+
const DefEvent &keyEvt = def.events[def.keyEventIdx];
266+
object result;
267+
268+
// checker (FIXME: suboptimal mapping to SARIF)
269+
const std::string ruleId = def.checker + ": " + keyEvt.event;
270+
result["ruleId"] = ruleId;
271+
272+
if (def.checker == "SHELLCHECK_WARNING") {
273+
boost::smatch sm;
274+
static const RE reShellCheckMsg("(\\[)?(SC[0-9]+)(\\])?$");
275+
boost::regex_search(keyEvt.event, sm, reShellCheckMsg);
276+
277+
// update ShellCheck rule map
278+
shellCheckMap_[ruleId] = sm[2];
279+
}
280+
281+
if (def.cwe)
282+
// update CWE map
283+
cweMap_[ruleId] = def.cwe;
284+
285+
// key event severity level
286+
sarifEncodeLevel(&result, keyEvt.event);
287+
288+
// key event location
289+
object loc;
290+
sarifEncodeLoc(&loc, def, def.keyEventIdx);
291+
result["locations"] = array{std::move(loc)};
292+
293+
// key msg
294+
sarifEncodeMsg(&result, keyEvt.msg);
295+
296+
// other events
297+
array flowLocs, relatedLocs;
298+
for (unsigned i = 0; i < def.events.size(); ++i) {
299+
if (def.events[i].event == "#")
300+
sarifEncodeComment(&relatedLocs, def, i);
301+
else
302+
sarifEncodeEvt(&flowLocs, def, i);
303+
}
304+
305+
// codeFlows
306+
result["codeFlows"] = {
307+
// threadFlows
308+
{{ "threadFlows", {
309+
// locations
310+
{{ "locations", std::move(flowLocs) }}
311+
}}}
312+
};
313+
314+
if (!relatedLocs.empty())
315+
// our stash for comments
316+
result["relatedLocations"] = std::move(relatedLocs);
317+
318+
// append the `result` object to the `results` array
319+
results_.push_back(std::move(result));
320+
}
321+
322+
void SarifTreeEncoder::writeTo(std::ostream &str)
323+
{
324+
object root = {
325+
// mandatory: schema/version
326+
{ "$schema", "https://json.schemastore.org/sarif-2.1.0.json" },
327+
{ "version", "2.1.0" }
328+
};
329+
330+
if (!scanProps_.empty()) {
331+
// scan props
332+
root["inlineExternalProperties"] = {
333+
{{ "externalizedProperties", jsonSerializeScanProps(scanProps_) }}
334+
};
335+
}
336+
337+
this->initToolVersion();
338+
339+
if (!cweMap_.empty() || !shellCheckMap_.empty())
340+
// needs to run before we pick driver_
341+
this->serializeRules();
342+
343+
object run0 = {
344+
{ "tool", {
345+
{ "driver", std::move(driver_) }
346+
}}
347+
};
348+
349+
// results
350+
run0["results"] = std::move(results_);
351+
352+
// mandatory: runs
353+
root["runs"] = array{std::move(run0)};
354+
355+
// encode as JSON
356+
jsonPrettyPrint(str, root);
357+
}

0 commit comments

Comments
 (0)