diff --git a/AUTHORS b/AUTHORS index ce58094baec..3f784a8e052 100644 --- a/AUTHORS +++ b/AUTHORS @@ -411,6 +411,7 @@ Tommy Bergman Toralf Förster Troshin V.S. Tyson Nottingham +Usman Majid Valentin Batz Valerii Lashmanov Vasily Maslyukov diff --git a/Makefile b/Makefile index 5f47959f60e..22cff7582a6 100644 --- a/Makefile +++ b/Makefile @@ -176,7 +176,7 @@ ifndef INCLUDE_FOR_CLI endif ifndef INCLUDE_FOR_TEST - INCLUDE_FOR_TEST=-Ilib -Ifrontend -Icli -isystem externals/simplecpp -isystem externals/tinyxml2 + INCLUDE_FOR_TEST=-Ilib -Ifrontend -Icli -isystem externals/picojson -isystem externals/simplecpp -isystem externals/tinyxml2 endif ifndef CFLAGS_FOR_TEST @@ -249,6 +249,7 @@ LIBOBJ = $(libcppdir)/valueflow.o \ $(libcppdir)/programmemory.o \ $(libcppdir)/regex.o \ $(libcppdir)/reverseanalyzer.o \ + $(libcppdir)/sarifreport.o \ $(libcppdir)/settings.o \ $(libcppdir)/standards.o \ $(libcppdir)/summaries.o \ @@ -327,6 +328,7 @@ TESTOBJ = test/fixture.o \ test/testprocessexecutor.o \ test/testprogrammemory.o \ test/testregex.o \ + test/testsarifreport.o \ test/testsettings.o \ test/testsimplifytemplate.o \ test/testsimplifytokens.o \ @@ -638,6 +640,9 @@ $(libcppdir)/regex.o: lib/regex.cpp lib/config.h lib/regex.h $(libcppdir)/reverseanalyzer.o: lib/reverseanalyzer.cpp lib/addoninfo.h lib/analyzer.h lib/astutils.h lib/checkers.h lib/config.h lib/errortypes.h lib/forwardanalyzer.h lib/library.h lib/mathlib.h lib/platform.h lib/reverseanalyzer.h lib/settings.h lib/smallvector.h lib/sourcelocation.h lib/standards.h lib/symboldatabase.h lib/templatesimplifier.h lib/token.h lib/utils.h lib/valueptr.h lib/vfvalue.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/reverseanalyzer.cpp +$(libcppdir)/sarifreport.o: lib/sarifreport.cpp externals/picojson/picojson.h lib/addoninfo.h lib/check.h lib/checkers.h lib/config.h lib/cppcheck.h lib/errorlogger.h lib/errortypes.h lib/json.h lib/library.h lib/mathlib.h lib/platform.h lib/sarifreport.h lib/settings.h lib/standards.h lib/utils.h + $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/sarifreport.cpp + $(libcppdir)/settings.o: lib/settings.cpp externals/picojson/picojson.h lib/addoninfo.h lib/checkers.h lib/config.h lib/errortypes.h lib/json.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/summaries.h lib/suppressions.h lib/utils.h lib/vfvalue.h $(CXX) ${INCLUDE_FOR_LIB} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/settings.cpp @@ -683,7 +688,7 @@ frontend/frontend.o: frontend/frontend.cpp frontend/frontend.h lib/addoninfo.h l cli/cmdlineparser.o: cli/cmdlineparser.cpp cli/cmdlinelogger.h cli/cmdlineparser.h cli/filelister.h externals/tinyxml2/tinyxml2.h lib/addoninfo.h lib/check.h lib/checkers.h lib/color.h lib/config.h lib/cppcheck.h lib/errorlogger.h lib/errortypes.h lib/filesettings.h lib/importproject.h lib/library.h lib/mathlib.h lib/path.h lib/pathmatch.h lib/platform.h lib/regex.h lib/settings.h lib/standards.h lib/suppressions.h lib/timer.h lib/utils.h lib/xml.h $(CXX) ${INCLUDE_FOR_CLI} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ cli/cmdlineparser.cpp -cli/cppcheckexecutor.o: cli/cppcheckexecutor.cpp cli/cmdlinelogger.h cli/cmdlineparser.h cli/cppcheckexecutor.h cli/executor.h cli/processexecutor.h cli/sehwrapper.h cli/signalhandler.h cli/singleexecutor.h cli/threadexecutor.h externals/picojson/picojson.h lib/addoninfo.h lib/analyzerinfo.h lib/check.h lib/checkers.h lib/checkersreport.h lib/color.h lib/config.h lib/cppcheck.h lib/errorlogger.h lib/errortypes.h lib/filesettings.h lib/json.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/utils.h +cli/cppcheckexecutor.o: cli/cppcheckexecutor.cpp cli/cmdlinelogger.h cli/cmdlineparser.h cli/cppcheckexecutor.h cli/executor.h cli/processexecutor.h cli/sehwrapper.h cli/signalhandler.h cli/singleexecutor.h cli/threadexecutor.h externals/picojson/picojson.h lib/addoninfo.h lib/analyzerinfo.h lib/check.h lib/checkers.h lib/checkersreport.h lib/color.h lib/config.h lib/cppcheck.h lib/errorlogger.h lib/errortypes.h lib/filesettings.h lib/json.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/sarifreport.h lib/settings.h lib/standards.h lib/suppressions.h lib/utils.h $(CXX) ${INCLUDE_FOR_CLI} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ cli/cppcheckexecutor.cpp cli/executor.o: cli/executor.cpp cli/executor.h lib/addoninfo.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/utils.h @@ -854,6 +859,9 @@ test/testprogrammemory.o: test/testprogrammemory.cpp lib/addoninfo.h lib/check.h test/testregex.o: test/testregex.cpp lib/addoninfo.h lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/platform.h lib/regex.h lib/settings.h lib/standards.h lib/utils.h test/fixture.h $(CXX) ${INCLUDE_FOR_TEST} ${CFLAGS_FOR_TEST} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ test/testregex.cpp +test/testsarifreport.o: test/testsarifreport.cpp externals/picojson/picojson.h lib/addoninfo.h lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/json.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/sarifreport.h lib/settings.h lib/standards.h lib/tokenize.h lib/tokenlist.h lib/utils.h test/fixture.h test/helpers.h + $(CXX) ${INCLUDE_FOR_TEST} ${CFLAGS_FOR_TEST} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ test/testsarifreport.cpp + test/testsettings.o: test/testsettings.cpp lib/addoninfo.h lib/check.h lib/checkers.h lib/color.h lib/config.h lib/errorlogger.h lib/errortypes.h lib/library.h lib/mathlib.h lib/path.h lib/platform.h lib/settings.h lib/standards.h lib/suppressions.h lib/tokenize.h lib/tokenlist.h lib/utils.h test/fixture.h test/helpers.h $(CXX) ${INCLUDE_FOR_TEST} ${CFLAGS_FOR_TEST} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ test/testsettings.cpp diff --git a/cli/cppcheckexecutor.cpp b/cli/cppcheckexecutor.cpp index 4b9809556b8..acac0b5f8cd 100644 --- a/cli/cppcheckexecutor.cpp +++ b/cli/cppcheckexecutor.cpp @@ -34,6 +34,7 @@ #include "filesettings.h" #include "json.h" #include "path.h" +#include "sarifreport.h" #include "settings.h" #include "singleexecutor.h" #include "suppressions.h" @@ -79,156 +80,6 @@ #endif namespace { - class SarifReport { - public: - void addFinding(ErrorMessage msg) { - mFindings.push_back(std::move(msg)); - } - - picojson::array serializeRules() const { - picojson::array ret; - std::set ruleIds; - for (const auto& finding : mFindings) { - // github only supports findings with locations - if (finding.callStack.empty()) - continue; - if (ruleIds.insert(finding.id).second) { - picojson::object rule; - rule["id"] = picojson::value(finding.id); - // rule.shortDescription.text - picojson::object shortDescription; - shortDescription["text"] = picojson::value(finding.shortMessage()); - rule["shortDescription"] = picojson::value(shortDescription); - // rule.fullDescription.text - picojson::object fullDescription; - fullDescription["text"] = picojson::value(finding.verboseMessage()); - rule["fullDescription"] = picojson::value(fullDescription); - // rule.help.text - picojson::object help; - help["text"] = picojson::value(finding.verboseMessage()); // FIXME provide proper help text - rule["help"] = picojson::value(help); - // rule.properties.precision, rule.properties.problem.severity - picojson::object properties; - properties["precision"] = picojson::value(sarifPrecision(finding)); - const char* securitySeverity = nullptr; - if (finding.severity == Severity::error && !ErrorLogger::isCriticalErrorId(finding.id)) - securitySeverity = "9.9"; // We see undefined behavior - //else if (finding.severity == Severity::warning) - // securitySeverity = 5.1; // We see potential undefined behavior - if (securitySeverity) { - properties["security-severity"] = picojson::value(securitySeverity); - const picojson::array tags{picojson::value("security")}; - properties["tags"] = picojson::value(tags); - } - rule["properties"] = picojson::value(properties); - // rule.defaultConfiguration.level - picojson::object defaultConfiguration; - defaultConfiguration["level"] = picojson::value(sarifSeverity(finding)); - rule["defaultConfiguration"] = picojson::value(defaultConfiguration); - - ret.emplace_back(rule); - } - } - return ret; - } - - static picojson::array serializeLocations(const ErrorMessage& finding) { - picojson::array ret; - for (const auto& location : finding.callStack) { - picojson::object physicalLocation; - picojson::object artifactLocation; - artifactLocation["uri"] = picojson::value(location.getfile(false)); - physicalLocation["artifactLocation"] = picojson::value(artifactLocation); - picojson::object region; - region["startLine"] = picojson::value(static_cast(location.line < 1 ? 1 : location.line)); - region["startColumn"] = picojson::value(static_cast(location.column < 1 ? 1 : location.column)); - region["endLine"] = region["startLine"]; - region["endColumn"] = region["startColumn"]; - physicalLocation["region"] = picojson::value(region); - picojson::object loc; - loc["physicalLocation"] = picojson::value(physicalLocation); - ret.emplace_back(loc); - } - return ret; - } - - picojson::array serializeResults() const { - picojson::array results; - for (const auto& finding : mFindings) { - // github only supports findings with locations - if (finding.callStack.empty()) - continue; - picojson::object res; - res["level"] = picojson::value(sarifSeverity(finding)); - res["locations"] = picojson::value(serializeLocations(finding)); - picojson::object message; - message["text"] = picojson::value(finding.shortMessage()); - res["message"] = picojson::value(message); - res["ruleId"] = picojson::value(finding.id); - results.emplace_back(res); - } - return results; - } - - picojson::value serializeRuns(const std::string& productName, const std::string& version) const { - picojson::object driver; - driver["name"] = picojson::value(productName); - driver["semanticVersion"] = picojson::value(version); - driver["informationUri"] = picojson::value("https://cppcheck.sourceforge.io"); - driver["rules"] = picojson::value(serializeRules()); - picojson::object tool; - tool["driver"] = picojson::value(driver); - picojson::object run; - run["tool"] = picojson::value(tool); - run["results"] = picojson::value(serializeResults()); - picojson::array runs{picojson::value(run)}; - return picojson::value(runs); - } - - std::string serialize(std::string productName) const { - const auto nameAndVersion = Settings::getNameAndVersion(productName); - productName = nameAndVersion.first.empty() ? "Cppcheck" : nameAndVersion.first; - std::string version = nameAndVersion.first.empty() ? CppCheck::version() : nameAndVersion.second; - if (version.find(' ') != std::string::npos) - version.erase(version.find(' '), std::string::npos); - - picojson::object doc; - doc["version"] = picojson::value("2.1.0"); - doc["$schema"] = picojson::value("https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json"); - doc["runs"] = serializeRuns(productName, version); - - return picojson::value(doc).serialize(true); - } - private: - - static std::string sarifSeverity(const ErrorMessage& errmsg) { - if (ErrorLogger::isCriticalErrorId(errmsg.id)) - return "error"; - switch (errmsg.severity) { - case Severity::error: - case Severity::warning: - case Severity::style: - case Severity::portability: - case Severity::performance: - return "warning"; - case Severity::information: - case Severity::internal: - case Severity::debug: - case Severity::none: - return "note"; - } - return "note"; - } - - static std::string sarifPrecision(const ErrorMessage& errmsg) { - if (errmsg.certainty == Certainty::inconclusive) - return "medium"; - return "high"; - } - - std::vector mFindings; - }; - class CmdLineLoggerStd : public CmdLineLogger { public: @@ -712,18 +563,20 @@ void StdLogger::reportErr(const ErrorMessage &msg) msgCopy.classification = getClassification(msgCopy.guideline, mSettings.reportType); // TODO: there should be no need for verbose and default messages here - const std::string msgStr = msgCopy.toString(mSettings.verbose, mSettings.templateFormat, mSettings.templateLocation); + const std::string msgStr = + msgCopy.toString(mSettings.verbose, mSettings.templateFormat, mSettings.templateLocation); // Alert only about unique errors if (!mSettings.emitDuplicates && !mShownErrors.insert(msgStr).second) return; - if (mSettings.outputFormat == Settings::OutputFormat::sarif) + if (mSettings.outputFormat == Settings::OutputFormat::sarif) { mSarifReport.addFinding(std::move(msgCopy)); - else if (mSettings.outputFormat == Settings::OutputFormat::xml) + } else if (mSettings.outputFormat == Settings::OutputFormat::xml) { reportErr(msgCopy.toXML()); - else + } else { reportErr(msgStr); + } } /** diff --git a/lib/cppcheck.vcxproj b/lib/cppcheck.vcxproj index 6709b1bf5bd..78e9eaecd69 100644 --- a/lib/cppcheck.vcxproj +++ b/lib/cppcheck.vcxproj @@ -81,6 +81,7 @@ + @@ -158,6 +159,7 @@ + diff --git a/lib/sarifreport.cpp b/lib/sarifreport.cpp new file mode 100644 index 00000000000..08eddbf8198 --- /dev/null +++ b/lib/sarifreport.cpp @@ -0,0 +1,211 @@ +/* + * Cppcheck - A tool for static C/C++ code analysis + * Copyright (C) 2007-2025 Cppcheck team. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "sarifreport.h" +#include "errorlogger.h" +#include "errortypes.h" +#include "settings.h" +#include "cppcheck.h" + +#include +#include + +void SarifReport::addFinding(ErrorMessage msg) +{ + mFindings.push_back(std::move(msg)); +} + +picojson::array SarifReport::serializeRules() const +{ + picojson::array ret; + std::set ruleIds; + for (const auto& finding : mFindings) { + // github only supports findings with locations + if (finding.callStack.empty()) + continue; + if (ruleIds.insert(finding.id).second) { + // setting name and description to empty strings will make github default + // to the instance specific violation message and not rule description, + // this makes it so not all the violations have the same description. + picojson::object rule; + rule["id"] = picojson::value(finding.id); + // rule.name + rule["name"] = picojson::value(""); + // rule.shortDescription.text + picojson::object shortDescription; + shortDescription["text"] = picojson::value(""); + rule["shortDescription"] = picojson::value(shortDescription); + // rule.fullDescription.text + picojson::object fullDescription; + fullDescription["text"] = picojson::value(""); + rule["fullDescription"] = picojson::value(fullDescription); + // rule.help.text + picojson::object help; + help["text"] = picojson::value(""); + rule["help"] = picojson::value(help); + // rule.properties.precision, rule.properties.problem.severity + picojson::object properties; + properties["precision"] = picojson::value(sarifPrecision(finding)); + // rule.properties.security-severity, rule.properties.tags + picojson::array tags; + + // If we have a CWE ID, treat it as security-related (CWE is the authoritative source for security weaknesses) + if (finding.cwe.id > 0) { + double securitySeverity = 0; + if (finding.severity == Severity::error && !ErrorLogger::isCriticalErrorId(finding.id)) { + securitySeverity = 9.9; // critical = 9.0+ + } + else if (finding.severity == Severity::warning) { + securitySeverity = 8.5; // high = 7.0 to 8.9 + } + else if (finding.severity == Severity::performance || finding.severity == Severity::portability || + finding.severity == Severity::style) { + securitySeverity = 5.5; // medium = 4.0 to 6.9 + } + else if (finding.severity == Severity::information || finding.severity == Severity::internal || + finding.severity == Severity::debug || finding.severity == Severity::none) { + securitySeverity = 2.0; // low = 0.1 to 3.9 + } + if (securitySeverity > 0.0) { + std::ostringstream ss; + ss << securitySeverity; + properties["security-severity"] = picojson::value(ss.str()); + tags.emplace_back("external/cwe/cwe-" + std::to_string(finding.cwe.id)); + tags.emplace_back("security"); + } + } + + // Add tags array if it has any content + if (!tags.empty()) { + properties["tags"] = picojson::value(tags); + } + + // Set problem.severity for use with github + const std::string problemSeverity = sarifSeverity(finding); + properties["problem.severity"] = picojson::value(problemSeverity); + rule["properties"] = picojson::value(properties); + // rule.defaultConfiguration.level + picojson::object defaultConfiguration; + defaultConfiguration["level"] = picojson::value(sarifSeverity(finding)); + rule["defaultConfiguration"] = picojson::value(defaultConfiguration); + + ret.emplace_back(rule); + } + } + return ret; +} + +picojson::array SarifReport::serializeLocations(const ErrorMessage& finding) +{ + picojson::array ret; + for (const auto& location : finding.callStack) { + picojson::object physicalLocation; + picojson::object artifactLocation; + artifactLocation["uri"] = picojson::value(location.getfile(false)); + physicalLocation["artifactLocation"] = picojson::value(artifactLocation); + picojson::object region; + region["startLine"] = picojson::value(static_cast(location.line < 1 ? 1 : location.line)); + region["startColumn"] = picojson::value(static_cast(location.column < 1 ? 1 : location.column)); + region["endLine"] = region["startLine"]; + region["endColumn"] = region["startColumn"]; + physicalLocation["region"] = picojson::value(region); + picojson::object loc; + loc["physicalLocation"] = picojson::value(physicalLocation); + ret.emplace_back(loc); + } + return ret; +} + +picojson::array SarifReport::serializeResults() const +{ + picojson::array results; + for (const auto& finding : mFindings) { + // github only supports findings with locations + if (finding.callStack.empty()) + continue; + picojson::object res; + res["level"] = picojson::value(sarifSeverity(finding)); + res["locations"] = picojson::value(serializeLocations(finding)); + picojson::object message; + message["text"] = picojson::value(finding.shortMessage()); + res["message"] = picojson::value(message); + res["ruleId"] = picojson::value(finding.id); + results.emplace_back(res); + } + return results; +} + +picojson::value SarifReport::serializeRuns(const std::string& productName, const std::string& version) const +{ + picojson::object driver; + driver["name"] = picojson::value(productName); + driver["semanticVersion"] = picojson::value(version); + driver["informationUri"] = picojson::value("https://cppcheck.sourceforge.io"); + driver["rules"] = picojson::value(serializeRules()); + picojson::object tool; + tool["driver"] = picojson::value(driver); + picojson::object run; + run["tool"] = picojson::value(tool); + run["results"] = picojson::value(serializeResults()); + picojson::array runs{picojson::value(run)}; + return picojson::value(runs); +} + +std::string SarifReport::serialize(std::string productName) const +{ + const auto nameAndVersion = Settings::getNameAndVersion(productName); + productName = nameAndVersion.first.empty() ? "Cppcheck" : nameAndVersion.first; + std::string version = nameAndVersion.first.empty() ? CppCheck::version() : nameAndVersion.second; + if (version.find(' ') != std::string::npos) + version.erase(version.find(' '), std::string::npos); + + picojson::object doc; + doc["version"] = picojson::value("2.1.0"); + doc["$schema"] = picojson::value("https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json"); + doc["runs"] = serializeRuns(productName, version); + + return picojson::value(doc).serialize(true); +} + +std::string SarifReport::sarifSeverity(const ErrorMessage& errmsg) +{ + if (ErrorLogger::isCriticalErrorId(errmsg.id)) + return "error"; + switch (errmsg.severity) { + case Severity::error: + case Severity::warning: + return "error"; + case Severity::style: + case Severity::portability: + case Severity::performance: + return "warning"; + case Severity::information: + case Severity::internal: + case Severity::debug: + case Severity::none: + return "note"; + } + return "note"; +} + +std::string SarifReport::sarifPrecision(const ErrorMessage& errmsg) +{ + if (errmsg.certainty == Certainty::inconclusive) + return "medium"; + return "high"; +} diff --git a/lib/sarifreport.h b/lib/sarifreport.h new file mode 100644 index 00000000000..26953703a51 --- /dev/null +++ b/lib/sarifreport.h @@ -0,0 +1,54 @@ +/* + * Cppcheck - A tool for static C/C++ code analysis + * Copyright (C) 2007-2025 Cppcheck team. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef SARIF_REPORT_H +#define SARIF_REPORT_H + +#include "config.h" +#include "errorlogger.h" +#include "errortypes.h" + +#include +#include + +// Include picojson headers +#include "json.h" + +class CPPCHECKLIB SarifReport { +public: + SarifReport() = default; + ~SarifReport() = default; + + void addFinding(ErrorMessage msg); + std::string serialize(std::string productName) const; + +private: + // Implementation methods + picojson::array serializeRules() const; + static picojson::array serializeLocations(const ErrorMessage& finding); + picojson::array serializeResults() const; + picojson::value serializeRuns(const std::string& productName, const std::string& version) const; + + // Utility methods + static std::string sarifSeverity(const ErrorMessage& errmsg); + static std::string sarifPrecision(const ErrorMessage& errmsg); + + std::vector mFindings; +}; + +#endif // SARIF_REPORT_H diff --git a/oss-fuzz/Makefile b/oss-fuzz/Makefile index 76c81cc19f2..1ac59db940d 100644 --- a/oss-fuzz/Makefile +++ b/oss-fuzz/Makefile @@ -95,6 +95,7 @@ LIBOBJ = $(libcppdir)/valueflow.o \ $(libcppdir)/programmemory.o \ $(libcppdir)/regex.o \ $(libcppdir)/reverseanalyzer.o \ + $(libcppdir)/sarifreport.o \ $(libcppdir)/settings.o \ $(libcppdir)/standards.o \ $(libcppdir)/summaries.o \ @@ -319,6 +320,9 @@ $(libcppdir)/regex.o: ../lib/regex.cpp ../lib/config.h ../lib/regex.h $(libcppdir)/reverseanalyzer.o: ../lib/reverseanalyzer.cpp ../lib/addoninfo.h ../lib/analyzer.h ../lib/astutils.h ../lib/checkers.h ../lib/config.h ../lib/errortypes.h ../lib/forwardanalyzer.h ../lib/library.h ../lib/mathlib.h ../lib/platform.h ../lib/reverseanalyzer.h ../lib/settings.h ../lib/smallvector.h ../lib/sourcelocation.h ../lib/standards.h ../lib/symboldatabase.h ../lib/templatesimplifier.h ../lib/token.h ../lib/utils.h ../lib/valueptr.h ../lib/vfvalue.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/reverseanalyzer.cpp +$(libcppdir)/sarifreport.o: ../lib/sarifreport.cpp ../externals/picojson/picojson.h ../lib/addoninfo.h ../lib/check.h ../lib/checkers.h ../lib/config.h ../lib/cppcheck.h ../lib/errorlogger.h ../lib/errortypes.h ../lib/json.h ../lib/library.h ../lib/mathlib.h ../lib/platform.h ../lib/sarifreport.h ../lib/settings.h ../lib/standards.h ../lib/utils.h + $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/sarifreport.cpp + $(libcppdir)/settings.o: ../lib/settings.cpp ../externals/picojson/picojson.h ../lib/addoninfo.h ../lib/checkers.h ../lib/config.h ../lib/errortypes.h ../lib/json.h ../lib/library.h ../lib/mathlib.h ../lib/path.h ../lib/platform.h ../lib/settings.h ../lib/standards.h ../lib/summaries.h ../lib/suppressions.h ../lib/utils.h ../lib/vfvalue.h $(CXX) ${LIB_FUZZING_ENGINE} $(CPPFLAGS) $(CXXFLAGS) -c -o $@ $(libcppdir)/settings.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 0bbcea9d8a9..8044af1c249 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -20,6 +20,7 @@ if (BUILD_TESTS) target_include_directories(testrunner SYSTEM PRIVATE ${tinyxml2_INCLUDE_DIRS}) endif() target_externals_include_directories(testrunner PRIVATE ${PROJECT_SOURCE_DIR}/externals/simplecpp/) + target_externals_include_directories(testrunner PRIVATE ${PROJECT_SOURCE_DIR}/externals/picojson/) if (Boost_FOUND) target_include_directories(testrunner SYSTEM PRIVATE ${Boost_INCLUDE_DIRS}) endif() diff --git a/test/cli/helloworld_test.py b/test/cli/helloworld_test.py index 54de0d60930..3b1e21fe19f 100644 --- a/test/cli/helloworld_test.py +++ b/test/cli/helloworld_test.py @@ -377,6 +377,9 @@ def test_sarif(): assert 'security' in res['runs'][0]['tool']['driver']['rules'][0]['properties']['tags'] assert re.match(r'[0-9]+(.[0-9]+)+', res['runs'][0]['tool']['driver']['semanticVersion']) assert 'level' in res['runs'][0]['tool']['driver']['rules'][0]['defaultConfiguration'] # #13885 + assert res['runs'][0]['tool']['driver']['rules'][0]['shortDescription']['text'] == '' + assert res['runs'][0]['results'][0]['message']['text'] == 'Division by zero.' + assert res['runs'][0]['tool']['driver']['rules'][0]['properties']['problem.severity'] == 'error' def test_xml_checkers_report(): diff --git a/test/cli/sarif_test.py b/test/cli/sarif_test.py new file mode 100644 index 00000000000..d2fe9396350 --- /dev/null +++ b/test/cli/sarif_test.py @@ -0,0 +1,622 @@ +# python -m pytest sarif_test.py + +import os +import json +import tempfile + +import pytest + +from testutils import cppcheck + +__script_dir = os.path.dirname(os.path.abspath(__file__)) + +# Test code with various error types +TEST_CODE = """ +#include +#include +#include +#include +#include +#include + +class TestClass { +public: + TestClass() : value(0) {} + ~TestClass() { delete ptr; } + + void setValue(int v) { value = v; } + int getValue() const { return value; } + +private: + int value; + int* ptr = nullptr; +}; + +void testSecurityViolations() { + // Null pointer dereference + int* ptr = nullptr; + *ptr = 5; + + // Array bounds violation + int array[5]; + array[10] = 1; + + // Memory leak + int* mem = (int*)malloc(sizeof(int) * 10); + // forgot to free mem + + // Uninitialized variable + int x; + printf("%d", x); + + // Double free + int* p = (int*)malloc(sizeof(int)); + free(p); + free(p); + + // Buffer overflow with strcpy + char buffer[10]; + char source[20] = "This is too long"; + strcpy(buffer, source); + + // Use after free + int* freed = (int*)malloc(sizeof(int)); + free(freed); + *freed = 42; +} + +void testStyleAndPortabilityIssues() { + // Redundant assignment + int redundant = 5; + redundant = redundant; + + // Unused variable + int unused = 42; + + // Variable scope reduction + int i; + for (i = 0; i < 10; i++) { + // i could be declared in for loop + } +} + +int main() { + testSecurityViolations(); + testStyleAndPortabilityIssues(); + return 0; +} +""" + +BASIC_TEST_CODE = """ +int main() { + int* p = nullptr; + *p = 5; // null pointer dereference + return 0; +} +""" + + +def create_test_file(tmp_path, filename="test.cpp", content=TEST_CODE): + """Create a temporary test file with the given content.""" + filepath = tmp_path / filename + filepath.write_text(content) + return str(filepath) + + +def run_sarif_check(code, extra_args=None): + """Run cppcheck with SARIF output on the given code.""" + with tempfile.TemporaryDirectory() as tmpdir: + tmp_path = os.path.join(tmpdir, "test.cpp") + with open(tmp_path, "w") as f: + f.write(code) + + args = ["--output-format=sarif", "--enable=all", tmp_path] + + if extra_args: + args.extend(extra_args) + + _, _, stderr = cppcheck(args) + + # SARIF output is in stderr + try: + sarif_data = json.loads(stderr) + return sarif_data + except json.JSONDecodeError as e: + pytest.fail(f"Failed to parse SARIF JSON: {e}\nOutput: {stderr}") + return None + + +def test_sarif_basic_structure(): + """Test that SARIF output has the correct basic structure.""" + sarif = run_sarif_check(BASIC_TEST_CODE) + + # Check required SARIF fields + assert sarif["version"] == "2.1.0" + assert "$schema" in sarif + assert "sarif-schema" in sarif["$schema"] + assert "runs" in sarif + assert len(sarif["runs"]) == 1 + + run = sarif["runs"][0] + assert "tool" in run + assert "results" in run + + tool = run["tool"] + assert "driver" in tool + + driver = tool["driver"] + assert driver["name"] == "Cppcheck" + assert "rules" in driver + assert "semanticVersion" in driver + + +def test_sarif_null_pointer(): + """Test SARIF output for null pointer dereference.""" + sarif = run_sarif_check(BASIC_TEST_CODE) + + run = sarif["runs"][0] + results = run["results"] + + # Should have at least one result + assert len(results) > 0 + + # Find null pointer result + null_pointer_results = [r for r in results if r["ruleId"] == "nullPointer"] + assert len(null_pointer_results) > 0 + + result = null_pointer_results[0] + assert result["level"] == "error" + assert "message" in result + assert "text" in result["message"] + assert ( + "null" in result["message"]["text"].lower() + or "nullptr" in result["message"]["text"].lower() + ) + + # Check location information + assert "locations" in result + assert len(result["locations"]) > 0 + location = result["locations"][0] + assert "physicalLocation" in location + assert "artifactLocation" in location["physicalLocation"] + assert "region" in location["physicalLocation"] + + region = location["physicalLocation"]["region"] + assert "startLine" in region + assert region["startLine"] > 0 + assert "startColumn" in region + assert region["startColumn"] > 0 + + +def test_sarif_security_rules(): + """Test that security-related rules have proper security properties.""" + sarif = run_sarif_check(TEST_CODE) + + run = sarif["runs"][0] + driver = run["tool"]["driver"] + rules = driver["rules"] + + # Check for security-related rules + security_rule_ids = [ + "nullPointer", + "arrayIndexOutOfBounds", + "memleak", + "uninitvar", + "doubleFree", + ] + + for rule_id in security_rule_ids: + matching_rules = [r for r in rules if r["id"] == rule_id] + if matching_rules: + rule = matching_rules[0] + props = rule.get("properties", {}) + + # Security rules should have security-severity + if "tags" in props and "security" in props["tags"]: + assert "security-severity" in props + assert float(props["security-severity"]) > 0 + + # Should have problem.severity + assert "problem.severity" in props + + # Should have precision + assert "precision" in props + + +def test_sarif_rule_descriptions(): + """Test that rule descriptions are properly formatted.""" + sarif = run_sarif_check(TEST_CODE) + + run = sarif["runs"][0] + driver = run["tool"]["driver"] + rules = driver["rules"] + + assert len(rules) > 0 + + for rule in rules: + # Each rule should have required fields + assert "id" in rule + assert "name" in rule + assert "shortDescription" in rule + assert "fullDescription" in rule + + # Descriptions should be empty (allowing GitHub to use instance messages) + assert rule["name"] == "" + assert rule["shortDescription"]["text"] == "" + assert rule["fullDescription"]["text"] == "" + + # Should have properties + assert "properties" in rule + + +def test_sarif_cwe_tags(): + """Test that CWE tags are properly formatted.""" + sarif = run_sarif_check(TEST_CODE) + + run = sarif["runs"][0] + driver = run["tool"]["driver"] + rules = driver["rules"] + + # Find rules with CWE tags + rules_with_cwe = [] + for rule in rules: + props = rule.get("properties", {}) + tags = props.get("tags", []) + cwe_tags = [t for t in tags if t.startswith("external/cwe/cwe-")] + if cwe_tags: + rules_with_cwe.append((rule["id"], cwe_tags)) + + # Should have at least some rules with CWE tags + assert len(rules_with_cwe) > 0 + + # Validate CWE tag format + for _, cwe_tags in rules_with_cwe: + for tag in cwe_tags: + assert tag.startswith("external/cwe/cwe-") + cwe_num = tag[17:] # After 'external/cwe/cwe-' + assert cwe_num.isdigit() + assert int(cwe_num) > 0 + + +def test_sarif_severity_levels(): + """Test that different severity levels are properly mapped.""" + sarif = run_sarif_check(TEST_CODE) + + run = sarif["runs"][0] + results = run["results"] + + # Collect severity levels + levels = set() + for result in results: + levels.add(result["level"]) + + # Should have at least error level + assert "error" in levels + + # Valid SARIF levels + valid_levels = {"error", "warning", "note", "none"} + for level in levels: + assert level in valid_levels + + +def test_sarif_instance_specific_messages(): + """Test that result messages contain instance-specific information.""" + sarif = run_sarif_check(TEST_CODE) + + run = sarif["runs"][0] + results = run["results"] + + # Check that messages are instance-specific + found_specific_messages = False + + for result in results: + message_text = result["message"]["text"] + rule_id = result["ruleId"] + + # Skip system include warnings + if rule_id == "missingIncludeSystem": + continue + + # Messages should not be empty + assert len(message_text) > 0 + + # Check for specific variable names or values from our test code + if rule_id == "nullPointer" and "ptr" in message_text: + found_specific_messages = True + elif rule_id == "arrayIndexOutOfBounds" and ( + "array" in message_text or "10" in message_text + ): + found_specific_messages = True + elif rule_id == "uninitvar" and "x" in message_text: + found_specific_messages = True + elif rule_id == "memleak" and "mem" in message_text: + found_specific_messages = True + elif rule_id == "doubleFree" and "p" in message_text: + found_specific_messages = True + + assert ( + found_specific_messages + ), "Should find at least some instance-specific messages" + + +def test_sarif_location_info(): + """Test that location information is correct.""" + sarif = run_sarif_check(BASIC_TEST_CODE) + + run = sarif["runs"][0] + results = run["results"] + + for result in results: + assert "locations" in result + locations = result["locations"] + assert len(locations) > 0 + + for location in locations: + assert "physicalLocation" in location + phys_loc = location["physicalLocation"] + + assert "artifactLocation" in phys_loc + assert "uri" in phys_loc["artifactLocation"] + + assert "region" in phys_loc + region = phys_loc["region"] + + # SARIF requires line and column numbers >= 1 + assert "startLine" in region + assert region["startLine"] >= 1 + + assert "startColumn" in region + assert region["startColumn"] >= 1 + + +def test_sarif_with_multiple_files(tmp_path): + """Test SARIF output with multiple source files.""" + # Create two test files + file1_content = """ + void test1() { + int* p = nullptr; + *p = 1; + } + """ + + file2_content = """ + void test2() { + int arr[5]; + arr[10] = 2; + } + """ + + file1 = tmp_path / "file1.cpp" + file2 = tmp_path / "file2.cpp" + file1.write_text(file1_content) + file2.write_text(file2_content) + + args = ["--output-format=sarif", "--enable=all", str(tmp_path)] + + _, _, stderr = cppcheck(args) + sarif = json.loads(stderr) + + run = sarif["runs"][0] + results = run["results"] + + # Should have results from both files + files = set() + for result in results: + if "locations" in result: + for location in result["locations"]: + uri = location["physicalLocation"]["artifactLocation"]["uri"] + files.add(os.path.basename(uri)) + + assert "file1.cpp" in files or "file2.cpp" in files + + +def test_sarif_default_configuration(): + """Test that rules have default configuration with level.""" + sarif = run_sarif_check(BASIC_TEST_CODE) + + run = sarif["runs"][0] + driver = run["tool"]["driver"] + rules = driver["rules"] + + for rule in rules: + # Check for defaultConfiguration.level (#13885) + assert "defaultConfiguration" in rule + assert "level" in rule["defaultConfiguration"] + + level = rule["defaultConfiguration"]["level"] + assert level in ["error", "warning", "note", "none"] + + +def test_sarif_rule_properties(): + """Test that rules have required properties.""" + sarif = run_sarif_check(TEST_CODE) + + run = sarif["runs"][0] + driver = run["tool"]["driver"] + rules = driver["rules"] + + for rule in rules: + assert "properties" in rule + props = rule["properties"] + + # Required properties + assert "precision" in props + assert props["precision"] in ["very-high", "high", "medium", "low"] + + assert "problem.severity" in props + assert props["problem.severity"] in [ + "error", + "warning", + "style", + "performance", + "portability", + "information", + "note", + ] + + +def test_sarif_rule_coverage(): + """Test that a variety of rules are triggered by comprehensive test code.""" + sarif = run_sarif_check(TEST_CODE) + + run = sarif["runs"][0] + driver = run["tool"]["driver"] + rules = driver["rules"] + + # Collect all rule IDs + rule_ids = set(rule["id"] for rule in rules) + + # Should have at least 5 different rules triggered + assert ( + len(rule_ids) >= 5 + ), f"Expected at least 5 rules, found {len(rule_ids)}: {rule_ids}" + + # Check for some specific expected rules from different categories + expected_rules = [ + "nullPointer", # Security + "arrayIndexOutOfBounds", # Security + "memleak", # Security + "uninitvar", # Security + "unusedVariable", # Style + "redundantAssignment", # Style + "unusedFunction", # Style (if enabled) + "constParameter", # Style/Performance + "cstyleCast", # Style + "variableScope", # Style + ] + + found_expected_rules = sum(1 for rule in expected_rules if rule in rule_ids) + + # Should find at least 3 of our expected rules + assert ( + found_expected_rules >= 3 + ), f"Expected at least 3 known rules, found {found_expected_rules}" + + +def test_sarif_generic_descriptions(): + """Test that ALL rule descriptions are empty for GitHub integration.""" + sarif = run_sarif_check(TEST_CODE) + + run = sarif["runs"][0] + driver = run["tool"]["driver"] + rules = driver["rules"] + + assert len(rules) > 0, "Should have at least one rule" + + # Verify that ALL rule descriptions are empty so GitHub uses instance-specific messages + for rule in rules: + rule_id = rule["id"] + + # All rules must have these fields + assert "name" in rule, f"Rule {rule_id} missing 'name'" + assert "shortDescription" in rule, f"Rule {rule_id} missing 'shortDescription'" + assert "fullDescription" in rule, f"Rule {rule_id} missing 'fullDescription'" + + # The key test: ALL descriptions should be empty + assert ( + rule["name"] == "" + ), f"Rule {rule_id} name should be empty, got: {rule['name']}" + assert ( + rule["shortDescription"]["text"] == "" + ), f"Rule {rule_id} shortDescription should be empty, got: {rule['shortDescription']['text']}" + assert ( + rule["fullDescription"]["text"] == "" + ), f"Rule {rule_id} fullDescription should be empty, got: {rule['fullDescription']['text']}" + + +def test_sarif_security_rules_classification(): + """Test that security classification is correctly based on CWE IDs.""" + sarif = run_sarif_check(TEST_CODE) + + run = sarif["runs"][0] + driver = run["tool"]["driver"] + rules = driver["rules"] + + found_rule_with_cwe = False + found_rule_without_cwe = False + + for rule in rules: + rule_id = rule["id"] + props = rule.get("properties", {}) + tags = props.get("tags", []) + + # Check if rule has CWE tag + cwe_tags = [t for t in tags if t.startswith("external/cwe/")] + has_cwe = len(cwe_tags) > 0 + + if has_cwe: + found_rule_with_cwe = True + + # Rules with CWE should have security-severity and security tag + assert ( + "security-severity" in props + ), f"Rule {rule_id} with CWE should have security-severity" + assert ( + float(props["security-severity"]) > 0 + ), f"Rule {rule_id} security-severity should be positive" + + # Check for security tag + assert ( + "security" in tags + ), f"Rule {rule_id} with CWE should have 'security' tag" + else: + found_rule_without_cwe = True + + # Rules without CWE should NOT have security-severity or security tag + assert ( + "security-severity" not in props + ), f"Rule {rule_id} without CWE should not have security-severity" + assert ( + "security" not in tags + ), f"Rule {rule_id} without CWE should not have 'security' tag" + + # All rules should still have basic properties + assert "precision" in props, f"Rule {rule_id} missing 'precision'" + assert "problem.severity" in props, f"Rule {rule_id} missing 'problem.severity'" + + # Should find at least some rules in test data + assert ( + found_rule_with_cwe or found_rule_without_cwe + ), "Should find at least some rules with or without CWE" + + +def test_sarif_results_consistency(): + """Test consistency between rule definitions and results.""" + sarif = run_sarif_check(TEST_CODE) + + run = sarif["runs"][0] + driver = run["tool"]["driver"] + rules = driver["rules"] + results = run["results"] + + # Collect rule IDs from both rules and results + rule_ids_in_rules = set(rule["id"] for rule in rules) + rule_ids_in_results = set(result["ruleId"] for result in results) + + # Every rule ID in results should have a corresponding rule definition + for result_rule_id in rule_ids_in_results: + assert ( + result_rule_id in rule_ids_in_rules + ), f"Result references undefined rule: {result_rule_id}" + + # Verify severity level consistency + severity_levels = set() + for result in results: + level = result["level"] + severity_levels.add(level) + + # Valid SARIF levels + assert level in [ + "error", + "warning", + "note", + "none", + ], f"Invalid severity level: {level}" + + # Should have at least error level + assert "error" in severity_levels, "Should have at least one error" + + # Should have multiple severity levels for comprehensive test + assert ( + len(severity_levels) >= 2 + ), f"Expected multiple severity levels, found: {severity_levels}" diff --git a/test/testrunner.vcxproj b/test/testrunner.vcxproj index dc12b024853..6acc2a937b1 100755 --- a/test/testrunner.vcxproj +++ b/test/testrunner.vcxproj @@ -87,6 +87,7 @@ + diff --git a/test/testsarifreport.cpp b/test/testsarifreport.cpp new file mode 100644 index 00000000000..76fe64fe20f --- /dev/null +++ b/test/testsarifreport.cpp @@ -0,0 +1,900 @@ +/* + * Cppcheck - A tool for static C/C++ code analysis + * Copyright (C) 2007-2025 Cppcheck team. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "sarifreport.h" +#include "errorlogger.h" +#include "errortypes.h" +#include "fixture.h" +#include "helpers.h" + +#include +#include +#include + +class TestSarifReport : public TestFixture +{ +public: + TestSarifReport() : TestFixture("TestSarifReport") + {} + +private: + void run() override + { + TEST_CASE(emptyReport); + TEST_CASE(singleError); + TEST_CASE(multipleErrors); + TEST_CASE(errorWithoutLocation); + TEST_CASE(errorWithMultipleLocations); + TEST_CASE(differentSeverityLevels); + TEST_CASE(securityRelatedErrors); + TEST_CASE(cweTagsPresent); + TEST_CASE(noCweNoSecurity); + TEST_CASE(inconclusiveCertainty); + TEST_CASE(criticalErrorId); + TEST_CASE(emptyDescriptions); + TEST_CASE(locationBoundaryValues); + TEST_CASE(duplicateRuleIds); + TEST_CASE(customProductName); + TEST_CASE(versionHandling); + TEST_CASE(securitySeverityMapping); + TEST_CASE(versionWithSpace); + TEST_CASE(customProductNameAndVersion); + TEST_CASE(normalizeLineColumnToOne); + TEST_CASE(internalAndDebugSeverity); + TEST_CASE(problemSeverityMapping); + TEST_CASE(mixedLocationAndNoLocation); + } + + // Helper to create an ErrorMessage + static ErrorMessage createErrorMessage(const std::string& id, + Severity severity, + const std::string& msg, + const std::string& file = "test.cpp", + int line = 10, + int column = 5, + int cweId = 0, + Certainty certainty = Certainty::normal) + { + ErrorMessage::FileLocation loc(file, line, column); + ErrorMessage errorMessage({loc}, file, severity, msg, id, certainty); + if (cweId > 0) + { + errorMessage.cwe = CWE(cweId); + } + return errorMessage; + } + + // Helper to parse JSON and validate structure + static bool parseAndValidateJson(const std::string& json, picojson::value& root) + { + std::string parseError = picojson::parse(root, json); + return parseError.empty() && root.is(); + } + + void emptyReport() + { + SarifReport report; + std::string sarif = report.serialize("TestProduct"); + + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + ASSERT_EQUALS("2.1.0", root.at("version").get()); + ASSERT(root.at("$schema").get().find("sarif-schema-2.1.0") != std::string::npos); + + const picojson::array& runs = root.at("runs").get(); + ASSERT_EQUALS(1U, runs.size()); + + const picojson::object& cur_run = runs[0].get(); + const picojson::array& results = cur_run.at("results").get(); + ASSERT_EQUALS(0U, results.size()); + + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + ASSERT_EQUALS(0U, rules.size()); + } + + void singleError() + { + SarifReport report; + report.addFinding(createErrorMessage("nullPointer", Severity::error, "Null pointer dereference")); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + + // Check results + const picojson::array& results = cur_run.at("results").get(); + ASSERT_EQUALS(1U, results.size()); + + const picojson::object& result = results[0].get(); + ASSERT_EQUALS("nullPointer", result.at("ruleId").get()); + ASSERT_EQUALS("error", result.at("level").get()); + + const picojson::object& message = result.at("message").get(); + ASSERT_EQUALS("Null pointer dereference", message.at("text").get()); + + // Check rules + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + ASSERT_EQUALS(1U, rules.size()); + + const picojson::object& rule = rules[0].get(); + ASSERT_EQUALS("nullPointer", rule.at("id").get()); + } + + void multipleErrors() + { + SarifReport report; + report.addFinding(createErrorMessage("error1", Severity::error, "Error 1", "file1.cpp", 10, 5)); + report.addFinding(createErrorMessage("error2", Severity::warning, "Error 2", "file2.cpp", 20, 10)); + report.addFinding(createErrorMessage("error3", Severity::style, "Error 3", "file3.cpp", 30, 15)); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + + const picojson::array& results = cur_run.at("results").get(); + ASSERT_EQUALS(3U, results.size()); + + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + ASSERT_EQUALS(3U, rules.size()); + } + + void errorWithoutLocation() + { + SarifReport report; + + // Create error without location (empty callStack) + ErrorMessage errorMessage( + {}, "test.cpp", Severity::error, "Error without location", "testError", Certainty::normal); + report.addFinding(errorMessage); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + + // Should have no results (GitHub doesn't support findings without locations) + const picojson::array& results = cur_run.at("results").get(); + ASSERT_EQUALS(0U, results.size()); + + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + ASSERT_EQUALS(0U, rules.size()); + } + + void errorWithMultipleLocations() + { + SarifReport report; + + ErrorMessage::FileLocation loc1("test1.cpp", 10, 5); + ErrorMessage::FileLocation loc2("test2.cpp", 20, 10); + ErrorMessage::FileLocation loc3("test3.cpp", 30, 15); + + ErrorMessage errorMessage({loc1, loc2, loc3}, + "test1.cpp", + Severity::error, + "Error with multiple locations", + "multiLocError", + Certainty::normal); + report.addFinding(errorMessage); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::array& results = cur_run.at("results").get(); + + ASSERT_EQUALS(1U, results.size()); + + const picojson::object& result = results[0].get(); + const picojson::array& locations = result.at("locations").get(); + ASSERT_EQUALS(3U, locations.size()); + + // Verify each location + const picojson::object& loc1Obj = locations[0].get(); + const picojson::object& physLoc1 = loc1Obj.at("physicalLocation").get(); + const picojson::object& region1 = physLoc1.at("region").get(); + ASSERT_EQUALS(10, static_cast(region1.at("startLine").get())); + ASSERT_EQUALS(5, static_cast(region1.at("startColumn").get())); + } + + void differentSeverityLevels() + { + SarifReport report; + + report.addFinding(createErrorMessage("error1", Severity::error, "Error severity")); + report.addFinding(createErrorMessage("warning1", Severity::warning, "Warning severity")); + report.addFinding(createErrorMessage("style1", Severity::style, "Style severity")); + report.addFinding(createErrorMessage("perf1", Severity::performance, "Performance severity")); + report.addFinding(createErrorMessage("port1", Severity::portability, "Portability severity")); + report.addFinding(createErrorMessage("info1", Severity::information, "Information severity")); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + + const picojson::array& results = cur_run.at("results").get(); + ASSERT_EQUALS(6U, results.size()); + + // Check severity mappings + ASSERT_EQUALS("error", results[0].get().at("level").get()); // error + ASSERT_EQUALS("error", results[1].get().at("level").get()); // warning + ASSERT_EQUALS("warning", results[2].get().at("level").get()); // style + ASSERT_EQUALS("warning", results[3].get().at("level").get()); // performance + ASSERT_EQUALS("warning", results[4].get().at("level").get()); // portability + ASSERT_EQUALS("note", results[5].get().at("level").get()); // information + } + + void securityRelatedErrors() + { + SarifReport report; + + // Add errors with CWE IDs + report.addFinding(createErrorMessage("nullPointer", Severity::error, "Null pointer", "test.cpp", 10, 5, 476)); + report.addFinding( + createErrorMessage("bufferOverflow", Severity::error, "Buffer overflow", "test.cpp", 20, 5, 121)); + report.addFinding(createErrorMessage("memleak", Severity::warning, "Memory leak", "test.cpp", 30, 5, 401)); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + + for (const auto& rule : rules) + { + const picojson::object& r = rule.get(); + const picojson::object& props = r.at("properties").get(); + + // Should have security-severity + ASSERT(props.find("security-severity") != props.end()); + + // Should have tags with security and CWE + ASSERT(props.find("tags") != props.end()); + const picojson::array& tags = props.at("tags").get(); + + bool hasSecurityTag = false; + bool hasCweTag = false; + for (const auto& tag : tags) + { + const std::string& tagStr = tag.get(); + if (tagStr == "security") + hasSecurityTag = true; + if (tagStr.find("external/cwe/cwe-") == 0) + hasCweTag = true; + } + + ASSERT(hasSecurityTag); + ASSERT(hasCweTag); + } + } + + void cweTagsPresent() + { + SarifReport report; + + report.addFinding(createErrorMessage("testError", Severity::error, "Test error", "test.cpp", 10, 5, 119)); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + + ASSERT_EQUALS(1U, rules.size()); + + const picojson::object& rule = rules[0].get(); + const picojson::object& props = rule.at("properties").get(); + const picojson::array& tags = props.at("tags").get(); + + bool foundCwe119 = false; + for (const auto& tag : tags) + { + if (tag.get() == "external/cwe/cwe-119") + foundCwe119 = true; + } + + ASSERT(foundCwe119); + } + + void noCweNoSecurity() + { + SarifReport report; + + // Error without CWE ID should not have security properties + report.addFinding(createErrorMessage("styleError", Severity::style, "Style error", "test.cpp", 10, 5, 0)); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + + ASSERT_EQUALS(1U, rules.size()); + + const picojson::object& rule = rules[0].get(); + const picojson::object& props = rule.at("properties").get(); + + // Should NOT have security-severity + ASSERT(props.find("security-severity") == props.end()); + + // Should NOT have tags (or if present, no security tag) + if (props.find("tags") != props.end()) + { + const picojson::array& tags = props.at("tags").get(); + for (const auto& tag : tags) + { + ASSERT(tag.get() != "security"); + } + } + } + + void inconclusiveCertainty() + { + SarifReport report; + + report.addFinding( + createErrorMessage("test1", Severity::error, "Conclusive", "test.cpp", 10, 5, 0, Certainty::normal)); + report.addFinding(createErrorMessage( + "test2", Severity::error, "Inconclusive", "test.cpp", 20, 5, 0, Certainty::inconclusive)); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + + ASSERT_EQUALS(2U, rules.size()); + + // Check precision values + const picojson::object& rule1 = rules[0].get(); + const picojson::object& props1 = rule1.at("properties").get(); + ASSERT_EQUALS("high", props1.at("precision").get()); + + const picojson::object& rule2 = rules[1].get(); + const picojson::object& props2 = rule2.at("properties").get(); + ASSERT_EQUALS("medium", props2.at("precision").get()); + } + + void criticalErrorId() + { + SarifReport report; + + // Use a critical error ID (from ErrorLogger::isCriticalErrorId) + report.addFinding(createErrorMessage("syntaxError", Severity::error, "Syntax error")); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::array& results = cur_run.at("results").get(); + + ASSERT_EQUALS(1U, results.size()); + + const picojson::object& result = results[0].get(); + // Critical errors should always map to "error" level + ASSERT_EQUALS("error", result.at("level").get()); + } + + void emptyDescriptions() + { + SarifReport report; + + report.addFinding(createErrorMessage("testError", Severity::error, "Test error")); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + + ASSERT_EQUALS(1U, rules.size()); + + const picojson::object& rule = rules[0].get(); + + // All descriptions should be empty for GitHub integration + ASSERT_EQUALS("", rule.at("name").get()); + ASSERT_EQUALS("", rule.at("shortDescription").get().at("text").get()); + ASSERT_EQUALS("", rule.at("fullDescription").get().at("text").get()); + ASSERT_EQUALS("", rule.at("help").get().at("text").get()); + } + + void locationBoundaryValues() + { + SarifReport report; + + // Test with line/column values that are 0 + // Note: Negative values don't work correctly if FileLocation uses unsigned types + ErrorMessage::FileLocation loc1("test.cpp", 0, 0); + ErrorMessage::FileLocation loc2("test.cpp", 1, 1); + + ErrorMessage errorMessage1({loc1}, "test.cpp", Severity::error, "Error at 0,0", "error1", Certainty::normal); + ErrorMessage errorMessage2({loc2}, "test.cpp", Severity::error, "Error at 1,1", "error2", Certainty::normal); + + report.addFinding(errorMessage1); + report.addFinding(errorMessage2); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::array& results = cur_run.at("results").get(); + + ASSERT_EQUALS(2U, results.size()); + + // Check first result (0,0 should be normalized to 1,1) + { + const picojson::object& res = results[0].get(); + const picojson::array& locations = res.at("locations").get(); + const picojson::object& loc = locations[0].get(); + const picojson::object& physLoc = loc.at("physicalLocation").get(); + const picojson::object& region = physLoc.at("region").get(); + + int line = static_cast(region.at("startLine").get()); + int column = static_cast(region.at("startColumn").get()); + + // 0 should be normalized to 1 + ASSERT_EQUALS(1, line); + ASSERT_EQUALS(1, column); + } + + // Check second result (1,1 should stay as 1,1) + { + const picojson::object& res = results[1].get(); + const picojson::array& locations = res.at("locations").get(); + const picojson::object& loc = locations[0].get(); + const picojson::object& physLoc = loc.at("physicalLocation").get(); + const picojson::object& region = physLoc.at("region").get(); + + int line = static_cast(region.at("startLine").get()); + int column = static_cast(region.at("startColumn").get()); + + ASSERT_EQUALS(1, line); + ASSERT_EQUALS(1, column); + } + } + + void duplicateRuleIds() + { + SarifReport report; + + // Add multiple errors with the same rule ID + report.addFinding(createErrorMessage("duplicateId", Severity::error, "First error", "file1.cpp", 10, 5)); + report.addFinding(createErrorMessage("duplicateId", Severity::error, "Second error", "file2.cpp", 20, 10)); + report.addFinding(createErrorMessage("duplicateId", Severity::error, "Third error", "file3.cpp", 30, 15)); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + + // Should have 3 results but only 1 rule + const picojson::array& results = cur_run.at("results").get(); + ASSERT_EQUALS(3U, results.size()); + + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + ASSERT_EQUALS(1U, rules.size()); + + const picojson::object& rule = rules[0].get(); + ASSERT_EQUALS("duplicateId", rule.at("id").get()); + } + + void customProductName() + { + SarifReport report; + report.addFinding(createErrorMessage("testError", Severity::error, "Test error")); + + // Test with custom product name + std::string sarif = report.serialize("CustomChecker"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + + // Should use "Cppcheck" as default when custom name doesn't parse + ASSERT_EQUALS("Cppcheck", driver.at("name").get()); + } + + void versionHandling() + { + SarifReport report; + report.addFinding(createErrorMessage("testError", Severity::error, "Test error")); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + + // Should have a semantic version + ASSERT(driver.find("semanticVersion") != driver.end()); + const std::string version = driver.at("semanticVersion").get(); + + // Version should not contain spaces (they should be stripped) + ASSERT(version.find(' ') == std::string::npos); + } + + void securitySeverityMapping() + { + // Test the detailed security-severity mapping for different severity levels with CWE + SarifReport report; + + // Error with CWE should get 9.9 (critical) + report.addFinding(createErrorMessage("error1", Severity::error, "Error with CWE", "test.cpp", 10, 5, 119)); + + // Warning with CWE should get 8.5 (high) + report.addFinding( + createErrorMessage("warning1", Severity::warning, "Warning with CWE", "test.cpp", 20, 5, 120)); + + // Style/Performance/Portability with CWE should get 5.5 (medium) + report.addFinding(createErrorMessage("style1", Severity::style, "Style with CWE", "test.cpp", 30, 5, 398)); + report.addFinding( + createErrorMessage("perf1", Severity::performance, "Performance with CWE", "test.cpp", 40, 5, 407)); + report.addFinding( + createErrorMessage("port1", Severity::portability, "Portability with CWE", "test.cpp", 50, 5, 562)); + + // Information with CWE should get 2.0 (low) + report.addFinding(createErrorMessage("info1", Severity::information, "Info with CWE", "test.cpp", 60, 5, 561)); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + + // Check each rule's security-severity value + for (const auto& rule : rules) + { + const picojson::object& r = rule.get(); + const std::string& id = r.at("id").get(); + const picojson::object& props = r.at("properties").get(); + + ASSERT(props.find("security-severity") != props.end()); + const std::string& severity = props.at("security-severity").get(); + double severityValue = std::stod(severity); + + if (id == "error1") + { + // Use a tolerance for floating-point comparison to avoid warning + ASSERT(std::abs(severityValue - 9.9) < 0.01); + } + else if (id == "warning1") + { + ASSERT(std::abs(severityValue - 8.5) < 0.01); + } + else if (id == "style1" || id == "perf1" || id == "port1") + { + ASSERT(std::abs(severityValue - 5.5) < 0.01); + } + else if (id == "info1") + { + ASSERT(std::abs(severityValue - 2.0) < 0.01); + } + } + } + + void versionWithSpace() + { + // Test that version strings with spaces are properly truncated + SarifReport report; + report.addFinding(createErrorMessage("testError", Severity::error, "Test error")); + + // This test would need a way to inject a version with a space + // The current implementation gets version from CppCheck::version() + // This test verifies the space-trimming logic works + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + + const std::string& version = driver.at("semanticVersion").get(); + // Version should not contain any spaces + ASSERT(version.find(' ') == std::string::npos); + } + + void customProductNameAndVersion() + { + // Test custom product name that includes version info + SarifReport report; + report.addFinding(createErrorMessage("testError", Severity::error, "Test error")); + + // Test with product name that might parse differently + std::string sarif = report.serialize("MyChecker-1.0.0"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + + // Should have a name (either parsed or default) + ASSERT(driver.find("name") != driver.end()); + ASSERT(driver.find("semanticVersion") != driver.end()); + } + + void normalizeLineColumnToOne() + { + SarifReport report; + + // Test with 0 values + ErrorMessage::FileLocation loc0("test.cpp", 0, 0); + ErrorMessage errorMessage0({loc0}, "test.cpp", Severity::error, "Error at 0", "error0", Certainty::normal); + report.addFinding(errorMessage0); + + // Test with positive values + ErrorMessage::FileLocation locPos("test.cpp", 10, 5); + ErrorMessage errorMessagePos( + {locPos}, "test.cpp", Severity::error, "Error at positive", "errorPos", Certainty::normal); + report.addFinding(errorMessagePos); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::array& results = cur_run.at("results").get(); + + ASSERT_EQUALS(2U, results.size()); + + // Check first result with 0,0 + const picojson::object& res0 = results[0].get(); + const picojson::array& locations0 = res0.at("locations").get(); + const picojson::object& loc0_obj = locations0[0].get(); + const picojson::object& physLoc0 = loc0_obj.at("physicalLocation").get(); + const picojson::object& region0 = physLoc0.at("region").get(); + + int line0 = static_cast(region0.at("startLine").get()); + int column0 = static_cast(region0.at("startColumn").get()); + + // 0 values should be normalized to 1 + ASSERT(line0 == 1); + ASSERT(column0 == 1); + + // Check second result with positive values + const picojson::object& res1 = results[1].get(); + const picojson::array& locations1 = res1.at("locations").get(); + const picojson::object& loc1_obj = locations1[0].get(); + const picojson::object& physLoc1 = loc1_obj.at("physicalLocation").get(); + const picojson::object& region1 = physLoc1.at("region").get(); + + ASSERT_EQUALS(10, static_cast(region1.at("startLine").get())); + ASSERT_EQUALS(5, static_cast(region1.at("startColumn").get())); + } + + void internalAndDebugSeverity() + { + // Test internal and debug severity levels + // Based on the implementation in sarifSeverity(): + // - internal -> error + // - debug -> note + // - none -> note + SarifReport report; + + // Create errors with internal and debug severities + ErrorMessage::FileLocation loc1("test.cpp", 10, 5); + ErrorMessage::FileLocation loc2("test.cpp", 20, 10); + ErrorMessage::FileLocation loc3("test.cpp", 30, 15); + + ErrorMessage internal( + {loc1}, "test.cpp", Severity::internal, "Internal message", "internalError", Certainty::normal); + ErrorMessage debug({loc2}, "test.cpp", Severity::debug, "Debug message", "debugError", Certainty::normal); + ErrorMessage none({loc3}, "test.cpp", Severity::none, "None message", "noneError", Certainty::normal); + + report.addFinding(internal); + report.addFinding(debug); + report.addFinding(none); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::array& results = cur_run.at("results").get(); + + ASSERT_EQUALS(3U, results.size()); + + // Check the actual mapping + const picojson::object& res0 = results[0].get(); + const picojson::object& res1 = results[1].get(); + const picojson::object& res2 = results[2].get(); + + const std::string level0 = res0.at("level").get(); + const std::string level1 = res1.at("level").get(); + const std::string level2 = res2.at("level").get(); + + // Actual implementation behavior: + ASSERT_EQUALS("error", level0); // internal -> error + ASSERT_EQUALS("note", level1); // debug -> note + ASSERT_EQUALS("note", level2); // none -> note + } + + void problemSeverityMapping() + { + // Test that problem.severity property matches the SARIF severity + SarifReport report; + + report.addFinding(createErrorMessage("error1", Severity::error, "Error")); + report.addFinding(createErrorMessage("warning1", Severity::warning, "Warning")); + report.addFinding(createErrorMessage("style1", Severity::style, "Style")); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + + for (const auto& rule : rules) + { + const picojson::object& r = rule.get(); + const picojson::object& props = r.at("properties").get(); + const picojson::object& defaultConfig = r.at("defaultConfiguration").get(); + + // problem.severity should match defaultConfiguration.level + const std::string& problemSeverity = props.at("problem.severity").get(); + const std::string& defaultLevel = defaultConfig.at("level").get(); + + ASSERT_EQUALS(defaultLevel, problemSeverity); + } + } + + void mixedLocationAndNoLocation() + { + // Test a mix of findings with and without locations + SarifReport report; + + // Add findings with locations + report.addFinding(createErrorMessage("withLoc1", Severity::error, "Error with location", "test.cpp", 10, 5)); + report.addFinding( + createErrorMessage("withLoc2", Severity::warning, "Warning with location", "test.cpp", 20, 5)); + + // Add findings without locations + ErrorMessage noLoc1({}, "test.cpp", Severity::error, "Error without location", "noLoc1", Certainty::normal); + ErrorMessage noLoc2({}, "test.cpp", Severity::warning, "Warning without location", "noLoc2", Certainty::normal); + + report.addFinding(noLoc1); + report.addFinding(noLoc2); + + // Add more with locations + report.addFinding(createErrorMessage("withLoc3", Severity::style, "Style with location", "test.cpp", 30, 5)); + + std::string sarif = report.serialize("Cppcheck"); + picojson::value json; + ASSERT(parseAndValidateJson(sarif, json)); + + const picojson::object& root = json.get(); + const picojson::array& runs = root.at("runs").get(); + const picojson::object& cur_run = runs[0].get(); + + // Should only have results for findings with locations + const picojson::array& results = cur_run.at("results").get(); + ASSERT_EQUALS(3U, results.size()); + + // Should only have rules for findings with locations + const picojson::object& tool = cur_run.at("tool").get(); + const picojson::object& driver = tool.at("driver").get(); + const picojson::array& rules = driver.at("rules").get(); + ASSERT_EQUALS(3U, rules.size()); + + // Verify the rule IDs are only for findings with locations + std::set ruleIds; + for (const auto& rule : rules) + { + const picojson::object& r = rule.get(); + ruleIds.insert(r.at("id").get()); + } + + ASSERT(ruleIds.find("withLoc1") != ruleIds.end()); + ASSERT(ruleIds.find("withLoc2") != ruleIds.end()); + ASSERT(ruleIds.find("withLoc3") != ruleIds.end()); + ASSERT(ruleIds.find("noLoc1") == ruleIds.end()); + ASSERT(ruleIds.find("noLoc2") == ruleIds.end()); + } +}; + +REGISTER_TEST(TestSarifReport) diff --git a/tools/dmake/dmake.cpp b/tools/dmake/dmake.cpp index 740b265d6c5..6b91c7cbe31 100644 --- a/tools/dmake/dmake.cpp +++ b/tools/dmake/dmake.cpp @@ -766,7 +766,7 @@ int main(int argc, char **argv) makeConditionalVariable(fout, "INCLUDE_FOR_LIB", "-Ilib -isystem externals -isystem externals/picojson -isystem externals/simplecpp -isystem externals/tinyxml2"); makeConditionalVariable(fout, "INCLUDE_FOR_FE", "-Ilib"); makeConditionalVariable(fout, "INCLUDE_FOR_CLI", "-Ilib -Ifrontend -isystem externals/picojson -isystem externals/simplecpp -isystem externals/tinyxml2"); - makeConditionalVariable(fout, "INCLUDE_FOR_TEST", "-Ilib -Ifrontend -Icli -isystem externals/simplecpp -isystem externals/tinyxml2"); + makeConditionalVariable(fout, "INCLUDE_FOR_TEST", "-Ilib -Ifrontend -Icli -isystem externals/picojson -isystem externals/simplecpp -isystem externals/tinyxml2"); makeConditionalVariable(fout, "CFLAGS_FOR_TEST", "-Wno-dollar-in-identifier-extension");