From aef1a3368f38c4f5aa767b7c75754bd444724af1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Csord=C3=A1s?= Date: Tue, 12 Apr 2022 11:07:13 +0200 Subject: [PATCH] [report-converter] Support sarif format and gcc analyzer Fixes #1797. Based on a commit authored by @csordasmarton. Credit goes to him! We've long wanted to support sarif (https://sarifweb.azurewebsites.net/), and finally, this is the first real step towards it! This patch can both parse and export to sarif. My intent is that the code is self explanatory (because I explained things in the code!), there are two things I'd like to highlight: 1. I strugged a LOT with mypy, which lead me to express a things things in a rather cumbersome manner. I left comments around these parts 2. I copied all example tests from https://github.com/microsoft/sarif-tutorials/ to tools/report-converter/tests/unit/parser/sarif/sarif_test_files/. These examples come with an MIT licence, which I also copied over. --- analyzer/requirements.txt | 1 + docs/README.md | 2 +- docs/supported_code_analyzers.md | 1 + docs/tools/report-converter.md | 70 ++- .../analyzers/gcc/__init__.py | 7 + .../analyzers/gcc/analyzer_result.py | 36 ++ .../codechecker_report_converter/cli.py | 9 +- .../report/parser/base.py | 52 ++- .../report/parser/plist.py | 51 +- .../report/parser/sarif.py | 441 ++++++++++++++++++ .../report/report_file.py | 21 +- .../requirements_py/dev/requirements.txt | 1 + .../double_free.cpp.sarif | 229 +++++++++ .../double_free.expected.plist | 260 +++++++++++ .../files/double_free.cpp | 13 + .../tests/unit/analyzers/test_gcc_parser.py | 107 +++++ .../tests/unit/parser/sarif/__init__.py | 10 + .../1-Introduction/simple-example.js | 1 + .../1-Introduction/simple-example.sarif | 57 +++ .../2-Basics/empty-log.sarif.json | 14 + .../3-Beyond-basics/automation-details.sarif | 21 + .../bad-eval-related-locations.sarif | 47 ++ .../bad-eval-with-code-flow.py | 9 + .../bad-eval-with-code-flow.sarif | 99 ++++ .../3-Beyond-basics/bad-eval.py | 4 + .../3-Beyond-basics/bad-eval.sarif | 32 ++ .../3-Beyond-basics/link-to-location.sarif | 49 ++ .../message-from-metadata.sarif | 31 ++ .../message-with-arguments.sarif | 34 ++ .../3-Beyond-basics/standard-taxonomy.sarif | 87 ++++ .../sarif/sarif_test_files/Baseline.sarif | 48 ++ .../sarif/sarif_test_files/CodeFlows.sarif | 207 ++++++++ .../sarif_test_files/ContextRegion.sarif | 51 ++ .../DefaultRuleConfiguration.sarif | 69 +++ .../EmbeddedBinaryContent.sarif | 53 +++ .../EmbeddedTextContent.sarif | 78 ++++ .../Catastrophic configuration error.sarif | 28 ++ .../Catastrophic execution error.sarif | 28 ++ .../ExceptionalConditions/Empty runs.sarif | 5 + .../ExceptionalConditions/No runs.sarif | 5 + .../One run empty results.sarif | 16 + .../One run no results.sarif | 15 + .../One run with results.sarif | 23 + .../ExceptionalConditions/README.md | 7 + .../sarif/sarif_test_files/LICENSE-CODE | 26 ++ .../sarif_test_files/Notifications.sarif | 167 +++++++ .../sarif_test_files/OriginalUriBaseIds.sarif | 103 ++++ .../sarif_test_files/RegionVariants.sarif | 430 +++++++++++++++++ .../sarif/sarif_test_files/ResultStacks.sarif | 127 +++++ .../sarif/sarif_test_files/RuleMetadata.sarif | 71 +++ .../sarif/sarif_test_files/Suppressions.sarif | 131 ++++++ .../sarif/sarif_test_files/Taxonomies.sarif | 130 ++++++ .../sarif/sarif_test_files/TextFile.txt | 1 + .../sarif/sarif_test_files/_template.sarif | 24 + .../sarif/sarif_test_files/code/example.cs | 1 + .../sarif_test_files/collections/example.cs | 1 + .../sarif/sarif_test_files/collections/list.h | 1 + .../parser/sarif/sarif_test_files/default.txt | 1 + .../sarif/sarif_test_files/explicit.txt | 1 + .../parser/sarif/sarif_test_files/test.json | 1 + .../unit/parser/sarif/test_sarif_parser.py | 43 ++ web/requirements.txt | 1 + 62 files changed, 3609 insertions(+), 80 deletions(-) create mode 100644 tools/report-converter/codechecker_report_converter/analyzers/gcc/__init__.py create mode 100644 tools/report-converter/codechecker_report_converter/analyzers/gcc/analyzer_result.py create mode 100644 tools/report-converter/codechecker_report_converter/report/parser/sarif.py create mode 100644 tools/report-converter/tests/unit/analyzers/gcc_output_test_files/double_free.cpp.sarif create mode 100644 tools/report-converter/tests/unit/analyzers/gcc_output_test_files/double_free.expected.plist create mode 100644 tools/report-converter/tests/unit/analyzers/gcc_output_test_files/files/double_free.cpp create mode 100644 tools/report-converter/tests/unit/analyzers/test_gcc_parser.py create mode 100644 tools/report-converter/tests/unit/parser/sarif/__init__.py create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/1-Introduction/simple-example.js create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/1-Introduction/simple-example.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/2-Basics/empty-log.sarif.json create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/automation-details.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-related-locations.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-with-code-flow.py create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-with-code-flow.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval.py create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/link-to-location.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/message-from-metadata.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/message-with-arguments.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/standard-taxonomy.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Baseline.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/CodeFlows.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ContextRegion.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/DefaultRuleConfiguration.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/EmbeddedBinaryContent.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/EmbeddedTextContent.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Catastrophic configuration error.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Catastrophic execution error.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Empty runs.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/No runs.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run empty results.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run no results.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run with results.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/README.md create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/LICENSE-CODE create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Notifications.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/OriginalUriBaseIds.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/RegionVariants.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ResultStacks.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/RuleMetadata.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Suppressions.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Taxonomies.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/TextFile.txt create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/_template.sarif create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/code/example.cs create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/collections/example.cs create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/collections/list.h create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/default.txt create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/explicit.txt create mode 100644 tools/report-converter/tests/unit/parser/sarif/sarif_test_files/test.json create mode 100644 tools/report-converter/tests/unit/parser/sarif/test_sarif_parser.py diff --git a/analyzer/requirements.txt b/analyzer/requirements.txt index 977930d4a8..105a60739c 100644 --- a/analyzer/requirements.txt +++ b/analyzer/requirements.txt @@ -2,4 +2,5 @@ lxml==4.9.2 portalocker==2.2.1 psutil==5.8.0 PyYAML==6.0.1 +sarif-tools==1.0.0 mypy_extensions==0.4.3 diff --git a/docs/README.md b/docs/README.md index 9131871db6..539092b6c0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -193,7 +193,7 @@ For details see Useful tools that can also be used outside CodeChecker. * [Build Logger (to generate JSON Compilation Database from your builds)](/analyzer/tools/build-logger/README.md) -* [Plist to HTML converter (to generate HTML files from the given plist files)](/docs/tools/report-converter.md#plist-to-html-tool) +* [Plist/Sarif to HTML converter (to generate HTML files from the given plist or sarif files)](/docs/tools/report-converter.md#plist-to-html-tool) * [Report Converter Tool (to convert analysis results from other analyzers to the codechecker report directory format))](/docs/tools/report-converter.md) * [Translation Unit Collector (to collect source files of a translation unit or to get source files which depend on the given header files)](/docs/tools/tu_collector.md) * [Report Hash generator (to generate unique hash identifiers for reports)](/docs/tools/report-converter.md#report-hash-generation-module) diff --git a/docs/supported_code_analyzers.md b/docs/supported_code_analyzers.md index e754cb965e..3dfc4a4b2d 100644 --- a/docs/supported_code_analyzers.md +++ b/docs/supported_code_analyzers.md @@ -21,6 +21,7 @@ CodeChecker result directory which can be stored to a CodeChecker server. | | [Kernel-Doc](/docs/tools/report-converter.md#kernel-doc) | ✓ | | | [Sparse](/docs/tools/report-converter.md#sparse) | ✓ | | | [cpplint](/docs/tools/report-converter.md#cpplint) | ✓ | +| | [GNU GCC Static Analyzer](/docs/tools/report-converter.md#gcc) | ✓ | | **C#** | [Roslynator.DotNet.Cli](/docs/tools/report-converter.md#roslynatordotnetcli) | ✓ | | **Java** | [FindBugs](http://findbugs.sourceforge.net/) | ✗ | | | [SpotBugs](/docs/tools/report-converter.md#spotbugs) | ✓ | diff --git a/docs/tools/report-converter.md b/docs/tools/report-converter.md index 69c71b681a..9076fb2088 100644 --- a/docs/tools/report-converter.md +++ b/docs/tools/report-converter.md @@ -14,6 +14,7 @@ a CodeChecker server. * [Thread Sanitizer](#thread-sanitizer) * [Leak Sanitizer](#leak-sanitizer) * [Cppcheck](#cppcheck) + * [GNU GCC Static Analyzer](#gnu-gcc-static-analyzer) * [Spotbugs](#spotbugs) * [Facebook Infer](#facebook-infer) * [ESLint](#eslint) @@ -29,7 +30,7 @@ a CodeChecker server. * [Sparse](#sparse) * [cpplint](#cpplint) * [Roslynator.DotNet.Cli](#roslynatordotnetcli) -* [Plist to html tool](#plist-to-html-tool) +* [Plist/Sarif to html tool](#plistsarif-to-html-tool) * [Usage](#usage-1) * [Report hash generation module](#report-hash-generation-module) * [Generate path sensitive report hash](#generate-path-sensitive-report-hash) @@ -53,33 +54,37 @@ make package $ report-converter --help (click to expand) ``` -usage: report-converter [-h] -o OUTPUT_DIR -t TYPE [--meta [META [META ...]]] - [--filename FILENAME] [-c] [-v] - file +usage: report-converter [-h] -o OUTPUT_DIR -t TYPE [-e EXPORT] + [--meta [META ...]] [--filename FILENAME] [-c] [-v] + input [input ...] Creates a CodeChecker report directory from the given code analyzer output which can be stored to a CodeChecker web server. positional arguments: - file Code analyzer output result file which will be parsed - and used to generate a CodeChecker report directory. + input Code analyzer output result files or directories which + will be parsed and used to generate a CodeChecker + report directory. -optional arguments: +options: -h, --help show this help message and exit -o OUTPUT_DIR, --output OUTPUT_DIR This directory will be used to generate CodeChecker report directory files. -t TYPE, --type TYPE Specify the format of the code analyzer output. - Currently supported output types are: asan, clang- - tidy, coccinelle, cppcheck, cpplint, eslint, - fbinfer, golint, kernel-doc, lsan, mdl, msan, - pyflakes, pylint, roslynator, smatch, sparse, sphinx, spotbugs, - tsan, tslint, ubsan. - --meta [META [META ...]] - Metadata information which will be stored alongside - the run when the created report directory will be - stored to a running CodeChecker server. It has the - following format: key=value. Valid key values are: + Currently supported output types are: asan, clang-tidy, + coccinelle, cppcheck, cpplint, eslint, fbinfer, gcc, + golint, kernel-doc, lsan, mdl, msan, pyflakes, pylint, + roslynator, smatch, sparse, sphinx, spotbugs, tsan, + tslint, ubsan. + -e EXPORT, --export EXPORT + Specify the export format of the converted reports. + Currently supported export types are: .plist, .sarif. + (default: plist) + --meta [META ...] Metadata information which will be stored alongside the + run when the created report directory will be stored to + a running CodeChecker server. It has the following + format: key=value. Valid key values are: analyzer_command, analyzer_version. (default: None) --filename FILENAME This option can be used to override the default plist file name output of this tool. This tool can produce @@ -106,6 +111,7 @@ Supported analyzers: cpplint - cpplint, https://github.com/cpplint/cpplint eslint - ESLint, https://eslint.org/ fbinfer - Facebook Infer, https://fbinfer.com + gcc - GNU Compiler Collection Static Analyzer, https://gcc.gnu.org/wiki/StaticAnalyzer golint - Golint, https://github.com/golang/lint kernel-doc - Kernel-Doc, https://github.com/torvalds/linux/blob/master/scripts/kernel-doc lsan - LeakSanitizer, https://clang.llvm.org/docs/LeakSanitizer.html @@ -254,6 +260,34 @@ CppCheck: `analysis statistics`, `analysis duration`, `cppcheck command` etc. For more information about logging checkout the log section in the [user guide](/docs/usage.md). +### [GNU GCC Static Analyzer](https://gcc.gnu.org/wiki/StaticAnalyzer) + +This project introduces a static analysis pass for GCC that can diagnose +various kinds of problems in C/C++ code at compile-time (e.g. double-free, +use-after-free, etc). + +The analyzer runs as an IPA pass on the gimple SSA representation. It +associates state machines with data, with transitions at certain statements +and edges. It finds "interesting" interprocedural paths through the user's +code, in which bogus state transitions happen. + +GCC 13.0.0 and later versions support the output in sarif formats, which +report-converter can parse. Earlier versions only supported a json output, +which report-converter doesn't support. + +You can enable the GNU GCC Static Analyzer and the sarif output with the +following flags: +```sh +# Complie and analyze my_file.cpp. +g++ -fanalyzer -fdiagnostics-format=sarif-file my_file.cpp + +# GCC created a new file, my_file.cpp.sarif. +report-converter -t gcc -o my_file.cpp.sarif ./gcc_reports + +# Store the Cppcheck reports with CodeChecker. +CodeChecker store ./codechecker_cppcheck_reports -n cppcheck +``` + ### [Spotbugs](https://spotbugs.github.io/) [Spotbugs](https://spotbugs.github.io/) is a static analysis tool for `Java` code. @@ -618,7 +652,7 @@ report-converter -t roslynator -o ./codechecker_roslynator_reports ./sample.xml CodeChecker store ./codechecker_roslynator_reports -n roslynator ``` -## Plist to html tool +## Plist/Sarif to html tool `plist-to-html` is a python tool which parses and creates HTML files from one or more `.plist` result files. diff --git a/tools/report-converter/codechecker_report_converter/analyzers/gcc/__init__.py b/tools/report-converter/codechecker_report_converter/analyzers/gcc/__init__.py new file mode 100644 index 0000000000..4259749345 --- /dev/null +++ b/tools/report-converter/codechecker_report_converter/analyzers/gcc/__init__.py @@ -0,0 +1,7 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- diff --git a/tools/report-converter/codechecker_report_converter/analyzers/gcc/analyzer_result.py b/tools/report-converter/codechecker_report_converter/analyzers/gcc/analyzer_result.py new file mode 100644 index 0000000000..2cdfbbcd0d --- /dev/null +++ b/tools/report-converter/codechecker_report_converter/analyzers/gcc/analyzer_result.py @@ -0,0 +1,36 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- + +import logging +from typing import Dict, List + +from codechecker_report_converter.report import File, Report +from codechecker_report_converter.report.parser import sarif + +from ..analyzer_result import AnalyzerResultBase + + +LOG = logging.getLogger('report-converter') + + +class AnalyzerResult(AnalyzerResultBase): + """ Transform analyzer result of the GCC Static Analyzer. """ + + TOOL_NAME = 'gcc' + NAME = 'GNU Compiler Collection Static Analyzer' + URL = 'https://gcc.gnu.org/wiki/StaticAnalyzer' + + def __init__(self): + super(AnalyzerResult, self).__init__() + self.__infer_out_parent_dir = None + self._file_cache: Dict[str, File] = {} + + def get_reports(self, result_file_path: str) -> List[Report]: + """ Get reports from the given analyzer result file. """ + + return sarif.Parser().get_reports(result_file_path) diff --git a/tools/report-converter/codechecker_report_converter/cli.py b/tools/report-converter/codechecker_report_converter/cli.py index 236685d2fe..55761b360a 100755 --- a/tools/report-converter/codechecker_report_converter/cli.py +++ b/tools/report-converter/codechecker_report_converter/cli.py @@ -73,7 +73,7 @@ class RawDescriptionDefaultHelpFormatter( analyzer_result = getattr(module, "AnalyzerResult") supported_converters[analyzer_result.TOOL_NAME] = analyzer_result except ModuleNotFoundError: - pass + raise supported_metadata_keys = ["analyzer_command", "analyzer_version"] @@ -188,6 +188,9 @@ def __add_arguments_to_parser(parser): "Currently supported output types are: " + ', '.join(sorted(supported_converters)) + ".") + extensions_with_dot = \ + sorted([f".{ext}" for ext in SUPPORTED_ANALYZER_EXTENSIONS]) + parser.add_argument('-e', '--export', type=str, dest='export', @@ -196,8 +199,8 @@ def __add_arguments_to_parser(parser): default=plist.EXTENSION, help="Specify the export format of the converted " "reports. Currently supported export types " - "are: " + ', '.join(sorted( - SUPPORTED_ANALYZER_EXTENSIONS)) + ".") + "are: " + + ', '.join(extensions_with_dot) + ".") parser.add_argument('--meta', nargs='*', diff --git a/tools/report-converter/codechecker_report_converter/report/parser/base.py b/tools/report-converter/codechecker_report_converter/report/parser/base.py index fb1bf3bdbf..a86660ec9a 100644 --- a/tools/report-converter/codechecker_report_converter/report/parser/base.py +++ b/tools/report-converter/codechecker_report_converter/report/parser/base.py @@ -9,11 +9,14 @@ Base parser class to parse analyzer result files. """ +import json import logging +import os from abc import ABCMeta, abstractmethod -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple +from codechecker_report_converter import __title__, __version__ from codechecker_report_converter.report import File, Report from codechecker_report_converter.report.checker_labels import CheckerLabels from codechecker_report_converter.report.hash import HashType @@ -22,6 +25,53 @@ LOG = logging.getLogger('report-converter') +def load_json(path: str): + """ + Load the contents of the given file as a JSON and return it's value, + or default if the file can't be loaded. + """ + + ret = None + try: + with open(path, 'r', encoding='utf-8', errors='ignore') as handle: + ret = json.load(handle) + except IOError as ex: + LOG.warning("Failed to open json file: %s", path) + LOG.warning(ex) + except OSError as ex: + LOG.warning("Failed to open json file: %s", path) + LOG.warning(ex) + except ValueError as ex: + LOG.warning("%s is not a valid json file.", path) + LOG.warning(ex) + except TypeError as ex: + LOG.warning('Failed to process json file: %s', path) + LOG.warning(ex) + + return ret + + +def get_tool_info() -> Tuple[str, str]: + """ Get tool info. + + If this was called through CodeChecker, this function will return + CodeChecker information, otherwise this tool (report-converter) + information. + """ + data_files_dir_path = os.environ.get('CC_DATA_FILES_DIR') + if data_files_dir_path: + analyzer_version_file_path = os.path.join( + data_files_dir_path, 'config', 'analyzer_version.json') + if os.path.exists(analyzer_version_file_path): + data = load_json(analyzer_version_file_path) + version = data.get('version') + if version: + return 'CodeChecker', f"{version['major']}." \ + f"{version['minor']}.{version['revision']}" + + return __title__, __version__ + + class AnalyzerInfo: """ Hold information about the analyzer. """ def __init__(self, name: str): diff --git a/tools/report-converter/codechecker_report_converter/report/parser/plist.py b/tools/report-converter/codechecker_report_converter/report/parser/plist.py index f87700790f..34a75cb72e 100644 --- a/tools/report-converter/codechecker_report_converter/report/parser/plist.py +++ b/tools/report-converter/codechecker_report_converter/report/parser/plist.py @@ -10,7 +10,6 @@ """ import importlib -import json import logging import os import plistlib @@ -27,12 +26,11 @@ else: from mypy_extensions import TypedDict -from codechecker_report_converter import __title__, __version__ from codechecker_report_converter.report import BugPathEvent, \ BugPathPosition, File, get_or_create_file, MacroExpansion, Range, Report from codechecker_report_converter.report.hash import get_report_hash, HashType from codechecker_report_converter.report.parser.base import AnalyzerInfo, \ - BaseParser + BaseParser, get_tool_info LOG = logging.getLogger('report-converter') @@ -422,58 +420,13 @@ def __get_macro_expansions( return macro_expansions - def __load_json(self, path: str): - """ - Load the contents of the given file as a JSON and return it's value, - or default if the file can't be loaded. - """ - - ret = {} - try: - with open(path, 'r', encoding='utf-8', errors='ignore') as handle: - ret = json.load(handle) - except IOError as ex: - LOG.warning("Failed to open json file: %s", path) - LOG.warning(ex) - except OSError as ex: - LOG.warning("Failed to open json file: %s", path) - LOG.warning(ex) - except ValueError as ex: - LOG.warning("%s is not a valid json file.", path) - LOG.warning(ex) - except TypeError as ex: - LOG.warning('Failed to process json file: %s', path) - LOG.warning(ex) - - return ret - - def __get_tool_info(self) -> Tuple[str, str]: - """ Get tool info. - - If this was called through CodeChecker, this function will return - CodeChecker information, otherwise this tool (report-converter) - information. - """ - data_files_dir_path = os.environ.get('CC_DATA_FILES_DIR') - if data_files_dir_path: - analyzer_version_file_path = os.path.join( - data_files_dir_path, 'config', 'analyzer_version.json') - if os.path.exists(analyzer_version_file_path): - data = self.__load_json(analyzer_version_file_path) - version = data.get('version') - if version: - return 'CodeChecker', f"{version['major']}." \ - f"{version['minor']}.{version['revision']}" - - return __title__, __version__ - def convert( self, reports: List[Report], analyzer_info: Optional[AnalyzerInfo] = None ): """ Converts the given reports. """ - tool_name, tool_version = self.__get_tool_info() + tool_name, tool_version = get_tool_info() data: Dict[str, Any] = { 'files': [], diff --git a/tools/report-converter/codechecker_report_converter/report/parser/sarif.py b/tools/report-converter/codechecker_report_converter/report/parser/sarif.py new file mode 100644 index 0000000000..13ec6bbc15 --- /dev/null +++ b/tools/report-converter/codechecker_report_converter/report/parser/sarif.py @@ -0,0 +1,441 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- + +import json +import logging +import os + +from sarif import loader + +from typing import Dict, List, Optional + +from urllib.parse import urlparse +from typing import Any, Dict, List, Optional, Tuple + +from codechecker_report_converter.report import BugPathEvent, \ + BugPathPosition, MacroExpansion, File, get_or_create_file, Range, Report +from codechecker_report_converter.report.hash import get_report_hash, HashType +from codechecker_report_converter.report.parser.base import AnalyzerInfo, \ + BaseParser, load_json, get_tool_info + + +EXTENSION = 'sarif' + + +LOG = logging.getLogger('report-converter') + + +# §3.37 +class ThreadFlowInfo: + def __init__(self): + self.bug_path_events: List[BugPathEvent] = [] + self.bug_path_positions: List[BugPathPosition] = [] + self.notes: List[BugPathEvent] = [] + self.macro_expansions: List[MacroExpansion] = [] + + +# Parse to/from sarif formats. +# Sarif has an extensive documentation, and supports a grand number of fields +# that CodeChecker has no use for as of writing this comment (e.g. §3.39 graph, +# §3.30 region), leading to these fields to be ignored. +# This is NOT (and will likely never be) a full fledged sarif parser, only what +# we need from it at the time. +class Parser(BaseParser): + def get_reports( + self, + result_file_path: str, + source_dir_path: Optional[str] = None + ) -> List[Report]: + """ Get reports from the given analyzer result file. """ + + if not self.has_any_runs(result_file_path): + return [] + + reports: List[Report] = [] + self.result_file_path = result_file_path + self.had_error = False + + data = loader.load_sarif_file(result_file_path) + + for run in data.runs: + rules = self._get_rules(run.run_data) + # $3.14.14 + self.original_uri_base_ids = None + if "originalUriBaseIds" in run.run_data: + self.original_uri_base_ids = run.run_data["originalUriBaseIds"] + + if "results" not in run.run_data: + continue + + for result in run.get_results(): + rule_id = result["ruleId"] + message = self._process_message( + result["message"], rule_id, rules) # §3.11 + + # severity = self.get_severity(rule_id) + + thread_flow_info = self._process_code_flows( + result, rule_id, rules) + for location in result.get("locations", []): + # TODO: We don't really support non-local analyses, so we + # only parse physical locations here. + file, rng = self._process_physical_location(location) + if not (file and rng): + continue + if self.had_error: + return [] + + bug_path_events = thread_flow_info.bug_path_events or None + + report = Report( + file, rng.start_line, rng.start_col, + message, rule_id, # severity, + analyzer_result_file_path=result_file_path, + bug_path_events=bug_path_events, + bug_path_positions=thread_flow_info.bug_path_positions, + notes=thread_flow_info.notes, + macro_expansions=thread_flow_info.macro_expansions) + + if report.report_hash is None: + report.report_hash = get_report_hash( + report, HashType.PATH_SENSITIVE) + + reports.append(report) + + return reports + + def has_any_runs(self, result_file_path: str) -> bool: + """ + The package that contains the sarif import, sarif-tools currently + crashes when a sarif file contains no runs, so we need to manually + check it. + """ + ret = load_json(result_file_path) + return ret and ret["runs"] + + def _get_rules(self, data: Dict) -> Dict[str, Dict]: + """ """ + rules: Dict[str, Dict] = {} + + driver = data["tool"]["driver"] + for rule in driver.get("rules", []): + rules[rule["id"]] = rule + + return rules + + def _process_code_flows( + self, + result: Dict, + rule_id: str, + rules: Dict[str, Dict] + ) -> ThreadFlowInfo: + """ """ + + thread_flow_info = ThreadFlowInfo() + + # TODO: Currently, we only collect bug path events. + + for code_flow in result.get("codeFlows", []): + for thread_flow in code_flow.get("threadFlows", []): # §3.36.3 + for location_data in thread_flow["locations"]: + # There are a lot data stored alongside the location worth + # parsing, but we only need the actual location now. + location = location_data["location"] + + if "message" not in location: + # TODO: this is a bug path position + continue + + message = self._process_message( + location["message"], rule_id, rules) + + file, rng = self._process_physical_location(location) + if not (file and rng): + continue + + importance = location.get("importance") + if importance == 'important': + pass + elif importance == 'essential': + pass + else: + pass + + # TODO: check the importance field. + thread_flow_info.bug_path_events.append(BugPathEvent( + message, file, rng.start_line, rng.start_col, rng)) + + return thread_flow_info + + def _process_physical_location( + self, + location: Dict, + ) -> Tuple[Optional[File], Optional[Range]]: + """ """ + physical_loc = location.get("physicalLocation") + if physical_loc: + file = self._get_file(physical_loc) + rng = self._get_range(physical_loc) + return file, rng + + return None, None + + def _get_range(self, physical_loc: Dict) -> Optional[Range]: + """ Get range from a physical location. """ + region = physical_loc.get("region", {}) + start_line = region.get("startLine") + if start_line is None: + return None + + start_col = region.get("startColumn", 1) + end_line = region.get("endLine", start_line) + end_col = region.get("endColumn", start_col) + + return Range(start_line, start_col, end_line, end_col) + + def _resolve_uri_base_id(self, uri_base_id: str, postfix: str): + """ + Try to assemble the leading part of the URI needed to find out the + absolute path of the file. + """ + error_str = \ + f"Failed to fully resolve the path for '{postfix}'. sarif " \ + "v2.1.0 permits this, but CodeChecker requires it to be " \ + "available. If you intend to hide the absolute path before " \ + "storing the results to a CodeChecker server, use " \ + "--trim-path-prefix." + + if not self.original_uri_base_ids: + LOG.error("Missing 'originalUriBaseIds' (sarif v2.1.0 §3.14.14) " + f"in '{self.result_file_path}'.") + LOG.error(error_str) + self.had_error = True + return "" + + original = self.original_uri_base_ids.get(uri_base_id) + if not original: + LOG.error(f"Missing entry for '{uri_base_id} in" + "'originalUriBaseIds' (sarif v2.1.0 §3.14.14)" + f"in '{self.result_file_path}'.") + LOG.error(error_str) + self.had_error = True + return "" + + abs_uri_prefix = original.get("uri") + if not abs_uri_prefix: + LOG.warning("Missing 'uri' (sarif v2.1.0 §3.4.3) for " + f"'{uri_base_id} in 'originalUriBaseIds' (sarif " + f"v2.1.0 §3.14.14) in '{self.result_file_path}'.") + LOG.error(error_str) + self.had_error = True + return "" + + if "uri_base_id" in original: + prefix = self._resolve_uri_base_id(original.get("uri_base_id"), + postfix) + return prefix + abs_uri_prefix + return abs_uri_prefix + + def _get_file( + self, + physical_loc: Dict + ) -> Optional[File]: + """ Get file path. """ + artifact_loc = physical_loc.get("artifactLocation") + if not artifact_loc: + return None + + uri = artifact_loc.get("uri") + + if "uriBaseId" in artifact_loc: + uri = self._resolve_uri_base_id( + artifact_loc.get("uriBaseId"), uri) + uri + + uri_parsed = urlparse(uri) + if uri_parsed is None: + LOG.warning(f"Failed to urlparse {uri}!") + return None + + file_path = os.path.join(uri_parsed.netloc, uri_parsed.path) + + return get_or_create_file(file_path, self._file_cache) + + def _process_message( + self, + msg: Dict, + rule_id: str, + rules: Dict[str, Dict] + ) -> str: + """ Get message string. """ + if "text" in msg: + return msg["text"] + + args = msg.get("arguments", []) + + rule = rules[rule_id] + message_strings = rule.get("messageStrings", {}) + return message_strings[msg["id"]]["text"].format(*args) + + def convert( + self, + reports: List[Report], + analyzer_info: Optional[AnalyzerInfo] = None + ): + """ Converts the given reports to sarif format. """ + tool_name, tool_version = get_tool_info() + + rules = {} + results = [] + for report in reports: + if report.checker_name not in rules: + rules[report.checker_name] = { + "id": report.checker_name, + "fullDescription": { + "text": report.message + } + } + + results.append(self._create_result(report)) + + return { + "vesion": "2.1.0", + "$schema": "https://raw.githubusercontent.com/oasis-tcs/" + "sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "runs": [{ + "tool": { + "driver": { + "name": tool_name, + "version": tool_version, + "rules": list(rules.values()) + } + }, + "results": results + }] + } + + def _create_result(self, report: Report) -> Dict: + """ Create result dictionary from the given report. """ + result = { + "ruleId": report.checker_name, + "message": { + "text": report.message + }, + "locations": [{ + "physicalLocation": { + "artifactLocation": { + "uri": f"file://{report.file.original_path}" + }, + "region": { + "startLine": report.line, + "startColumn": report.column + } + } + }] + } + + locations = [] + + if report.bug_path_events: + for event in report.bug_path_events: + locations.append(self._create_location_from_bug_path_event( + event, "important")) + + if report.notes: + for note in report.notes: + locations.append(self._create_location_from_bug_path_event( + note, "essential")) + + if report.macro_expansions: + for macro_expansion in report.macro_expansions: + locations.append(self._create_location_from_bug_path_event( + macro_expansion, "essential")) + + if report.bug_path_positions: + for bug_path_position in report.bug_path_positions: + locations.append(self._create_location(bug_path_position)) + + if locations: + result["codeFlows"] = [{ + "threadFlows": [{"locations": locations}] + }] + + return result + + def _create_location_from_bug_path_event( + self, + event: BugPathEvent, + importance: str + ) -> Dict[str, Any]: + """ Create location from bug path event. """ + location = self._create_location(event, event.line, event.column) + + location["importance"] = importance + location["location"]["message"] = {"text": event.message} + + return location + + def _create_location( + self, + pos: BugPathPosition, + line: Optional[int] = -1, + column: Optional[int] = -1 + ) -> Dict[str, Any]: + """ Create location from bug path position. """ + if pos.range: + rng = pos.range + region = { + "startLine": rng.start_line, + "startColumn": rng.start_col, + "endLine": rng.end_line, + "endColumn": rng.end_col, + } + else: + # Seems like there is no smarter way to make mypy happy. Without + # this trickery, it will complain about Optional[int] and int being + # incompatible. + assert line is not None + assert column is not None + linei: int = line + columni: int = column + region = { + "startLine": linei, + "startColumn": columni, + } + + return { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": f"file://{pos.file.original_path}" + }, + "region": region + } + } + } + + def write(self, data: Any, output_file_path: str): + """ Creates an analyzer output file from the given data. """ + try: + with open(output_file_path, 'w', + encoding="utf-8", errors="ignore") as f: + json.dump(data, f) + except TypeError as err: + LOG.error('Failed to write sarif file: %s', output_file_path) + LOG.error(err) + import traceback + traceback.print_exc() + + def replace_report_hash( + self, + analyzer_result_file_path: str, + hash_type=HashType.CONTEXT_FREE + ): + """ + Override hash in the given file by using the given version hash. + """ + pass diff --git a/tools/report-converter/codechecker_report_converter/report/report_file.py b/tools/report-converter/codechecker_report_converter/report/report_file.py index 3bf6457bb0..41421fe207 100644 --- a/tools/report-converter/codechecker_report_converter/report/report_file.py +++ b/tools/report-converter/codechecker_report_converter/report/report_file.py @@ -14,23 +14,19 @@ from codechecker_report_converter.report import File, Report from codechecker_report_converter.report.checker_labels import CheckerLabels from codechecker_report_converter.report.hash import HashType -from codechecker_report_converter.report.parser import plist +from codechecker_report_converter.report.parser import plist, sarif from codechecker_report_converter.report.parser.base import AnalyzerInfo LOG = logging.getLogger('report-converter') -SUPPORTED_ANALYZER_EXTENSIONS = [plist.EXTENSION] - - -__SUPPORTED_ANALYZER_EXTENSIONS = tuple([ - f".{extension}" for extension in SUPPORTED_ANALYZER_EXTENSIONS]) +SUPPORTED_ANALYZER_EXTENSIONS = tuple([plist.EXTENSION, sarif.EXTENSION]) def is_supported(analyzer_result_file_path: str) -> bool: """ True if the given report file can be parsed. """ - return analyzer_result_file_path.endswith(__SUPPORTED_ANALYZER_EXTENSIONS) + return analyzer_result_file_path.endswith(SUPPORTED_ANALYZER_EXTENSIONS) def get_parser( @@ -39,8 +35,13 @@ def get_parser( file_cache: Optional[Dict[str, File]] = None ): """ Returns a parser object for the given analyzer result file. """ - if analyzer_result_file_path.endswith(f".{plist.EXTENSION}"): + # FIXME: It would be more eletgant to collect these modiles (plist, sarif, + # etc) into a list, but mypy seems to not recognize attributes from modules + # saved into variables. + if analyzer_result_file_path.endswith(plist.EXTENSION): return plist.Parser(checker_labels, file_cache) + if analyzer_result_file_path.endswith(sarif.EXTENSION): + return sarif.Parser(checker_labels, file_cache) def get_reports( @@ -54,6 +55,10 @@ def get_reports( if parser: return parser.get_reports(analyzer_result_file_path, source_dir_path) + else: + LOG.error(f"Found no parsers to parse {analyzer_result_file_path}! " + "Supported file extension types are " + f"{SUPPORTED_ANALYZER_EXTENSIONS}.") return [] diff --git a/tools/report-converter/requirements_py/dev/requirements.txt b/tools/report-converter/requirements_py/dev/requirements.txt index bd20124f6c..50981f0c0f 100644 --- a/tools/report-converter/requirements_py/dev/requirements.txt +++ b/tools/report-converter/requirements_py/dev/requirements.txt @@ -1,4 +1,5 @@ pytest==7.3.1 +sarif-tools==1.0.0 pycodestyle==2.7.0 pylint==2.8.2 portalocker==2.2.1 diff --git a/tools/report-converter/tests/unit/analyzers/gcc_output_test_files/double_free.cpp.sarif b/tools/report-converter/tests/unit/analyzers/gcc_output_test_files/double_free.cpp.sarif new file mode 100644 index 0000000000..6e58771b58 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/gcc_output_test_files/double_free.cpp.sarif @@ -0,0 +1,229 @@ +{ + "$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version":"2.1.0", + "runs":[ + { + "tool":{ + "driver":{ + "name":"GNU C++17", + "fullName":"GNU C++17 (Ubuntu 13.1.0-8ubuntu1~22.04) version 13.1.0 (x86_64-linux-gnu)", + "version":"13.1.0", + "informationUri":"https://gcc.gnu.org/gcc-13/", + "rules":[ + { + "id":"-Wanalyzer-double-free", + "helpUri":"https://gcc.gnu.org/onlinedocs/gcc/Static-Analyzer-Options.html#index-Wanalyzer-double-free" + } + ] + } + }, + "taxonomies":[ + { + "name":"CWE", + "version":"4.7", + "organization":"MITRE", + "shortDescription":{ + "text":"The MITRE Common Weakness Enumeration" + }, + "taxa":[ + { + "id":"415", + "helpUri":"https://cwe.mitre.org/data/definitions/415.html" + } + ] + } + ], + "invocations":[ + { + "executionSuccessful":true, + "toolExecutionNotifications":[ + + ] + } + ], + "originalUriBaseIds":{ + "PWD":{ + "uri":"./" + } + }, + "artifacts":[ + { + "location":{ + "uri":"files/double_free.cpp", + "uriBaseId":"PWD" + }, + "contents":{ + "text":"// -------------------------------------------------------------------------\n// Part of the CodeChecker project, under the Apache License v2.0 with\n// LLVM Exceptions. See LICENSE for license information.\n// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception\n// -------------------------------------------------------------------------\n\n#include \n\nvoid test() {\n void *ptr = malloc(sizeof(int));\n free(ptr);\n free(ptr);\n}\n" + }, + "sourceLanguage":"cplusplus" + } + ], + "results":[ + { + "ruleId":"-Wanalyzer-double-free", + "taxa":[ + { + "id":"415", + "toolComponent":{ + "name":"cwe" + } + } + ], + "level":"warning", + "message":{ + "text":"double-‘free’ of ‘ptr’" + }, + "locations":[ + { + "physicalLocation":{ + "artifactLocation":{ + "uri":"files/double_free.cpp", + "uriBaseId":"PWD" + }, + "region":{ + "startLine":12, + "startColumn":3, + "endColumn":12 + }, + "contextRegion":{ + "startLine":12, + "snippet":{ + "text":" free(ptr);\n" + } + } + }, + "logicalLocations":[ + { + "name":"test", + "fullyQualifiedName":"test", + "decoratedName":"_Z4testv", + "kind":"function" + } + ] + } + ], + "codeFlows":[ + { + "threadFlows":[ + { + "locations":[ + { + "location":{ + "physicalLocation":{ + "artifactLocation":{ + "uri":"files/double_free.cpp", + "uriBaseId":"PWD" + }, + "region":{ + "startLine":10, + "startColumn":15, + "endColumn":34 + }, + "contextRegion":{ + "startLine":10, + "snippet":{ + "text":" void *ptr = malloc(sizeof(int));\n" + } + } + }, + "logicalLocations":[ + { + "name":"test", + "fullyQualifiedName":"test", + "decoratedName":"_Z4testv", + "kind":"function" + } + ], + "message":{ + "text":"allocated here" + } + }, + "kinds":[ + "acquire", + "memory" + ], + "nestingLevel":1 + }, + { + "location":{ + "physicalLocation":{ + "artifactLocation":{ + "uri":"files/double_free.cpp", + "uriBaseId":"PWD" + }, + "region":{ + "startLine":11, + "startColumn":3, + "endColumn":12 + }, + "contextRegion":{ + "startLine":11, + "snippet":{ + "text":" free(ptr);\n" + } + } + }, + "logicalLocations":[ + { + "name":"test", + "fullyQualifiedName":"test", + "decoratedName":"_Z4testv", + "kind":"function" + } + ], + "message":{ + "text":"first ‘free’ here" + } + }, + "kinds":[ + "release", + "memory" + ], + "nestingLevel":1 + }, + { + "location":{ + "physicalLocation":{ + "artifactLocation":{ + "uri":"files/double_free.cpp", + "uriBaseId":"PWD" + }, + "region":{ + "startLine":12, + "startColumn":3, + "endColumn":12 + }, + "contextRegion":{ + "startLine":12, + "snippet":{ + "text":" free(ptr);\n" + } + } + }, + "logicalLocations":[ + { + "name":"test", + "fullyQualifiedName":"test", + "decoratedName":"_Z4testv", + "kind":"function" + } + ], + "message":{ + "text":"second ‘free’ here; first ‘free’ was at (2)" + } + }, + "kinds":[ + "danger" + ], + "nestingLevel":1 + } + ] + } + ] + } + ] + } + ] + } + ] +} diff --git a/tools/report-converter/tests/unit/analyzers/gcc_output_test_files/double_free.expected.plist b/tools/report-converter/tests/unit/analyzers/gcc_output_test_files/double_free.expected.plist new file mode 100644 index 0000000000..18dae19fe3 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/gcc_output_test_files/double_free.expected.plist @@ -0,0 +1,260 @@ + + + + + diagnostics + + + category + unknown + check_name + -Wanalyzer-double-free + description + double-‘free’ of ‘ptr’ + issue_hash_content_of_line_in_context + cff41672e36e4537c6939e290d9c0350 + location + + col + 3 + file + 0 + line + 12 + + path + + + edges + + + end + + + col + 3 + file + 0 + line + 11 + + + col + 12 + file + 0 + line + 11 + + + start + + + col + 15 + file + 0 + line + 10 + + + col + 34 + file + 0 + line + 10 + + + + + kind + control + + + edges + + + end + + + col + 3 + file + 0 + line + 12 + + + col + 12 + file + 0 + line + 12 + + + start + + + col + 3 + file + 0 + line + 11 + + + col + 12 + file + 0 + line + 11 + + + + + kind + control + + + depth + 0 + kind + event + location + + col + 15 + file + 0 + line + 10 + + message + allocated here + ranges + + + + col + 15 + file + 0 + line + 10 + + + col + 34 + file + 0 + line + 10 + + + + + + depth + 0 + kind + event + location + + col + 3 + file + 0 + line + 11 + + message + first ‘free’ here + ranges + + + + col + 3 + file + 0 + line + 11 + + + col + 12 + file + 0 + line + 11 + + + + + + depth + 0 + kind + event + location + + col + 3 + file + 0 + line + 12 + + message + second ‘free’ here; first ‘free’ was at (2) + ranges + + + + col + 3 + file + 0 + line + 12 + + + col + 12 + file + 0 + line + 12 + + + + + + type + gcc + + + files + + files/double_free.cpp + + metadata + + analyzer + + name + gcc + + generated_by + + name + report-converter + version + x.y.z + + + + diff --git a/tools/report-converter/tests/unit/analyzers/gcc_output_test_files/files/double_free.cpp b/tools/report-converter/tests/unit/analyzers/gcc_output_test_files/files/double_free.cpp new file mode 100644 index 0000000000..3c28204d7c --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/gcc_output_test_files/files/double_free.cpp @@ -0,0 +1,13 @@ +// ------------------------------------------------------------------------- +// Part of the CodeChecker project, under the Apache License v2.0 with +// LLVM Exceptions. See LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// ------------------------------------------------------------------------- + +#include + +void test() { + void *ptr = malloc(sizeof(int)); + free(ptr); + free(ptr); +} diff --git a/tools/report-converter/tests/unit/analyzers/test_gcc_parser.py b/tools/report-converter/tests/unit/analyzers/test_gcc_parser.py new file mode 100644 index 0000000000..caacd71c62 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/test_gcc_parser.py @@ -0,0 +1,107 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- + +""" +This module tests the correctness of the GccAnalyzerResult, which +used in sequence transform gcc output to a plist file. +""" + + +import os +import plistlib +import shutil +import unittest + +from codechecker_report_converter.analyzers.gcc import analyzer_result +from codechecker_report_converter.report.parser import plist + +from libtest import env + + +OLD_PWD = None + + +class GCCAnalyzerResultTestCase(unittest.TestCase): + """ Test the output of the GccAnalyzerResult. """ + + def setup_class(self): + """ Initialize test files. """ + + global TEST_WORKSPACE + TEST_WORKSPACE = env.get_workspace('gcc_parser') + os.environ['TEST_WORKSPACE'] = TEST_WORKSPACE + self.test_workspace = os.environ['TEST_WORKSPACE'] + self.cc_result_dir = self.test_workspace + + self.analyzer_result = analyzer_result.AnalyzerResult() + + self.test_files = os.path.join(os.path.dirname(__file__), + 'gcc_output_test_files') + global OLD_PWD + OLD_PWD = os.getcwd() + os.chdir(os.path.join(os.path.dirname(__file__), + 'gcc_output_test_files')) + + def teardown_class(self): + """Clean up after the test.""" + + global OLD_PWD + os.chdir(OLD_PWD) + + global TEST_WORKSPACE + + print("Removing: " + TEST_WORKSPACE) + shutil.rmtree(TEST_WORKSPACE, ignore_errors=True) + + def test_no_plist_file(self): + """ Test transforming single plist file. """ + analyzer_output_file = os.path.join(self.test_files, 'files', + 'double_free.cpp') + + ret = self.analyzer_result.transform( + [analyzer_output_file], self.cc_result_dir, plist.EXTENSION, + file_name="{source_file}_{analyzer}") + self.assertFalse(ret) + + def test_no_plist_dir(self): + """ Test transforming single plist file. """ + analyzer_output_file = os.path.join(self.test_files, 'non_existing') + + ret = self.analyzer_result.transform( + [analyzer_output_file], self.cc_result_dir, plist.EXTENSION, + file_name="{source_file}_{analyzer}") + self.assertFalse(ret) + + def test_gcc_transform_single_file(self): + """ Test transforming single plist file. """ + analyzer_output_file = os.path.join( + self.test_files, 'double_free.cpp.sarif') + self.analyzer_result.transform( + [analyzer_output_file], self.cc_result_dir, plist.EXTENSION, + file_name="{source_file}_{analyzer}") + + plist_file = os.path.join(self.cc_result_dir, + 'double_free.cpp_gcc.plist') + with open(plist_file, mode='rb') as pfile: + res = plistlib.load(pfile) + + # Use relative path for this test. + res['files'][0] = 'files/double_free.cpp' + + self.assertTrue(res['metadata']['generated_by']['version']) + res['metadata']['generated_by']['version'] = "x.y.z" + print( + res["diagnostics"][0]["issue_hash_content_of_line_in_context"]) + + plist_file = os.path.join(self.test_files, + 'double_free.expected.plist') + with open(plist_file, mode='rb') as pfile: + exp = plistlib.load(pfile) + + self.maxDiff = None + self.assertEqual(res, exp) diff --git a/tools/report-converter/tests/unit/parser/sarif/__init__.py b/tools/report-converter/tests/unit/parser/sarif/__init__.py new file mode 100644 index 0000000000..9cfe35318f --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/__init__.py @@ -0,0 +1,10 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- + +# This file is empty, and is only present so that this directory will form a +# package. diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/1-Introduction/simple-example.js b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/1-Introduction/simple-example.js new file mode 100644 index 0000000000..9a3e04e016 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/1-Introduction/simple-example.js @@ -0,0 +1 @@ +var x = 42 diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/1-Introduction/simple-example.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/1-Introduction/simple-example.sarif new file mode 100644 index 0000000000..2d2fa5cacf --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/1-Introduction/simple-example.sarif @@ -0,0 +1,57 @@ +{ + "version": "2.1.0", + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.4", + "runs": [ + { + "tool": { + "driver": { + "name": "ESLint", + "informationUri": "https://eslint.org", + "rules": [ + { + "id": "no-unused-vars", + "shortDescription": { + "text": "disallow unused variables" + }, + "helpUri": "https://eslint.org/docs/rules/no-unused-vars", + "properties": { + "category": "Variables" + } + } + ] + } + }, + "artifacts": [ + { + "location": { + "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js" + } + } + ], + "results": [ + { + "level": "error", + "message": { + "text": "'x' is assigned a value but never used." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///C:/dev/sarif/sarif-tutorials/samples/Introduction/simple-example.js", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 5 + } + } + } + ], + "ruleId": "no-unused-vars", + "ruleIndex": 0 + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/2-Basics/empty-log.sarif.json b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/2-Basics/empty-log.sarif.json new file mode 100644 index 0000000000..1499d15373 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/2-Basics/empty-log.sarif.json @@ -0,0 +1,14 @@ +{ + "version": "2.1.0", + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.4.json", + "runs": [ + { + "tool": { + "driver": { + "name": "CodeScanner" + } + }, + "results": [] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/automation-details.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/automation-details.sarif new file mode 100644 index 0000000000..268b01b741 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/automation-details.sarif @@ -0,0 +1,21 @@ +{ + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "CodeScanner" + } + }, + "automationDetails": { + "description": { + "text": "This is the October 10, 2018 nightly run of the CodeScanner tool on all product binaries in the 'master' branch of the 'sarif-sdk' repo" + }, + "id": "Nightly CredScan run for sarif-sdk/master/2018-10-05", + "guid": "d541006e-582d-4600-a603-64925b7f7f35", + "correlationGuid": "53819b2e-a790-4f8b-b68f-a145c13b4f39" + }, + "results": [] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-related-locations.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-related-locations.sarif new file mode 100644 index 0000000000..c07e407243 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-related-locations.sarif @@ -0,0 +1,47 @@ +{ + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "PythonScanner" + } + }, + "results": [ + { + "ruleId": "PY2335", + "message": { + "text": "Use of tainted variable 'expr' in the insecure function 'eval'." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "3-Beyond-basics/bad-eval.py" + }, + "region": { + "startLine": 4 + } + } + } + ], + "relatedLocations": [ + { + "message": { + "text": "The tainted data entered the system here." + }, + "physicalLocation": { + "artifactLocation": { + "uri": "3-Beyond-basics/bad-eval.py" + }, + "region": { + "startLine": 3 + } + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-with-code-flow.py b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-with-code-flow.py new file mode 100644 index 0000000000..e445b213d4 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-with-code-flow.py @@ -0,0 +1,9 @@ +# 3-Beyond-basics/bad-eval-with-code-flow.py + +print("Hello, world!") +expr = input("Expression> ") +use_input(expr) + + +def use_input(raw_input): + print(eval(raw_input)) diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-with-code-flow.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-with-code-flow.sarif new file mode 100644 index 0000000000..9dd75f95ac --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval-with-code-flow.sarif @@ -0,0 +1,99 @@ +{ + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "PythonScanner" + } + }, + "results": [ + { + "ruleId": "PY2335", + "message": { + "text": "Use of tainted variable 'raw_input' in the insecure function 'eval'." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "3-Beyond-basics/bad-eval-with-code-flow.py" + }, + "region": { + "startLine": 8 + } + } + } + ], + "codeFlows": [ + { + "message": { + "text": "Tracing the path from user input to insecure usage." + }, + "threadFlows": [ + { + "locations": [ + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "3-Beyond-basics/bad-eval-with-code-flow.py" + }, + "region": { + "startLine": 3 + } + } + }, + "state": { + "expr": { + "text": "undef" + } + }, + "nestingLevel": 0 + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "3-Beyond-basics/bad-eval-with-code-flow.py" + }, + "region": { + "startLine": 4 + } + } + }, + "state": { + "expr": { + "text": "42" + } + }, + "nestingLevel": 0 + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "3-Beyond-basics/bad-eval-with-code-flow.py" + }, + "region": { + "startLine": 38 + } + } + }, + "state": { + "raw_input": { + "text": "42" + } + }, + "nestingLevel": 1 + } + ] + } + ] + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval.py b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval.py new file mode 100644 index 0000000000..b21bb298b7 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval.py @@ -0,0 +1,4 @@ +# 3-Beyond-basics/bad-eval.py + +expr = input("Expression> ") +print(eval(expr)) diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval.sarif new file mode 100644 index 0000000000..2f43fc5f10 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/bad-eval.sarif @@ -0,0 +1,32 @@ +{ + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "PythonScanner" + } + }, + "results": [ + { + "ruleId": "PY2335", + "message": { + "text": "Use of tainted variable 'expr' in the insecure function 'eval'." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "3-Beyond-basics/bad-eval.py" + }, + "region": { + "startLine": 4 + } + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/link-to-location.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/link-to-location.sarif new file mode 100644 index 0000000000..666abe3be1 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/link-to-location.sarif @@ -0,0 +1,49 @@ +{ + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "PythonScanner" + } + }, + "results": [ + { + "ruleId": "PY2335", + "message": { + "text": "Use of tainted variable 'expr' (which entered the system [here](1)) in the insecure function 'eval'." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "3-Beyond-basics/bad-eval.py" + }, + "region": { + "startLine": 4 + } + } + } + ], + "relatedLocations": [ + { + "id": 1, + "message": { + "text": "The tainted data entered the system here." + }, + "physicalLocation": { + "artifactLocation": { + "uri": "3-Beyond-basics/bad-eval.py" + }, + "region": { + "startLine": 3 + } + } + } + ] + } + ] + } + ] +} + diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/message-from-metadata.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/message-from-metadata.sarif new file mode 100644 index 0000000000..abe2f9bcda --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/message-from-metadata.sarif @@ -0,0 +1,31 @@ +{ + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "CodeScanner", + "rules": [ + { + "id": "CS0001", + "messageStrings": { + "default": { + "text": "This is the message text. It might be very long." + } + } + } + ] + } + }, + "results": [ + { + "ruleId": "CS0001", + "ruleIndex": 0, + "message": { + "id": "default" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/message-with-arguments.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/message-with-arguments.sarif new file mode 100644 index 0000000000..5fc785f3e4 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/message-with-arguments.sarif @@ -0,0 +1,34 @@ +{ + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "CodeScanner", + "rules": [ + { + "id": "CS0001", + "messageStrings": { + "default": { + "text": "Variable '{0}' was used without being initialized." + } + } + } + ] + } + }, + "results": [ + { + "ruleId": "CS0001", + "ruleIndex": 0, + "message": { + "id": "default", + "arguments": [ + "x" + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/standard-taxonomy.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/standard-taxonomy.sarif new file mode 100644 index 0000000000..f22da09b06 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/3-Beyond-basics/standard-taxonomy.sarif @@ -0,0 +1,87 @@ +{ + "version": "2.1.0", + "runs": [ + { + "taxonomies": [ + { + "name": "CWE", + "version": "3.2", + "releaseDateUtc": "2019-01-03", + "guid": "A9282C88-F1FE-4A01-8137-E8D2A037AB82", + "informationUri": "https://cwe.mitre.org/data/published/cwe_v3.2.pdf/", + "downloadUri": "https://cwe.mitre.org/data/xml/cwec_v3.2.xml.zip", + "organization": "MITRE", + "shortDescription": { + "text": "The MITRE Common Weakness Enumeration" + }, + "taxa": [ + { + "id": "401", + "guid": "10F28368-3A92-4396-A318-75B9743282F6", + "name": "Memory Leak", + "shortDescription": { + "text": "Missing Release of Memory After Effective Lifetime" + }, + "defaultConfiguration": { + "level": "warning" + } + } + ], + "isComprehensive": false + } + ], + "tool": { + "driver": { + "name": "CodeScanner", + "supportedTaxonomies": [ + { + "name": "CWE", + "guid": "A9282C88-F1FE-4A01-8137-E8D2A037AB82" + } + ], + "rules": [ + { + "id": "CA2101", + "shortDescription": { + "text": "Failed to release dynamic memory." + }, + "relationships": [ + { + "target": { + "id": "401", + "guid": "A9282C88-F1FE-4A01-8137-E8D2A037AB82", + "toolComponent": { + "name": "CWE", + "guid": "10F28368-3A92-4396-A318-75B9743282F6" + } + }, + "kinds": [ + "superset" + ] + } + ] + } + ] + } + }, + "results": [ + { + "ruleId": "CA2101", + "message": { + "text": "Memory allocated in variable 'p' was not released." + }, + "taxa": [ + { + "id": "401", + "guid": "A9282C88-F1FE-4A01-8137-E8D2A037AB82", + "toolComponent": { + "name": "CWE", + "guid": "10F28368-3A92-4396-A318-75B9743282F6" + } + } + ] + } + ] + } + ] +} diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Baseline.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Baseline.sarif new file mode 100644 index 0000000000..7c7d4a7162 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Baseline.sarif @@ -0,0 +1,48 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample demonstrates the various baseline states a result can have." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0" + } + }, + "results": [ + { + "ruleId": "TUT1001", + "message": { + "text": "Absent result." + }, + "baselineState": "absent" + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Unchanged result." + }, + "baselineState": "unchanged" + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Updated result." + }, + "baselineState": "updated" + }, + { + "ruleId": "TUT1001", + "message": { + "text": "New result." + }, + "baselineState": "new" + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/CodeFlows.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/CodeFlows.sarif new file mode 100644 index 0000000000..5f062ff021 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/CodeFlows.sarif @@ -0,0 +1,207 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample demonstrates the use of code flows to provide information about a result." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0" + } + }, + "results": [ + { + "ruleId": "TUT1001", + "message": { + "text": "Use of uninitialized variable." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "collections/list.h" + }, + "region": { + "startLine": 25, + "startColumn": 8, + "snippet": { + "text": "add_core(ptr, offset, val)" + } + } + } + } + ], + "codeFlows": [ + { + "message": { + "text": "Path from declaration to usage." + }, + "threadFlows": [ + { + "locations": [ + { + "importance": "essential", + "location": { + "message": { + "text": "Variable 'ptr' declared." + }, + "physicalLocation": { + "artifactLocation": { + "uri": "collections/list.h" + }, + "region": { + "startLine": 15, + "startColumn": 8, + "snippet": { + "text": "int* ptr;" + } } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "collections::list::add" + } + ] + } + }, + { + "importance": "unimportant", + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "collections/list.h" + }, + "region": { + "startLine": 18, + "startColumn": 8, + "snippet": { + "text": "offset = 0;" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "collections::list::add" + } + ] + } + }, + { + "importance": "essential", + "location": { + "message": { + "text": "Uninitialized variable 'ptr' passed to method 'add_core'." + }, + "physicalLocation": { + "artifactLocation": { + "uri": "collections/list.h" + }, + "region": { + "startLine": 25, + "startColumn": 8, + "snippet": { + "text": "add_core(ptr, offset, val)" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "collections::list::add" + } + ] + } + } + ] + } + ] + }, + { + "message": { + "text": "Alternate path from declaration to usage." + }, + "threadFlows": [ + { + "locations": [ + { + "importance": "essential", + "location": { + "message": { + "text": "Variable 'ptr' declared." + }, + "physicalLocation": { + "artifactLocation": { + "uri": "collections/list.h" + }, + "region": { + "startLine": 15, + "startColumn": 8, + "snippet": { + "text": "int* ptr;" + } } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "collections::list::add" + } + ] + } + }, + { + "importance": "unimportant", + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "collections/list.h" + }, + "region": { + "startLine": 22, + "startColumn": 8, + "snippet": { + "text": "val = 0;" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "collections::list::add" + } + ] + } + }, + { + "importance": "essential", + "location": { + "message": { + "text": "Uninitialized variable 'ptr' passed to method 'add_core'." + }, + "physicalLocation": { + "artifactLocation": { + "uri": "collections/list.h" + }, + "region": { + "startLine": 25, + "startColumn": 8, + "snippet": { + "text": "add_core(ptr, offset, val)" + } + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "collections::list::add" + } + ] + } + } + ] + } + ] + } ] + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ContextRegion.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ContextRegion.sarif new file mode 100644 index 0000000000..38ca600efd --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ContextRegion.sarif @@ -0,0 +1,51 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample illustrates the relationship between region and contextRegion." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0" + } + }, + "results": [ + { + "ruleId": "TUT1001", + "message": { + "text": "The result." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "code/example.cs" + }, + "region": { + "startLine": 4, + "startColumn": 19, + "endColumn": 22, + "snippet": { + "text": "BAD" + } + }, + "contextRegion": { + "startLine": 4, + "startColumn": 5, + "endColumn": 28, + "snippet": { + "text": "/// This is a BAD word." + } + } + } + } + ] + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/DefaultRuleConfiguration.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/DefaultRuleConfiguration.sarif new file mode 100644 index 0000000000..75f45b663e --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/DefaultRuleConfiguration.sarif @@ -0,0 +1,69 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample demonstrates the use of default rule configuration." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0", + "rules": [ + { + "id": "TUT0001", + "defaultConfiguration": { + "level": "error" + } + }, + { + "id": "TUT0002", + "defaultConfiguration": { + "level": "warning", + "properties": { + "comment": "'warning' is the default, so this can be omitted." + } + } + } + ] + } + }, + "results": [ + { + "ruleId": "TUT1001", + "ruleIndex": 0, + "message": { + "text": "This result is an error according to the default rule configuration." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///C:/code/myProject/io/kb.c" + } + } + } + ] + }, + { + "ruleId": "TUT1002", + "ruleIndex": 1, + "message": { + "text": "This result is a warning according to the default rule configuration." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///C:/code/myProject/io/file.c" + } + } + } + ] + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/EmbeddedBinaryContent.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/EmbeddedBinaryContent.sarif new file mode 100644 index 0000000000..20bcc9c848 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/EmbeddedBinaryContent.sarif @@ -0,0 +1,53 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample contains a result in an artifact whose binary contents are embedded in the log file." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0" + } + }, + "results": [ + { + "ruleId": "TUT1001", + "message": { + "text": "The result." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "data.bin", + "index": 0 + }, + "region": { + "byteOffset": 2, + "byteLength": 2 + } + } + } + ] + } + ], + "artifacts": [ + { + "location": { + "uri": "data.bin" + }, + "contents": { + "binary": "AQIDBAU=" + }, + "properties": { + "comment": "The MIME Base64 encoding of the byte array { 0x01, 0x02, 0x03, 0x04, 0x05 }." + } + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/EmbeddedTextContent.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/EmbeddedTextContent.sarif new file mode 100644 index 0000000000..802070b7e0 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/EmbeddedTextContent.sarif @@ -0,0 +1,78 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample contains a result in an artifact whose textual contents are embedded in the log file." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0" + } + }, + "results": [ + { + "ruleId": "TUT1001", + "message": { + "text": "Result 1: points to an artifact with an explicit encoding." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "explicit.txt", + "index": 0 + }, + "region": { + "startLine": 2 + } + } + } + ] + }, + { + "ruleId": "TUT1002", + "message": { + "text": "Result 2: points to an artifact that inherits the run's default encoding." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "default.txt", + "index": 1 + }, + "region": { + "startLine": 2 + } + } + } + ] + } + ], + "artifacts": [ + { + "location": { + "uri": "explicit.txt" + }, + "contents": { + "text": "Hello,\r\nworld" + }, + "encoding": "UTF-8" + }, + { + "location": { + "uri": "default.txt" + }, + "contents": { + "text": "Hello,\r\nworld" + } + } + ], + "defaultEncoding": "UTF-8", + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Catastrophic configuration error.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Catastrophic configuration error.sarif new file mode 100644 index 0000000000..d61793730c --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Catastrophic configuration error.sarif @@ -0,0 +1,28 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "CodeScanner", + "version": "1.0", + "informationUri": "https://www.example.com/codescanner" + } + }, + "invocations": [ + { + "toolConfigurationNotifications": [ + { + "message": { + "text": "Ruleset 'no-such-ruleset.xml' does not exist." + }, + "level": "error" + } + ], + "executionSuccessful": false + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Catastrophic execution error.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Catastrophic execution error.sarif new file mode 100644 index 0000000000..d8c3df4921 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Catastrophic execution error.sarif @@ -0,0 +1,28 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "CodeScanner", + "version": "1.0", + "informationUri": "https://www.example.com/codescanner" + } + }, + "invocations": [ + { + "toolExecutionNotifications": [ + { + "message": { + "text": "Out of memory." + }, + "level": "error" + } + ], + "executionSuccessful": false + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Empty runs.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Empty runs.sarif new file mode 100644 index 0000000000..787ce0d800 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/Empty runs.sarif @@ -0,0 +1,5 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": [] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/No runs.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/No runs.sarif new file mode 100644 index 0000000000..5bbf8b416a --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/No runs.sarif @@ -0,0 +1,5 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": null +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run empty results.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run empty results.sarif new file mode 100644 index 0000000000..ac46080e3e --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run empty results.sarif @@ -0,0 +1,16 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "CodeScanner", + "version": "1.0", + "informationUri": "https://www.example.com/codescanner" + } + }, + "results": [] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run no results.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run no results.sarif new file mode 100644 index 0000000000..0dc76c790c --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run no results.sarif @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "CodeScanner", + "version": "1.0", + "informationUri": "https://www.example.com/codescanner" + } + } + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run with results.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run with results.sarif new file mode 100644 index 0000000000..530524ef16 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/One run with results.sarif @@ -0,0 +1,23 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "CodeScanner", + "version": "1.0", + "informationUri": "https://www.example.com/codescanner" + } + }, + "results": [ + { + "ruleId": "TEST1001", + "message": { + "text": "Missing semicolon." + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/README.md b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/README.md new file mode 100644 index 0000000000..dce1837d37 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ExceptionalConditions/README.md @@ -0,0 +1,7 @@ +This directory contains a set of sample SARIF files that illustrate various "exceptional conditions", for example: + +- A log file contains no runs. +- A run failed with an error-level "tool execution notification". +- A run contains no results. + +Developers of applications (such as viewers or bug filing systems) that consume SARIF log files can use the files in this directory to ensure that their application behaves properly under all these conditions. \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/LICENSE-CODE b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/LICENSE-CODE new file mode 100644 index 0000000000..dc08c3597a --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/LICENSE-CODE @@ -0,0 +1,26 @@ +This directory, and (most) of the code in it is copyrighted by Microsoft +Corporation, and was copied from https://github.com/microsoft/sarif-tutorials. +Code in this directory falls under the MIT license in addition to CodeCheckers +license. + +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Notifications.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Notifications.sarif new file mode 100644 index 0000000000..5dbb5a940b --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Notifications.sarif @@ -0,0 +1,167 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample illustrates tool configuration and execution notifications." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0", + "rules": [ + { + "id": "TUT1201", + "name": "MissingSemicolon", + "defaultConfiguration": { + "level": "error" + }, + "messageStrings": { + "default": { + "text": "The statement does not end with a semicolon." + } + } + } + ], + "notifications": [ + { + "id": "TUTN9001", + "name": "unknown-rule", + "defaultConfiguration": { + "level": "warning" + }, + "shortDescription": { + "text": "This notification is triggered when the user supplies a command line argument to enable or disable a rule that does not exist." + }, + "messageStrings": { + "disabled": { + "text": "'{0}' cannot be disabled because this rule does not exist." + }, + "enabled": { + "text": "'{0}' cannot be enabled because this rule does not exist." + } + } + }, + { + "id": "TUTN9002", + "name": "rule-threw-exception", + "defaultConfiguration": { + "level": "error" + }, + "shortDescription": { + "text": "This notification is triggered when an analysis rule throws an exception." + }, + "fullDescription": { + "text": "This notification is triggered when an analysis rule throws an exception while analyzing a file. Depending on the command line options, the rule might either be disabled, or it might continue to run on subsequent files." + }, + "messageStrings": { + "disable": { + "text": "'{0}' threw a '{1}' exception while analyzing file '{2}'. The rule has been disabled." + }, + "continue": { + "text": "'{0}' threw a '{1}' exception while analyzing file '{2}'. The rule will continue to be run on subsequent files." + } + } + } + ] + } + }, + "results": [], + "invocations": [ + { + "executionSuccessful": false, + "toolConfigurationNotifications": [ + { + "descriptor": { + "id": "TUTN9001", + "index": 0 + }, + "message": { + "id": "disabled", + "arguments": [ + "UNK1001" + ] + } + } + ], + "toolExecutionNotifications": [ + { + "descriptor": { + "id": "TUTN9002", + "index": 1 + }, + "message": { + "id": "continue", + "arguments": [ + "TUT1201", + "NullReferenceException", + "example.c" + ] + }, + "exception": { + "kind": "System.NullReferenceException", + "message": "Object reference not set to an instance of an object.", + "stack": { + "frames": [ + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "rules/MissingSemicolon.cs" + }, + "region": { + "startLine": 42 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "SarifSample.Rules.MissingSemicolon.Execute()" + } + ] + } + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "EvaluationEngine.cs" + }, + "region": { + "startLine": 104 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "SarifSample.EvaluationEngine.Run()" + } + ] + } + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "Program.cs" + }, + "region": { + "startLine": 25 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "SarifSample.Program.Main(string[])" + } + ] + } + } + ] + } + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/OriginalUriBaseIds.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/OriginalUriBaseIds.sarif new file mode 100644 index 0000000000..354949f7fe --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/OriginalUriBaseIds.sarif @@ -0,0 +1,103 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample demonstrates the use of originalUriBaseIds." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0" + } + }, + "originalUriBaseIds": { + "REPOROOT": { + "description": { + "text": "The directory into which the repo was cloned." + }, + "properties": { + "comment": "The SARIF producer has chosen not to specify a URI for REPOROOT. See §3.14.14, NOTE 1, for an explanation." + } + }, + "SRCROOT": { + "uri": "src/", + "uriBaseId": "REPOROOT", + "description": { + "text": "The r." + }, + "properties": { + "comment": "SRCROOT is expressed relative to REPOROOT." + } + }, + "LOGSROOT": { + "uri": "file:///C:/logs/", + "description": { + "text": "Destination for tool logs." + }, + "properties": { + "comment": "An originalUriBaseId that resolves directly to an absolute URI." + } + } + }, + "results": [ + { + "ruleId": "TUT1001", + "message": { + "text": "A result outside the source tree." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "README.md", + "uriBaseId": "REPOROOT", + "properties": { + "comment": "If REPOROOT is C:\\project, this file location resolves to C:\\project\\README.md" + } + } + } + } + ] + }, + { + "ruleId": "TUT1002", + "message": { + "text": "A result in a source file." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "io/kb.c", + "uriBaseId": "SRCROOT", + "properties": { + "comment": "If REPOROOT is C:\\project, this file location resolves to C:\\project\\src\\io\\kb.c" + } + } + } + } + ] + }, + { + "ruleId": "TUT1003", + "message": { + "text": "A result in a directory, for instance, 'This repo does not contain a LICENSE file.'." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": ".", + "uriBaseId": "REPOROOT" + } + } + } + ] + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/RegionVariants.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/RegionVariants.sarif new file mode 100644 index 0000000000..28955e94ba --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/RegionVariants.sarif @@ -0,0 +1,430 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample demonstrates the various ways a region can be specified." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0" + } + }, + "results": [ + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specified by all line/column properties. See SARIF Spec §3.30.2, Example 1." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 2, + "endLine": 1, + "endColumn": 4 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specified by all charOffset/charLength properties. See SARIF Spec §3.30.2, Example 1." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "charOffset": 1, + "charLength": 2 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specified by consistent line/column and charOffset/charLength properties. See SARIF Spec §3.30.2, Example 1." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 2, + "endLine": 1, + "endColumn": 4, + "charOffset": 1, + "charLength": 2 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specified by inconsistent line/column and charOffset/charLength properties. See SARIF Spec §3.30.2, Example 2." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endLine": 1, + "endColumn": 4, + "charOffset": 1, + "charLength": 0 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specified by startLine only. See SARIF Spec §3.30.2, Example 3." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "startLine": 2 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, spanning lines. See SARIF Spec §3.30.2, Example 4." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "startLine": 2, + "endLine": 3 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, including line end. See SARIF Spec §3.30.2, Example 5." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "startLine": 2, + "endLine": 3, + "endColumn": 1 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specifying insertion point with line/column properties. See SARIF Spec §3.30.2, Example 6." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 2, + "endColumn": 2 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specifying insertion point with charOffset/charLength properties. See SARIF Spec §3.30.2, Example 6." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "charOffset": 1, + "charLength": 0 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specifying insertion point at start of file with line/column properties. See SARIF Spec §3.30.2, Example 7." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endColumn": 1 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specifying insertion point at start of file with charOffset/charLength properties. See SARIF Spec §3.30.2, Example 7." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "charOffset": 0, + "charLength": 0 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specifying insertion point at end of file with line/column properties. See SARIF Spec §3.30.2, Example 8." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "startLine": 4, + "startColumn": 6, + "endColumn": 6 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specifying insertion point at end of file with charOffset/charLength properties. See SARIF Spec §3.30.2, Example 8." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "charOffset": 22, + "charLength": 0 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in text file, specifying a region with consistent line/column, charOffset/charLength, and byteOffset/byteLength properties. See SARIF Spec §3.30.2, Example 8." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "TextFile.txt", + "index": 0 + }, + "region": { + "startLine": 1, + "startColumn": 2, + "endLine": 1, + "endColumn": 4, + "charOffset": 1, + "charLength": 2, + "byteOffset": 1, + "byteLength": 2 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in binary file, specifying a region of non-zero length." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "BinaryFile.bin", + "index": 1 + }, + "region": { + "byteOffset": 1, + "byteLength": 2 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in binary file, specifying an insertion point." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "BinaryFile.bin", + "index": 1 + }, + "region": { + "byteOffset": 1, + "byteLength": 0 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in binary file, specifying an insertion point at the beginning of the file." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "BinaryFile.bin", + "index": 1 + }, + "region": { + "byteOffset": 0, + "byteLength": 0 + } + } + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "Result in binary file, specifying an insertion point at the end of the file." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "BinaryFile.txt", + "index": 1 + }, + "region": { + "byteOffset": 22, + "byteLength": 0 + } + } + } + ] + } + ], + "artifacts": [ + { + "location": { + "uri": "TextFile.txt", + "description": { + "text": "A text file used to demonstrate how to specify text regions." + } + }, + "contents": { + "text": "abcd\r\nefg\r\nhijk\r\nlmn\r\n", + "properties": { + "comment": "The file contents and some of the examples are taken from the SARIF specification, §3.30.2 ('Text regions') and §3.30.4 ('Independence of text and binary regions')." + } + }, + "encoding": "UTF-8" + }, + { + "location": { + "uri": "BinaryFile.bin", + "description": { + "text": "A binary file used to demonstrate how to specify binary regions." + } + }, + "contents": { + "binary": "AQIDBAUGBwgJCgsMDQ4PEA==" + }, + "properties": { + "comment": "The MIME Base64 encoding of the byte array { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10 }." + } + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ResultStacks.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ResultStacks.sarif new file mode 100644 index 0000000000..1d1d72f14d --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/ResultStacks.sarif @@ -0,0 +1,127 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample demonstrates the use of stacks to provide information about a result." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0" + } + }, + "results": [ + { + "ruleId": "TUT1001", + "message": { + "text": "Uninitialized variable." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "collections/list.h", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 15 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "collections::list::add" + } + ] + } + ], + "stacks": [ + { + "message": { + "text": "Call stack resulting from usage of uninitialized variable." + }, + "frames": [ + { + "location": { + "message": { + "text": "Exception thrown." + }, + "physicalLocation": { + "artifactLocation": { + "uri": "collections/list.h", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 110, + "startColumn": 15 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "collections::list::add_core" + } + ] + }, + "module": "platform", + "threadId": 52, + "parameters": [ + "null", + "0", + "14" + ] + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "collections/list.h", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 43, + "startColumn": 15 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "collections::list::add" + } + ] + }, + "module": "platform", + "threadId": 52, + "parameters": [ + "14" + ] + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "application/main.cpp", + "uriBaseId": "SRCROOT" + }, + "region": { + "startLine": 28, + "startColumn": 9 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "main" + } + ] + }, + "module": "application", + "threadId": 52 + } + ] + } + ] + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/RuleMetadata.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/RuleMetadata.sarif new file mode 100644 index 0000000000..8274c28808 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/RuleMetadata.sarif @@ -0,0 +1,71 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample demonstrates rule metadata and the connection between results and the metadata for the corresponding rules." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0", + "rules": [ + { + "id": "TUT1001", + "name": "InvalidUri", + "defaultConfiguration": { + "level": "error" + }, + "shortDescription": { + "text": "Properties defined with the 'uri' or 'uri-reference' format must contain valid URIs.", + "markdown": "Properties defined with the `uri` or `uri-reference` format must contain valid URIs." + }, + "fullDescription": { + "text": "Every JSON property whose value is defined by the schema to be a URI (with \"format\": \"uri\" or \"format\": \"uri-reference\") must contain a valid URI.", + "markdown": "Every JSON property whose value is defined by the schema to be a URI (with `\"format\": \"uri\"` or `\"format\": \"uri-reference\"`) must contain a valid URI." + }, + "messageStrings": { + "default": { + "text": "The URI '{0}' is invalid.", + "markdown": "The URI `{0}` is invalid." + } + } + } + ] + } + }, + "results": [ + { + "properties": { + "comment": "The ruleIndex property points into the array tool.driver.rules." + }, + "ruleId": "TUT1001", + "level": "error", + "ruleIndex": 0, + "message": { + "id": "default", + "arguments": [ + "//C:/code/dev" + ] + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "test.json" + }, + "region": { + "startLine": 15, + "startColumn": 8, + "endColumn": 22 + } + } + } + ] + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Suppressions.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Suppressions.sarif new file mode 100644 index 0000000000..6d6a0e32aa --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Suppressions.sarif @@ -0,0 +1,131 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample demonstrates how suppressions affect the visibility of results in SARIF viewers." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0" + } + }, + "results": [ + { + "ruleId": "TUT1001", + "message": { + "text": "This result is visible because it is not suppressed." + } + }, + { + "ruleId": "TUT1001", + "message": { + "text": "This result is hidden because it is suppressed in source." + }, + "suppressions": [ + { + "kind": "inSource" + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "This result is hidden because it is suppressed externally." + }, + "suppressions": [ + { + "kind": "external" + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "This result is visible because its suppression was rejected." + }, + "suppressions": [ + { + "kind": "external", + "status": "rejected" + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "This result is hidden because its suppression was accepted." + }, + "suppressions": [ + { + "kind": "external", + "status": "accepted" + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "This result is visible because its suppression is still under review." + }, + "suppressions": [ + { + "kind": "external", + "status": "underReview" + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "This result is hidden because at least one suppression was accepted." + }, + "suppressions": [ + { + "kind": "external", + "status": "accepted" + }, + { + "kind": "external", + "status": "rejected" + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "This result is hidden because it has at least one unqualified suppression." + }, + "suppressions": [ + { + "kind": "external" + }, + { + "kind": "external", + "status": "rejected" + } + ] + }, + { + "ruleId": "TUT1001", + "message": { + "text": "This result is hidden because at least one suppression has not yet been rejected (it is still under review)." + }, + "suppressions": [ + { + "kind": "external", + "status": "underReview" + }, + { + "kind": "external", + "status": "rejected" + } + ] + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Taxonomies.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Taxonomies.sarif new file mode 100644 index 0000000000..1d9fbe1562 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/Taxonomies.sarif @@ -0,0 +1,130 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This sample demonstrates the use of taxonomies to classify rules." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples", + "version": "1.0", + "rules": [ + { + "id": "TUT0001", + "defaultConfiguration": { + "level": "error" + }, + "relationships": [ + { + "target": { + "id": "RQL1001", + "index": 0, + "toolComponent": { + "name": "Requirement levels", + "guid": "1A567403-868F-405E-92CF-771A9ECB03A1", + "index": 0 + } + }, + "kinds": [ + "superset" + ], + "description": { + "text": "This relationship specifies that this rule is classified as 'Required'." + } + } + ] + }, + { + "id": "TUT0002", + "defaultConfiguration": { + "level": "warning" + }, + "relationships": [ + { + "target": { + "id": "RQL1002", + "index": 1, + "toolComponent": { + "name": "Requirement levels", + "guid": "1A567403-868F-405E-92CF-771A9ECB03A1", + "index": 0 + } + }, + "kinds": [ + "superset" + ], + "description": { + "text": "This relationship specifies that this rule is classified as 'Recommended'." + } + } + ] + } + ] + } + }, + "taxonomies": [ + { + "guid": "1A567403-868F-405E-92CF-771A9ECB03A1", + "name": "Requirement levels", + "shortDescription": { + "text": "This taxonomy classifies rules according to whether their use is required or recommended by company policy." + }, + "taxa": [ + { + "id": "RQL1001", + "name": "Required", + "shortDescription": { + "text": "Rules in this category are required by company policy. All violations must be fixed unless an exemption is granted." + } + }, + { + "id": "RQL1002", + "name": "Recommended", + "shortDescription": { + "text": "Rules in this category are recommended but not required by company policy. Violations should be fixed but an exemption is not required to suppress a result." + } + } + ] + } + ], + "results": [ + { + "ruleId": "TUT1001", + "ruleIndex": 0, + "level": "error", + "message": { + "text": "This result violates a rule that is classified as 'Required'." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///C:/code/myProject/io/kb.c" + } + } + } + ] + }, + { + "ruleId": "TUT1002", + "ruleIndex": 1, + "message": { + "text": "This result violates a rule that is classified as 'Recommended'." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///C:/code/myProject/io/file.c" + } + } + } + ] + } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/TextFile.txt b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/TextFile.txt new file mode 100644 index 0000000000..7898192261 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/TextFile.txt @@ -0,0 +1 @@ +a diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/_template.sarif b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/_template.sarif new file mode 100644 index 0000000000..ebf4d4181c --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/_template.sarif @@ -0,0 +1,24 @@ +{ + "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json", + "version": "2.1.0", + "properties": { + "comment": "This is a template that you can copy and modify to create other samples." + }, + "runs": [ + { + "tool": { + "driver": { + "name": "SarifSamples" + } + }, + "results": [ + { + "ruleId": "TUT1001", + "message": { + "text": "The result." + } } + ], + "columnKind": "utf16CodeUnits" + } + ] +} \ No newline at end of file diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/code/example.cs b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/code/example.cs new file mode 100644 index 0000000000..7898192261 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/code/example.cs @@ -0,0 +1 @@ +a diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/collections/example.cs b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/collections/example.cs new file mode 100644 index 0000000000..7898192261 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/collections/example.cs @@ -0,0 +1 @@ +a diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/collections/list.h b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/collections/list.h new file mode 100644 index 0000000000..7898192261 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/collections/list.h @@ -0,0 +1 @@ +a diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/default.txt b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/default.txt new file mode 100644 index 0000000000..7898192261 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/default.txt @@ -0,0 +1 @@ +a diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/explicit.txt b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/explicit.txt new file mode 100644 index 0000000000..7898192261 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/explicit.txt @@ -0,0 +1 @@ +a diff --git a/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/test.json b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/test.json new file mode 100644 index 0000000000..7898192261 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/sarif_test_files/test.json @@ -0,0 +1 @@ +a diff --git a/tools/report-converter/tests/unit/parser/sarif/test_sarif_parser.py b/tools/report-converter/tests/unit/parser/sarif/test_sarif_parser.py new file mode 100644 index 0000000000..b5ad59ffb9 --- /dev/null +++ b/tools/report-converter/tests/unit/parser/sarif/test_sarif_parser.py @@ -0,0 +1,43 @@ +# ------------------------------------------------------------------------- +# +# Part of the CodeChecker project, under the Apache License v2.0 with +# LLVM Exceptions. See LICENSE for license information. +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +# +# ------------------------------------------------------------------------- + +""" +Test the parsing of the sarif files. +""" + + +import os +import unittest + +from codechecker_report_converter.report import report_file + + +gen_sarif_dir_path = os.path.join( + os.path.dirname(__file__), 'sarif_test_files', 'gen_sarif') + + +class SarifParserTestCase(unittest.TestCase): + """Test the parsing of the sarif generated by multiple clang versions.""" + + @classmethod + def setup_class(cls): + """Initialize test source file.""" + # Already generated sarif files for the tests. + cls.__this_dir = os.path.dirname(__file__) + cls.__sarif_test_files = os.path.join( + cls.__this_dir, 'sarif_test_files') + + def test_no_crash(self): + """There was no bug in the checked file.""" + for root, _, files in os.walk(self.__sarif_test_files): + for filename in files: + if not filename.endswith(".sarif"): + continue + abs_filename = os.path.join(root, filename) + # We test for no crashes. + report_file.get_reports(abs_filename) diff --git a/web/requirements.txt b/web/requirements.txt index 30e911a2a6..973f65fe55 100644 --- a/web/requirements.txt +++ b/web/requirements.txt @@ -7,6 +7,7 @@ mypy_extensions==0.4.3 thrift==0.13.0 gitpython==3.1.35 PyYAML==6.0.1 +sarif-tools==1.0.0 ./api/py/codechecker_api/dist/codechecker_api.tar.gz ./api/py/codechecker_api_shared/dist/codechecker_api_shared.tar.gz