diff --git a/tools/report-converter/codechecker_report_converter/analyzers/seqra/__init__.py b/tools/report-converter/codechecker_report_converter/analyzers/seqra/__init__.py new file mode 100644 index 0000000000..4259749345 --- /dev/null +++ b/tools/report-converter/codechecker_report_converter/analyzers/seqra/__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/seqra/analyzer_result.py b/tools/report-converter/codechecker_report_converter/analyzers/seqra/analyzer_result.py new file mode 100644 index 0000000000..e02a379529 --- /dev/null +++ b/tools/report-converter/codechecker_report_converter/analyzers/seqra/analyzer_result.py @@ -0,0 +1,31 @@ +# ------------------------------------------------------------------------- +# +# 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 List + +from codechecker_report_converter.report import 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 Seqra.""" + + TOOL_NAME = 'seqra' + NAME = 'Seqra Security-Focused Static Analyzer' + URL = 'https://seqra.dev/' + + def get_reports(self, file_path: str) -> List[Report]: + """ Get reports from the given analyzer result file. """ + + return sarif.Parser().get_reports(file_path) diff --git a/tools/report-converter/tests/unit/analyzers/seqra_output_test_files/UserProfileController.expected.plist b/tools/report-converter/tests/unit/analyzers/seqra_output_test_files/UserProfileController.expected.plist new file mode 100644 index 0000000000..18aa89919d --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/seqra_output_test_files/UserProfileController.expected.plist @@ -0,0 +1,251 @@ + + + + + diagnostics + + + category + unknown + check_name + seqra.java.spring.xss + description + Controller returns an untrusted unvalidated data + issue_hash_content_of_line_in_context + 9912049596cf713fc0bdaee7280274d8 + location + + col + 1 + file + 0 + line + 18 + + path + + + edges + + + end + + + col + 1 + file + 0 + line + 18 + + + col + 1 + file + 0 + line + 18 + + + start + + + col + 1 + file + 0 + line + 17 + + + col + 1 + file + 0 + line + 17 + + + + + kind + control + + + depth + 0 + kind + event + location + + col + 1 + file + 0 + line + 17 + + message + Method entry marks "message" as $PARAM + ranges + + + + col + 1 + file + 0 + line + 17 + + + col + 1 + file + 0 + line + 17 + + + + + + depth + 0 + kind + event + location + + col + 1 + file + 0 + line + 18 + + message + Takes $PARAM data at "message" and ends up with $PARAM data at a local variable + ranges + + + + col + 1 + file + 0 + line + 18 + + + col + 1 + file + 0 + line + 18 + + + + + + depth + 0 + kind + event + location + + col + 1 + file + 0 + line + 18 + + message + The returning value is assigned a value with $PARAM data + ranges + + + + col + 1 + file + 0 + line + 18 + + + col + 1 + file + 0 + line + 18 + + + + + + depth + 0 + kind + event + location + + col + 1 + file + 0 + line + 18 + + message + Controller returns an untrusted unvalidated data + ranges + + + + col + 1 + file + 0 + line + 18 + + + col + 1 + file + 0 + line + 18 + + + + + + type + seqra + + + files + + files/UserProfileController.java + + metadata + + analyzer + + name + seqra + + generated_by + + name + report-converter + version + x.y.z + + + + diff --git a/tools/report-converter/tests/unit/analyzers/seqra_output_test_files/UserProfileController.java.sarif b/tools/report-converter/tests/unit/analyzers/seqra_output_test_files/UserProfileController.java.sarif new file mode 100644 index 0000000000..6c7fe14524 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/seqra_output_test_files/UserProfileController.java.sarif @@ -0,0 +1,208 @@ +{ + "version": "2.1.0", + "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "SAST", + "organization": "Seqra", + "version": "1.4.1", + "rules": [ + { + "id": "seqra.java.spring.xss", + "name": "seqra.java.spring.xss", + "defaultConfiguration": { + "level": "error" + }, + "fullDescription": { + "text": "Controller returns an untrusted unvalidated data" + }, + "shortDescription": { + "text": "Controller returns an untrusted unvalidated data" + }, + "properties": { + "tags": [ + "CWE-79" + ] + } + } + ] + } + }, + "results": [ + { + "level": "error", + "message": { + "text": "Controller returns an untrusted unvalidated data" + }, + "ruleId": "seqra.java.spring.xss", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "UserProfileController.java", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": 18 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "org.example.UserProfileController#displayUserProfile", + "decoratedName": "(id:69)org.example.UserProfileController#displayUserProfile(java.lang.String):1:(return %0)" + } + ] + } + ], + "relatedLocations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "UserProfileController.java" + }, + "region": { + "startLine": 18 + } + }, + "logicalLocations": [ + { + "name": "org.example.UserProfileController#displayUserProfile", + "fullyQualifiedName": "GET /profile/display", + "kind": "function" + } + ], + "message": { + "text": "Related Spring controller" + } + } + ], + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "UserProfileController.java", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": 17 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "org.example.UserProfileController#displayUserProfile", + "decoratedName": "(id:69)org.example.UserProfileController#displayUserProfile(java.lang.String):0:(goto JIRInstRef(index=2))" + } + ], + "message": { + "text": "Method entry marks \"message\" as $PARAM" + } + }, + "executionOrder": 0, + "index": 0, + "kinds": [ + "taint" + ] + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "UserProfileController.java", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": 18 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "org.example.UserProfileController#displayUserProfile", + "decoratedName": "(id:69)org.example.UserProfileController#displayUserProfile(java.lang.String):4:(%0 = %str)" + } + ], + "message": { + "text": "Takes $PARAM data at \"message\" and ends up with $PARAM data at a local variable" + } + }, + "executionOrder": 1, + "index": 1, + "kinds": [ + "unknown" + ] + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "UserProfileController.java", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": 18 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "org.example.UserProfileController#displayUserProfile", + "decoratedName": "(id:69)org.example.UserProfileController#displayUserProfile(java.lang.String):1:(return %0)" + } + ], + "message": { + "text": "The returning value is assigned a value with $PARAM data" + } + }, + "executionOrder": 2, + "index": 2, + "kinds": [ + "unknown" + ] + }, + { + "location": { + "physicalLocation": { + "artifactLocation": { + "uri": "UserProfileController.java", + "uriBaseId": "%SRCROOT%" + }, + "region": { + "startLine": 18 + } + }, + "logicalLocations": [ + { + "fullyQualifiedName": "org.example.UserProfileController#displayUserProfile", + "decoratedName": "(id:69)org.example.UserProfileController#displayUserProfile(java.lang.String):1:(return %0)" + } + ], + "message": { + "text": "Controller returns an untrusted unvalidated data" + } + }, + "executionOrder": 3, + "index": 3, + "kinds": [ + "taint" + ] + } + ] + } + ] + } + ] + } + ], + "originalUriBaseIds": { + "%SRCROOT%": { + "uri": "./" + } + } + } + ] +} diff --git a/tools/report-converter/tests/unit/analyzers/seqra_output_test_files/files/UserProfileController.java b/tools/report-converter/tests/unit/analyzers/seqra_output_test_files/files/UserProfileController.java new file mode 100644 index 0000000000..9822a0b0c0 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/seqra_output_test_files/files/UserProfileController.java @@ -0,0 +1,20 @@ +package org.example; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.util.HtmlUtils; + +@Controller +public class UserProfileController { + + // Display user profile with custom message + @GetMapping("/profile/display") + @ResponseBody + public String displayUserProfile( + @RequestParam(defaultValue = "Welcome") String message) { + // Direct output without escaping + return "

