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)