Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion analyzer/codechecker_analyzer/cli/parse.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ def get_output_file_path(default_file_name: str) -> Optional[str]:
data = gerrit.convert(all_reports)
dump_json_output(data, get_output_file_path("reports.json"))
elif export == 'sarif':
data = sarif.convert(all_reports)
data = sarif.convert(all_reports, context.checker_labels)
dump_json_output(data, get_output_file_path("reports.json"))
elif export == 'baseline':
data = baseline.convert(all_reports)
Expand Down
11 changes: 11 additions & 0 deletions analyzer/tests/functional/parse_sarif/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# coding=utf-8
# -------------------------------------------------------------------------
#
# 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.
7 changes: 7 additions & 0 deletions analyzer/tests/functional/parse_sarif/test_files/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
default: a.o

a.o: a.cpp
$(CXX) -c a.cpp -o /dev/null

clean:
rm -f a.o
9 changes: 9 additions & 0 deletions analyzer/tests/functional/parse_sarif/test_files/a.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
int main()
{
int x;
// codechecker_false_positive [DeadStores] testing suppression via source code comment
x = 1;

// Suppressed by config file.
return 1 / 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
$version: 1
rules:
- filters:
checker_name: core.DivideZero
actions:
review_status: intentional
reason: testing suppression via config file
179 changes: 179 additions & 0 deletions analyzer/tests/functional/parse_sarif/test_sarif.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
#
# -------------------------------------------------------------------------
#
# 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 parse --export sarif command.
"""

import os
import json
import shutil
import subprocess
import unittest

from libtest import env


class TestParseSarif(unittest.TestCase):
_ccClient = None

def setup_class(self):
"""Setup the environment for the tests."""

global TEST_WORKSPACE
TEST_WORKSPACE = env.get_workspace('skip')

report_dir = os.path.join(TEST_WORKSPACE, 'reports')
os.makedirs(report_dir)

os.environ['TEST_WORKSPACE'] = TEST_WORKSPACE

def teardown_class(self):
"""Delete the workspace associated with this test"""

# TODO: If environment variable is set keep the workspace
# and print out the path.
global TEST_WORKSPACE

print("Removing: " + TEST_WORKSPACE)
shutil.rmtree(TEST_WORKSPACE)

def setup_method(self, _):

# TEST_WORKSPACE is automatically set by test package __init__.py .
self.test_workspace = os.environ['TEST_WORKSPACE']

test_class = self.__class__.__name__
print('Running ' + test_class + ' tests in ' + self.test_workspace)

# Get the CodeChecker cmd if needed for the tests.
self._codechecker_cmd = env.codechecker_cmd()
self._tu_collector_cmd = env.tu_collector_cmd()
self.report_dir = os.path.join(self.test_workspace, "reports")
self.test_dir = os.path.join(os.path.dirname(__file__), 'test_files')

def __run_cmd(self, cmd):
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=self.test_dir,
encoding="utf-8",
errors="ignore")
out, err = process.communicate()

print(out)
print(err)

if process.returncode != (2 if "parse" in cmd else 0):
return err

return ''.join(out)

def __log_and_analyze(self, analyze_args=None):
""" Log and analyze the test project. """
build_json = os.path.join(self.test_workspace, "build.json")

clean_cmd = ["make", "clean"]
out = subprocess.check_output(clean_cmd,
cwd=self.test_dir,
encoding="utf-8", errors="ignore")
print(out)

# Create and run log command.
log_cmd = [self._codechecker_cmd, "log", "-b", "make",
"-o", build_json]
out = subprocess.check_output(log_cmd,
cwd=self.test_dir,
encoding="utf-8", errors="ignore")
print(out)

# Create and run analyze command.
analyze_cmd = [
self._codechecker_cmd, "analyze", "-c", build_json,
"--analyzers", "clangsa", "-o", self.report_dir]

analyze_cmd.extend(analyze_args or [])

self.__run_cmd(analyze_cmd)

def test_parse_sarif_rules(self):
self.__log_and_analyze()

parse_sarif_cmd = [self._codechecker_cmd, "parse", self.report_dir,
"-e", "sarif"]
out = self.__run_cmd(parse_sarif_cmd)
parsed_json = json.loads(out)

rules = parsed_json['runs'][0]['tool']['driver']['rules']
self.assertEqual(len(rules), 1)

self.assertEqual(rules[0], {
'id': 'core.DivideZero',
'fullDescription': {
'text': 'Division by zero'
},
# pylint: disable-next=line-too-long
'helpUri': 'https://clang.llvm.org/docs/analyzer/checkers.html#core-dividezero-c-c-objc', # noqa
'defaultConfiguration': {
'level': 'error'
}
})

def test_parse_sarif_suppression_source(self):
self.__log_and_analyze()

parse_sarif_cmd = [self._codechecker_cmd, "parse", self.report_dir,
"-e", "sarif", "--review-status", "false_positive"]
out = self.__run_cmd(parse_sarif_cmd)
parsed_json = json.loads(out)

res = parsed_json['runs'][0]['results'][0]

self.assertIn('suppressions', res)
sups = res['suppressions']
self.assertEqual(len(sups), 1)
suppression = sups[0]

self.assertIn('kind', suppression)
self.assertIn('status', suppression)
self.assertIn('justification', suppression)

self.assertEqual(suppression, {
'kind': 'inSource',
'status': 'accepted',
'justification': 'testing suppression via source code comment',
})

def test_parse_sarif_suppression_rs_config(self):
self.__log_and_analyze([
"--review-status-config", "review_status.yaml"
])

parse_sarif_cmd = [self._codechecker_cmd, "parse", self.report_dir,
"-e", "sarif", "--review-status", "intentional"]
out = self.__run_cmd(parse_sarif_cmd)
parsed_json = json.loads(out)

res = parsed_json['runs'][0]['results'][0]

self.assertIn('suppressions', res)
sups = res['suppressions']
self.assertEqual(len(sups), 1)
suppression = sups[0]

self.assertIn('kind', suppression)
self.assertIn('status', suppression)
self.assertIn('justification', suppression)

self.assertEqual(suppression, {
'kind': 'external',
'status': 'accepted',
'justification': 'testing suppression via config file',
})
2 changes: 1 addition & 1 deletion codechecker_common/review_status_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def get_review_status_from_config(
.encode(encoding='utf-8', errors='ignore')
if 'reason' in rule['actions'] else b'',
bug_hash=report.report_hash or "",
in_source=True)
in_source=False)

return None

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from typing import Dict, List
from typing import Dict, List, Optional

from codechecker_report_converter.report import Report
from codechecker_report_converter.report.checker_labels import CheckerLabels
from codechecker_report_converter.report.parser import sarif


def convert(reports: List[Report]) -> Dict:
sarif_parser = sarif.Parser()
def convert(
reports: List[Report],
checker_labels: Optional[CheckerLabels] = None
) -> Dict:
sarif_parser = sarif.Parser(checker_labels=checker_labels)
return sarif_parser.convert(reports)
Original file line number Diff line number Diff line change
Expand Up @@ -310,17 +310,35 @@ def convert(
""" Converts the given reports to sarif format. """
tool_name, tool_version = get_tool_info()

checker_labels = self._checker_labels

rules = {}
results = []
for report in reports:
if report.checker_name not in rules:
rules[report.checker_name] = {
"id": report.checker_name,
if (checker_name := report.checker_name) not in rules:
rules[checker_name] = {
"id": checker_name,
"fullDescription": {
"text": report.message
}
}

if checker_labels:
doc_url = checker_labels.label_of_checker(
checker_name, "doc_url", report.analyzer_name)
if doc_url:
if isinstance(doc_url, list):
doc_url = doc_url[0]
rules[checker_name]["helpUri"] = doc_url

severity = checker_labels.severity(
checker_name,
report.analyzer_name) # type: ignore[call-arg]
if severity:
rules[checker_name]["defaultConfiguration"] = {
"level": self._to_level(severity)
}

results.append(self._create_result(report))

return {
Expand Down Expand Up @@ -359,6 +377,25 @@ def _create_result(self, report: Report) -> Dict:
}]
}

# Set `suppressions` property.
if (rs := report.review_status) and rs.status != "unreviewed":
suppression = {}

if rs.in_source:
suppression["kind"] = "inSource"
else:
suppression["kind"] = "external"

if rs.status in ["suppress", "false_positive", "intentional"]:
suppression["status"] = "accepted"
elif rs.status == "confirmed":
suppression["status"] = "rejected"

suppression["justification"] = rs.message.decode('utf-8')

result["suppressions"] = [suppression]

# Set `codeFlows` property.
locations = []

if report.bug_path_events:
Expand Down Expand Up @@ -439,6 +476,16 @@ def _create_location(
}
}

def _to_level(self, severity: str) -> str:
""" Map severity label value to level (§3.50.3). """
if severity in ["STYLE", "LOW"]:
return "note"
elif severity == "MEDIUM":
return "warning"
elif severity in ["HIGH", "CRITICAL"]:
return "error"
return "warning"

def write(self, data: Any, output_file_path: str):
""" Creates an analyzer output file from the given data. """
try:
Expand Down