-
Notifications
You must be signed in to change notification settings - Fork 75
Expand file tree
/
Copy pathutils.py
More file actions
216 lines (195 loc) · 11.2 KB
/
Copy pathutils.py
File metadata and controls
216 lines (195 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
from collections import defaultdict, namedtuple
from contextlib import redirect_stdout
import json
from pathlib import Path
import re
import sys
REPO_ROOT = Path(__file__).parent.parent.parent
SUPPORTED_CODEQL_CONFIG_FILE = REPO_ROOT.joinpath(
'supported_codeql_configs.json')
def split_rule_id(rule_id):
"""Splits the rule_id into the rule_type and rule_number"""
return tuple(int(x) if x.isdigit() else x for x in filter(len, filter(None, re.split("(\d+)|-", rule_id))))
def load_supported_environments():
"""Return a list of supported environments for the current Coding Standards release."""
try:
supported_codeql_configs_file = SUPPORTED_CODEQL_CONFIG_FILE.open('r')
except PermissionError:
print("Error: No permission to read the supported_codeql_configs.json file located at 'supported_codeql_configs.json'.", file=sys.stderr)
sys.exit(1)
except FileNotFoundError:
print(
f"Error: Could not find the file {str(SUPPORTED_CODEQL_CONFIG_FILE)} expected to contain the supported CodeQL configurations.", file=sys.stderr)
sys.exit(1)
else:
with supported_codeql_configs_file:
supported_codeql_configs = json.load(supported_codeql_configs_file)
return supported_codeql_configs['supported_environment']
def load_sarif(sarif_results_file_path):
"""Read the SARIF file at sarif_results_file_path and return a dict containing the loaded file."""
# Read SARIF, process results, produce markdown report
print(f"Loading SARIF file...", file=sys.stderr)
try:
sarif_results_file = open(sarif_results_file_path, "r")
except PermissionError:
print("Error: No permission to read the SARIF results file located at '" +
str(sarif_results_file_path) + "'", file=sys.stderr)
sys.exit(1)
else:
with sarif_results_file:
sarif_results_json = json.load(sarif_results_file)
print(f"SARIF file loaded", file=sys.stderr)
return sarif_results_json
# TODO Warn if using a results file from a different version of the Coding Standards pack
class CodingStandardsResultSummary:
def __init__(self, sarif_results_file_path):
"""Create a results summary from the given SARIF path"""
sarif_results_json = load_sarif(sarif_results_file_path)
number_of_runs = len(sarif_results_json["runs"])
if not number_of_runs == 1:
print(
f"Error: Expected a single SARIF run, but found { number_of_runs } runs.", file=sys.stderr)
sys.exit(1)
run = sarif_results_json["runs"][0]
# Identify the Coding Standard version numbers used
tool = run["tool"]
driver = tool["driver"]
self.codeql_cli_version = driver["semanticVersion"]
# Validate that this is, indeed, a CodeQL file
if not driver["name"] == "CodeQL":
print(
f"Error: SARIF file is not produced by CodeQL (toolName { driver['name'] }", file=sys.stderr)
sys.exit(1)
extensions = tool["extensions"]
self.coding_standard_relevant_packs = []
coding_standard_name_endings = ["cpp-coding-standards", "c-coding-standards", "codeql/cpp-all"]
for extension in extensions:
for ending in coding_standard_name_endings:
if extension["name"].endswith(ending):
self.coding_standard_relevant_packs.append(extension)
# Count the results per SARIF rule ID
sarif_rule_result_count = defaultdict(int)
# Count the deviations per SARIF rule ID
sarif_rule_deviation_count = defaultdict(int)
results = run["results"]
for result in results:
sarif_rule_id = result["ruleId"]
if "suppressions" in result and len(result['suppressions']) != 0:
sarif_rule_deviation_count[sarif_rule_id] += 1
else:
sarif_rule_result_count[sarif_rule_id] += 1
# The number of guidelines violated for each obligation level
self.guidelines_violated_by_obligation = defaultdict(int)
self.guidelines_compliant_by_obligation = defaultdict(int)
# The number of violation and deviations results per guideline
self.guideline_violation_count = {}
self.guideline_obligation_level = {}
self.guideline_deviation_count = {}
obligation_level_re = re.compile(
"^external/([^/]+)/obligation/([^/]+)$")
id_re = re.compile("^external/([^/]+)/id/([^/]+)$")
rules = driver["rules"]
for rule in rules:
sarif_rule_id = rule["id"]
# Process the tags to determine rule id, standard name and obligation level
obligation_level = None
standard_short_name = None
standard_rule_id = None
for tag in rule["properties"]["tags"]:
obligation_level_result = obligation_level_re.search(tag)
if obligation_level_result:
obligation_level = obligation_level_result.group(2)
# Remap CERT "rule" obligations to "required", by default.
# CERT doesn't provide obligation levels, so we have to make a choice about how to handle this
# case. We choose to default to the MISRA Compliance category of 'required', but, unlike MISRA
# or AUTOSAR 'required' rules, we should permit re-categorization to 'advisory' or even
# 'disapplied'.
if obligation_level == "rule":
obligation_level = "required"
id_result = id_re.search(tag)
if id_result:
standard_short_name = id_result.group(1)
standard_rule_id = id_result.group(2)
if standard_rule_id:
if not obligation_level:
print(
f"WARNING: Rule { rule['id'] } does not have an obligation level.", file=sys.stderr)
elif sarif_rule_result_count[sarif_rule_id] > 0:
# Add to the violated obligation count
self.guidelines_violated_by_obligation[obligation_level] += 1
else:
# Otherwise add to the compliant obligation count
self.guidelines_compliant_by_obligation[obligation_level] += 1
# Add to counts for the rule
self.guideline_violation_count.setdefault(
standard_short_name, defaultdict(int))
self.guideline_violation_count[standard_short_name][
standard_rule_id] += sarif_rule_result_count[sarif_rule_id]
# Store the obligation level for each guideline
self.guideline_obligation_level.setdefault(
standard_short_name, {})
if standard_rule_id in self.guideline_obligation_level[standard_short_name]:
if not self.guideline_obligation_level[standard_short_name][standard_rule_id] == obligation_level:
print(
f"WARNING: Rule { rule['id'] } specifies a conflicting obligation level of { obligation_level }, was previously specified as { self.guideline_obligation_level[standard_short_name][standard_rule_id] }.")
else:
self.guideline_obligation_level[standard_short_name][standard_rule_id] = obligation_level
# Add deviation counts for the rule
self.guideline_deviation_count.setdefault(
standard_short_name, defaultdict(int))
self.guideline_deviation_count[standard_short_name][
standard_rule_id] += sarif_rule_deviation_count[sarif_rule_id]
def generate_guideline_compliance_summary(output_directory, results_summary):
"""Print "guideline compliance summary", as described by the MISRA Compliance 2020 document."""
guideline_compliance_summary_path = output_directory.joinpath(
"guideline_compliance_summary.md")
try:
guideline_compliance_summary_file = open(
guideline_compliance_summary_path, "w")
except PermissionError:
print("Error: No permission to write to the output file located at '" +
str(guideline_compliance_summary_path) + "'", file=sys.stderr)
sys.exit(1)
else:
with guideline_compliance_summary_file:
# Print to gcs file file, rather than stdout
with redirect_stdout(guideline_compliance_summary_file):
print("# Guideline Compliance Summary")
print()
print("## Overview")
print()
total_guidelines_violated = sum(
results_summary.guidelines_violated_by_obligation.values())
print(
"**Result**: " + ("Not compliant" if total_guidelines_violated > 0 else "Compliant"))
standard_pretty_name = {
"cert": "CERT C++ 2016", "autosar": "AUTOSAR C++ R22-11, R21-11, R20-11, R19-11 and R19-03"}
print("**Coding Standards applied**: " + ", ".join([standard_pretty_name[standard_short_name]
for standard_short_name in results_summary.guideline_violation_count.keys()]))
packs = ", ".join(
[f"{ pack['name'] }: { pack['semanticVersion'] }" for pack in results_summary.coding_standard_relevant_packs])
print(
f"**Tool version:** CodeQL CLI { results_summary.codeql_cli_version }")
print(f"**CodeQL packs used:** { packs }")
if total_guidelines_violated > 0:
print("**Violations by obligation level**:")
for obligation_level, count in results_summary.guidelines_violated_by_obligation.items():
print(
f" - { count } { obligation_level} guidelines violated")
print()
print("## Guidelines")
print()
print("| Standard | Guideline | MISRA Category | Compliance |")
print("| -------- | --------- | -------------- | ---------- |")
for standard_short_name in results_summary.guideline_violation_count.keys():
for guideline in sorted(results_summary.guideline_violation_count[standard_short_name].keys(), key=split_rule_id):
violation_count = results_summary.guideline_violation_count[
standard_short_name][guideline]
deviation_count = results_summary.guideline_deviation_count[
standard_short_name][guideline]
if deviation_count > 0:
compliance = f"{violation_count} violation(s) and {deviation_count} deviation(s)" if violation_count > 0 else f"Compliant with {deviation_count} deviation(s)"
else:
compliance = f"{violation_count} violation(s)" if violation_count > 0 else "Compliant"
print(
f"| { standard_pretty_name[standard_short_name] } | { guideline.upper() } | { results_summary.guideline_obligation_level[standard_short_name][guideline].capitalize() } | { compliance } |")