|
| 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