Skip to content

Commit 6732313

Browse files
authored
Improve logic for calculating result level (#69)
1 parent d41747d commit 6732313

File tree

3 files changed

+255
-7
lines changed

3 files changed

+255
-7
lines changed

sarif/sarif_file.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from sarif.sarif_file_utils import (
1414
SARIF_SEVERITIES_WITHOUT_NONE,
1515
SARIF_SEVERITIES_WITH_NONE,
16+
read_result_severity,
1617
)
1718
from sarif.filter.general_filter import GeneralFilter
1819
from sarif.filter.filter_stats import FilterStats
@@ -250,12 +251,7 @@ def result_to_record(self, result, include_blame_info=False):
250251
file_path = file_path[prefixlen:]
251252
break
252253

253-
# Get the error severity, if included, and code
254-
severity = result.get(
255-
"level", "warning"
256-
) # If an error has no specified level then by default it is a warning
257-
# TODO: improve this logic to match the rules in
258-
# https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html#_Toc141790898
254+
severity = read_result_severity(result, self.run_data)
259255

260256
if "message" in result:
261257
# per RFC3629 At least one of the text (§3.11.8) or id (§3.11.10) properties SHALL be present https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/sarif-v2.1.0-os.html#RFC3629

sarif/sarif_file_utils.py

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"""
77

88
import textwrap
9-
from typing import Tuple
9+
from typing import Literal, Tuple, Union
1010

1111
# SARIF severity levels as per
1212
# https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html#_Toc141790898
@@ -100,6 +100,101 @@ def read_result_location(result) -> Tuple[str, str]:
100100
return (file_path, line_number)
101101

102102

103+
def read_result_rule(result, run) -> Tuple[Union[dict, None], int]:
104+
"""
105+
Returns the corresponding rule object for the specified result, plus its index
106+
in the rules array. Follows the rules at
107+
https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html#_Toc141790895
108+
"""
109+
ruleIndex = result.get("ruleIndex")
110+
ruleId = result.get("ruleId")
111+
rule = result.get("rule")
112+
113+
if rule:
114+
if ruleIndex is None:
115+
ruleIndex = rule.get("index")
116+
117+
if ruleId is None:
118+
ruleId = rule.get("id")
119+
120+
rules = run.get("tool", {}).get("driver", {}).get("rules", [])
121+
122+
if ruleIndex is not None and ruleIndex >= 0 and ruleIndex < len(rules):
123+
return (rules[ruleIndex], ruleIndex)
124+
125+
if ruleId:
126+
for i, rule in enumerate(rules):
127+
if rule.get("id") == ruleId:
128+
return (rule, i)
129+
130+
return (None, -1)
131+
132+
133+
def read_result_invocation(result, run):
134+
"""
135+
Extract the invocation metadata for the result, following the rules at
136+
https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html#_Toc141790917
137+
"""
138+
invocationIndex = result.get("provenance", {}).get("invocationIndex")
139+
if invocationIndex is None:
140+
return None
141+
142+
invocations = run.get("invocations")
143+
144+
if invocations and invocationIndex >= 0 and invocationIndex < len(invocations):
145+
return invocations[invocationIndex]
146+
147+
return None
148+
149+
150+
def read_result_severity(result, run) -> Literal["none", "note", "warning", "error"]:
151+
"""
152+
Extract the severity level from the result following the rules at
153+
https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html#_Toc141790898
154+
"""
155+
severity = result.get("level")
156+
if severity:
157+
return severity
158+
159+
# If kind has any value other than "fail", then if level is absent,
160+
# it SHALL default to "none"
161+
kind = result.get("kind", "fail")
162+
if kind and kind != "fail":
163+
return "none"
164+
165+
# If kind has the value "fail" and level is absent, then...
166+
rule, ruleIndex = read_result_rule(result, run)
167+
if rule:
168+
# Honor the invocation's configuration override if present...
169+
invocation = read_result_invocation(result, run)
170+
if invocation:
171+
ruleConfigurationOverrides = invocation.get("ruleConfigurationOverrides", [])
172+
override = next(
173+
(
174+
override
175+
for override in ruleConfigurationOverrides
176+
if override.get("descriptor", {}).get("id") == rule.get("id") or
177+
override.get("descriptor", {}).get("index") == ruleIndex
178+
),
179+
None,
180+
)
181+
182+
if override:
183+
overrideLevel = override.get("configuration", {}).get("level")
184+
if overrideLevel:
185+
return overrideLevel
186+
187+
# Otherwise, use the rule's default configuraiton if present...
188+
defaultConfiguration = rule.get("defaultConfiguration")
189+
if defaultConfiguration:
190+
severity = defaultConfiguration.get("level")
191+
if severity:
192+
return severity
193+
194+
# Otherwise, fall back to warning
195+
return "warning"
196+
197+
103198
def record_sort_key(record: dict) -> str:
104199
"""Get a sort key for the record."""
105200
return (

tests/test_sarif_file_utils.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,160 @@ def test_combine_code_and_description_long_code():
3535
long_code, "wow that's a long code"
3636
)
3737
assert cd == long_code
38+
39+
40+
def test_read_result_rule():
41+
run = {"tool":
42+
{"driver":
43+
{"rules": [
44+
{"id": "id0", "defaultConfiguration": {"level": "none"}},
45+
{"id": "id1", "defaultConfiguration": {"level": "error"}}
46+
]}}}
47+
rule_id0 = run["tool"]["driver"]["rules"][0]
48+
rule_id1 = run["tool"]["driver"]["rules"][1]
49+
50+
result = {}
51+
(rule, ruleIndex) = sarif_file_utils.read_result_rule(result, run)
52+
assert rule is None
53+
assert ruleIndex == -1
54+
55+
result = {"ruleIndex": 1}
56+
(rule, ruleIndex) = sarif_file_utils.read_result_rule(result, run)
57+
assert rule == rule_id1
58+
assert ruleIndex == 1
59+
60+
result = {"rule": { "index": 1}}
61+
(rule, ruleIndex) = sarif_file_utils.read_result_rule(result, run)
62+
assert rule == rule_id1
63+
assert ruleIndex == 1
64+
65+
result = {"ruleId": "id1"}
66+
(rule, ruleIndex) = sarif_file_utils.read_result_rule(result, run)
67+
assert rule == rule_id1
68+
assert ruleIndex == 1
69+
70+
result = {"rule": { "id": "id1"}}
71+
(rule, ruleIndex) = sarif_file_utils.read_result_rule(result, run)
72+
assert rule == rule_id1
73+
assert ruleIndex == 1
74+
75+
result = {"ruleIndex": 0}
76+
(rule, ruleIndex) = sarif_file_utils.read_result_rule(result, run)
77+
assert rule == rule_id0
78+
assert ruleIndex == 0
79+
80+
result = {"ruleIndex": 0}
81+
(rule, ruleIndex) = sarif_file_utils.read_result_rule(result, {})
82+
assert rule is None
83+
assert ruleIndex == -1
84+
85+
86+
def test_read_result_invocation():
87+
run = {"invocations": [
88+
{"foo": 1},
89+
{"bar": "baz"}
90+
]}
91+
92+
result = {}
93+
invocation = sarif_file_utils.read_result_invocation(result, run)
94+
assert invocation is None
95+
96+
result = {"provenance": {}}
97+
invocation = sarif_file_utils.read_result_invocation(result, run)
98+
assert invocation is None
99+
100+
result = {"provenance": {"invocationIndex": 0}}
101+
invocation = sarif_file_utils.read_result_invocation(result, {})
102+
assert invocation is None
103+
104+
result = {"provenance": {"invocationIndex": -1}}
105+
invocation = sarif_file_utils.read_result_invocation(result, run)
106+
assert invocation is None
107+
108+
result = {"provenance": {"invocationIndex": 2}}
109+
invocation = sarif_file_utils.read_result_invocation(result, run)
110+
assert invocation is None
111+
112+
result = {"provenance": {"invocationIndex": 1}}
113+
invocation = sarif_file_utils.read_result_invocation(result, run)
114+
assert invocation == run["invocations"][1]
115+
116+
117+
def test_read_result_severity():
118+
result = {"level": "error"}
119+
severity = sarif_file_utils.read_result_severity(result, {})
120+
assert severity == "error"
121+
122+
# If kind has any value other than "fail", then if level is absent, it SHALL default to "none"...
123+
result = {"kind": "other"}
124+
severity = sarif_file_utils.read_result_severity(result, {})
125+
assert severity == "none"
126+
127+
run = {"invocations": [
128+
{"ruleConfigurationOverrides": [ {"descriptor": {"id": "id1"}, "configuration": {"level": "note"}} ]},
129+
{"ruleConfigurationOverrides": [ {"descriptor": {"index": 1}, "configuration": {"level": "note"}} ]},
130+
{ }
131+
],
132+
"tool":
133+
{"driver":
134+
{"rules": [
135+
{"id": "id0", "defaultConfiguration": {"level": "none"}},
136+
{"id": "id1", "defaultConfiguration": {"level": "error"}}
137+
]}
138+
}
139+
}
140+
141+
# If kind has the value "fail" and level is absent, then level SHALL be determined by the following procedure:
142+
# IF rule is present THEN
143+
# LET theDescriptor be the reportingDescriptor object that it specifies.
144+
# # Is there a configuration override for the level property?
145+
# IF result.provenance.invocationIndex is >= 0 THEN
146+
# LET theInvocation be the invocation object that it specifies.
147+
# IF theInvocation.ruleConfigurationOverrides is present
148+
# AND it contains a configurationOverride object whose
149+
# descriptor property specifies theDescriptor THEN
150+
# LET theOverride be that configurationOverride object.
151+
# IF theOverride.configuration.level is present THEN
152+
# Set level to theConfiguration.level.
153+
result = {"ruleIndex": 1, "provenance": {"invocationIndex": 0}}
154+
severity = sarif_file_utils.read_result_severity(result, run)
155+
assert severity == "note"
156+
157+
result = {"ruleIndex": 1, "provenance": {"invocationIndex": 1}}
158+
severity = sarif_file_utils.read_result_severity(result, run)
159+
assert severity == "note"
160+
161+
# ELSE
162+
# # There is no configuration override for level. Is there a default configuration for it?
163+
# IF theDescriptor.defaultConfiguration.level is present THEN
164+
# SET level to theDescriptor.defaultConfiguration.level.
165+
166+
result = {"ruleIndex": 1}
167+
severity = sarif_file_utils.read_result_severity(result, run)
168+
assert severity == "error"
169+
170+
result = {"rule": { "index": 1}}
171+
severity = sarif_file_utils.read_result_severity(result, run)
172+
assert severity == "error"
173+
174+
result = {"ruleId": "id1"}
175+
severity = sarif_file_utils.read_result_severity(result, run)
176+
assert severity == "error"
177+
178+
result = {"rule": { "id": "id1"}}
179+
severity = sarif_file_utils.read_result_severity(result, run)
180+
assert severity == "error"
181+
182+
result = {"ruleIndex": 1, "provenance": {"invocationIndex": 2}}
183+
severity = sarif_file_utils.read_result_severity(result, run)
184+
assert severity == "error"
185+
186+
# IF level has not yet been set THEN
187+
# SET level to "warning".
188+
result = {}
189+
severity = sarif_file_utils.read_result_severity(result, {})
190+
assert severity == "warning"
191+
192+
result = {"ruleIndex": -1}
193+
severity = sarif_file_utils.read_result_severity(result, {})
194+
assert severity == "warning"

0 commit comments

Comments
 (0)