Skip to content

Commit a8f1923

Browse files
committed
[feat] Add Seqra report converter
1 parent 7fde508 commit a8f1923

5 files changed

Lines changed: 372 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# -------------------------------------------------------------------------
2+
#
3+
# Part of the CodeChecker project, under the Apache License v2.0 with
4+
# LLVM Exceptions. See LICENSE for license information.
5+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
#
7+
# -------------------------------------------------------------------------
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# -------------------------------------------------------------------------
2+
#
3+
# Part of the CodeChecker project, under the Apache License v2.0 with
4+
# LLVM Exceptions. See LICENSE for license information.
5+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
#
7+
# -------------------------------------------------------------------------
8+
9+
import logging
10+
from typing import List
11+
12+
from codechecker_report_converter.report import Report
13+
from codechecker_report_converter.report.parser import sarif
14+
15+
from ..analyzer_result import AnalyzerResultBase
16+
17+
18+
LOG = logging.getLogger('report-converter')
19+
20+
21+
class AnalyzerResult(AnalyzerResultBase):
22+
""" Transform analyzer result of the Seqra."""
23+
24+
TOOL_NAME = 'seqra'
25+
NAME = 'Seqra Security-Focused Static Analyzer'
26+
URL = 'https://seqra.dev/'
27+
28+
def get_reports(self, file_path: str) -> List[Report]:
29+
""" Get reports from the given analyzer result file. """
30+
31+
return sarif.Parser().get_reports(file_path)
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
{
2+
"version": "2.1.0",
3+
"$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
4+
"runs": [
5+
{
6+
"tool": {
7+
"driver": {
8+
"name": "SAST",
9+
"organization": "Seqra",
10+
"version": "2025.11.13.98f5209",
11+
"rules": [
12+
{
13+
"id": "seqra.java.spring.xss",
14+
"name": "seqra.java.spring.xss",
15+
"defaultConfiguration": {
16+
"level": "error"
17+
},
18+
"fullDescription": {
19+
"text": "Controller returns an untrusted unvalidated data"
20+
},
21+
"shortDescription": {
22+
"text": "Controller returns an untrusted unvalidated data"
23+
},
24+
"properties": {
25+
"tags": [
26+
"CWE-79"
27+
]
28+
}
29+
}
30+
]
31+
}
32+
},
33+
"results": [
34+
{
35+
"level": "error",
36+
"message": {
37+
"text": "Controller returns an untrusted unvalidated data"
38+
},
39+
"ruleId": "seqra.java.spring.xss",
40+
"locations": [
41+
{
42+
"physicalLocation": {
43+
"artifactLocation": {
44+
"uri": "UserProfileController.java",
45+
"uriBaseId": "%SRCROOT%"
46+
},
47+
"region": {
48+
"startLine": 18
49+
}
50+
},
51+
"logicalLocations": [
52+
{
53+
"fullyQualifiedName": "org.example.UserProfileController#displayUserProfile",
54+
"decoratedName": "(id:69)org.example.UserProfileController#displayUserProfile(java.lang.String):1:(return %0)"
55+
}
56+
]
57+
}
58+
],
59+
"relatedLocations": [
60+
{
61+
"physicalLocation": {
62+
"artifactLocation": {
63+
"uri": "UserProfileController.java"
64+
},
65+
"region": {
66+
"startLine": 18
67+
}
68+
},
69+
"logicalLocations": [
70+
{
71+
"name": "org.example.UserProfileController#displayUserProfile",
72+
"fullyQualifiedName": "GET /profile/display",
73+
"kind": "function"
74+
}
75+
],
76+
"message": {
77+
"text": "Related Spring controller"
78+
}
79+
}
80+
],
81+
"codeFlows": [
82+
{
83+
"threadFlows": [
84+
{
85+
"locations": [
86+
{
87+
"location": {
88+
"physicalLocation": {
89+
"artifactLocation": {
90+
"uri": "UserProfileController.java",
91+
"uriBaseId": "%SRCROOT%"
92+
},
93+
"region": {
94+
"startLine": 17
95+
}
96+
},
97+
"logicalLocations": [
98+
{
99+
"fullyQualifiedName": "org.example.UserProfileController#displayUserProfile",
100+
"decoratedName": "(id:69)org.example.UserProfileController#displayUserProfile(java.lang.String):0:(goto JIRInstRef(index=2))"
101+
}
102+
],
103+
"message": {
104+
"text": "Method entry marks \"message\" as $PARAM"
105+
}
106+
},
107+
"executionOrder": 0,
108+
"index": 0,
109+
"kinds": [
110+
"taint"
111+
]
112+
},
113+
{
114+
"location": {
115+
"physicalLocation": {
116+
"artifactLocation": {
117+
"uri": "UserProfileController.java",
118+
"uriBaseId": "%SRCROOT%"
119+
},
120+
"region": {
121+
"startLine": 18
122+
}
123+
},
124+
"logicalLocations": [
125+
{
126+
"fullyQualifiedName": "org.example.UserProfileController#displayUserProfile",
127+
"decoratedName": "(id:69)org.example.UserProfileController#displayUserProfile(java.lang.String):4:(%0 = %str)"
128+
}
129+
],
130+
"message": {
131+
"text": "Takes $PARAM data at \"message\" and ends up with $PARAM data at a local variable"
132+
}
133+
},
134+
"executionOrder": 1,
135+
"index": 1,
136+
"kinds": [
137+
"unknown"
138+
]
139+
},
140+
{
141+
"location": {
142+
"physicalLocation": {
143+
"artifactLocation": {
144+
"uri": "UserProfileController.java",
145+
"uriBaseId": "%SRCROOT%"
146+
},
147+
"region": {
148+
"startLine": 18
149+
}
150+
},
151+
"logicalLocations": [
152+
{
153+
"fullyQualifiedName": "org.example.UserProfileController#displayUserProfile",
154+
"decoratedName": "(id:69)org.example.UserProfileController#displayUserProfile(java.lang.String):1:(return %0)"
155+
}
156+
],
157+
"message": {
158+
"text": "The returning value is assigned a value with $PARAM data"
159+
}
160+
},
161+
"executionOrder": 2,
162+
"index": 2,
163+
"kinds": [
164+
"unknown"
165+
]
166+
},
167+
{
168+
"location": {
169+
"physicalLocation": {
170+
"artifactLocation": {
171+
"uri": "UserProfileController.java",
172+
"uriBaseId": "%SRCROOT%"
173+
},
174+
"region": {
175+
"startLine": 18
176+
}
177+
},
178+
"logicalLocations": [
179+
{
180+
"fullyQualifiedName": "org.example.UserProfileController#displayUserProfile",
181+
"decoratedName": "(id:69)org.example.UserProfileController#displayUserProfile(java.lang.String):1:(return %0)"
182+
}
183+
],
184+
"message": {
185+
"text": "Controller returns an untrusted unvalidated data"
186+
}
187+
},
188+
"executionOrder": 3,
189+
"index": 3,
190+
"kinds": [
191+
"taint"
192+
]
193+
}
194+
]
195+
}
196+
]
197+
}
198+
]
199+
}
200+
],
201+
"originalUriBaseIds": {
202+
"%SRCROOT%": {
203+
"uri": "./"
204+
}
205+
}
206+
}
207+
]
208+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.example;
2+
3+
import org.springframework.stereotype.Controller;
4+
import org.springframework.web.bind.annotation.GetMapping;
5+
import org.springframework.web.bind.annotation.RequestParam;
6+
import org.springframework.web.bind.annotation.ResponseBody;
7+
import org.springframework.web.util.HtmlUtils;
8+
9+
@Controller
10+
public class UserProfileController {
11+
12+
// Display user profile with custom message
13+
@GetMapping("/profile/display")
14+
@ResponseBody
15+
public String displayUserProfile(
16+
@RequestParam(defaultValue = "Welcome") String message) {
17+
// Direct output without escaping
18+
return "<html><body><h1>Profile Message: " + message + "</h1></body></html>";
19+
}
20+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# -------------------------------------------------------------------------
2+
#
3+
# Part of the CodeChecker project, under the Apache License v2.0 with
4+
# LLVM Exceptions. See LICENSE for license information.
5+
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
#
7+
# -------------------------------------------------------------------------
8+
9+
"""
10+
This module tests the correctness of the SeqraAnalyzerResult, which
11+
used in sequence transform seqra output to a plist file.
12+
"""
13+
14+
15+
import os
16+
import plistlib
17+
import shutil
18+
import unittest
19+
20+
from codechecker_report_converter.analyzers.seqra import analyzer_result
21+
from codechecker_report_converter.report.parser import plist
22+
23+
from libtest import env
24+
25+
26+
OLD_PWD = None
27+
28+
29+
class SeqraAnalyzerResultTestCase(unittest.TestCase):
30+
""" Test the output of the SeqraAnalyzerResult. """
31+
32+
def setup_class(self):
33+
""" Initialize test files. """
34+
35+
global TEST_WORKSPACE
36+
TEST_WORKSPACE = env.get_workspace('seqra_parser')
37+
os.environ['TEST_WORKSPACE'] = TEST_WORKSPACE
38+
self.test_workspace = os.environ['TEST_WORKSPACE']
39+
self.cc_result_dir = self.test_workspace
40+
41+
self.analyzer_result = analyzer_result.AnalyzerResult()
42+
43+
self.test_files = os.path.join(os.path.dirname(__file__),
44+
'seqra_output_test_files')
45+
global OLD_PWD
46+
OLD_PWD = os.getcwd()
47+
os.chdir(os.path.join(os.path.dirname(__file__),
48+
'seqra_output_test_files'))
49+
50+
def teardown_class(self):
51+
"""Clean up after the test."""
52+
53+
global OLD_PWD
54+
os.chdir(OLD_PWD)
55+
56+
global TEST_WORKSPACE
57+
58+
print("Removing: " + TEST_WORKSPACE)
59+
shutil.rmtree(TEST_WORKSPACE, ignore_errors=True)
60+
61+
def test_no_plist_file(self):
62+
""" Test transforming single plist file. """
63+
analyzer_output_file = os.path.join(self.test_files, 'files',
64+
'UserProfileController.java')
65+
66+
ret = self.analyzer_result.transform(
67+
[analyzer_output_file], self.cc_result_dir, plist.EXTENSION,
68+
file_name="{source_file}_{analyzer}")
69+
self.assertFalse(ret)
70+
71+
def test_no_plist_dir(self):
72+
""" Test transforming single plist file. """
73+
analyzer_output_file = os.path.join(self.test_files, 'non_existing')
74+
75+
ret = self.analyzer_result.transform(
76+
[analyzer_output_file], self.cc_result_dir, plist.EXTENSION,
77+
file_name="{source_file}_{analyzer}")
78+
self.assertFalse(ret)
79+
80+
def test_seqra_transform_single_file(self):
81+
""" Test transforming single plist file. """
82+
analyzer_output_file = os.path.join(
83+
self.test_files, 'UserProfileController.java.sarif')
84+
self.analyzer_result.transform(
85+
[analyzer_output_file], self.cc_result_dir, plist.EXTENSION,
86+
file_name="{source_file}_{analyzer}")
87+
88+
plist_file = os.path.join(self.cc_result_dir,
89+
'UserProfileController.java_seqra.plist')
90+
with open(plist_file, mode='rb') as pfile:
91+
res = plistlib.load(pfile)
92+
93+
# Use relative path for this test.
94+
res['files'][0] = 'files/UserProfileController.java'
95+
96+
self.assertTrue(res['metadata']['generated_by']['version'])
97+
res['metadata']['generated_by']['version'] = "x.y.z"
98+
print(
99+
res["diagnostics"][0]["issue_hash_content_of_line_in_context"])
100+
101+
plist_file = os.path.join(self.test_files,
102+
'UserProfileController.expected.plist')
103+
with open(plist_file, mode='rb') as pfile:
104+
exp = plistlib.load(pfile)
105+
106+
self.assertEqual(res, exp)

0 commit comments

Comments
 (0)