Profile Message: " + message + "

"; + } +} diff --git a/tools/report-converter/tests/unit/analyzers/test_seqra_parser.py b/tools/report-converter/tests/unit/analyzers/test_seqra_parser.py new file mode 100644 index 0000000000..a4215c77d6 --- /dev/null +++ b/tools/report-converter/tests/unit/analyzers/test_seqra_parser.py @@ -0,0 +1,106 @@ +# ------------------------------------------------------------------------- +# +# 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 SeqraAnalyzerResult, which +used in sequence transform seqra output to a plist file. +""" + + +import os +import plistlib +import shutil +import unittest + +from codechecker_report_converter.analyzers.seqra import analyzer_result +from codechecker_report_converter.report.parser import plist + +from libtest import env + + +OLD_PWD = None + + +class SeqraAnalyzerResultTestCase(unittest.TestCase): + """ Test the output of the SeqraAnalyzerResult. """ + + def setup_class(self): + """ Initialize test files. """ + + global TEST_WORKSPACE + TEST_WORKSPACE = env.get_workspace('seqra_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__), + 'seqra_output_test_files') + global OLD_PWD + OLD_PWD = os.getcwd() + os.chdir(os.path.join(os.path.dirname(__file__), + 'seqra_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', + 'UserProfileController.java') + + 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_seqra_transform_single_file(self): + """ Test transforming single plist file. """ + analyzer_output_file = os.path.join( + self.test_files, 'UserProfileController.java.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, + 'UserProfileController.java_seqra.plist') + with open(plist_file, mode='rb') as pfile: + res = plistlib.load(pfile) + + # Use relative path for this test. + res['files'][0] = 'files/UserProfileController.java' + + 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, + 'UserProfileController.expected.plist') + with open(plist_file, mode='rb') as pfile: + exp = plistlib.load(pfile) + + self.assertEqual(res, exp)