Skip to content

Commit 9103b80

Browse files
jan-cernyrhmdnd
authored andcommitted
Introduce a script to compare ComplianceAsCode versions
This commit introduces a new script compare_versions.py which can show differences between two ComplianceAsCode versions. Lists added or removed rules, profiles, changes in profile composition and changes in remediations and platforms. For comparison, you can use git tags or ComplianceAsCode JSON manifest files directly. This new feature leverages the ComplianceAsCode JSON manifests, which have been introduced by ComplianceAsCode/content#10761
1 parent 06ed6dd commit 9103b80

File tree

2 files changed

+204
-0
lines changed

2 files changed

+204
-0
lines changed

docs/manual/developer/05_tools_and_utilities.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -707,3 +707,24 @@ $ python utils/generate_profile.py -i benchmark.xlsx generate --product-type ocp
707707
708708
The `PLACEHOLDER` values must be filled in later, ideally when the rules are
709709
provided for each control.
710+
711+
712+
### Compare ComplianceAsCode versions - `utils/compare_versions.py`
713+
714+
Show differences between two ComplianceAsCode versions.
715+
Lists added or removed rules, profiles, changes in profile composition and changes in remediations and platforms.
716+
For comparison, you can use git tags or ComplianceAsCode JSON manifest files directly.
717+
718+
To compare 2 ComplianceAsCode JSON manifests, provide the manifest files.
719+
720+
```
721+
python3 utils/compare_versions.py compare_manifests ~/manifests/old.json ~/manifests/new.json
722+
```
723+
724+
To compare 2 upstream versions, you need to specify the version git tags and a product ID.
725+
726+
```
727+
$ python3 utils/compare_versions.py compare_tags v0.1.67 v0.1.68 rhel9
728+
```
729+
730+
It will internally clone the upstream project, checkout these tags, generate ComplianceAsCode JSON manifests, compare them and print the output.

utils/compare_versions.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/python3
2+
3+
import argparse
4+
import json
5+
import os
6+
import subprocess
7+
import tempfile
8+
9+
10+
def parse_args() -> argparse.Namespace:
11+
parser = argparse.ArgumentParser(description="""
12+
Show differences between two ComplianceAsCode versions.
13+
Lists added or removed rules, profiles, changes in profile composition
14+
and changes in remediations and platforms.
15+
For comparison, you can use git tags or ComplianceAsCode JSON manifest
16+
files directly.
17+
""")
18+
subparsers = parser.add_subparsers()
19+
parser_compare_manifests = subparsers.add_parser("compare_manifests")
20+
parser_compare_manifests.set_defaults(func=compare_manifests)
21+
parser_compare_manifests.add_argument(
22+
"manifest1", help="Path to a ComplianceAsCode JSON manifest file")
23+
parser_compare_manifests.add_argument(
24+
"manifest2", help="Path to a ComplianceAsCode JSON manifest file")
25+
parser_compare_tags = subparsers.add_parser("compare_tags")
26+
parser_compare_tags.set_defaults(func=compare_tags)
27+
parser_compare_tags.add_argument("tag1", help="git tag, eg. v0.1.67")
28+
parser_compare_tags.add_argument("tag2", help="git tag, eg. v0.1.68")
29+
parser_compare_tags.add_argument(
30+
"product", help="product ID, eg. 'rhel9'")
31+
return parser.parse_args()
32+
33+
34+
def load_manifest(file_path: str) -> dict:
35+
with open(file_path) as f:
36+
manifest = json.load(f)
37+
return manifest
38+
39+
40+
def print_set(elements: set) -> None:
41+
for element in sorted(list(elements)):
42+
print(" - " + element)
43+
44+
45+
def compare_sets(set1: set, set2: set, title: str, name: str) -> None:
46+
added = set2 - set1
47+
removed = set1 - set2
48+
if not added and not removed:
49+
return
50+
print(title)
51+
if added:
52+
print(f"The following {name} were added:")
53+
print_set(added)
54+
if removed:
55+
print(f"The following {name} were removed:")
56+
print_set(removed)
57+
print()
58+
59+
60+
class ManifestComparator():
61+
def __init__(self, manifest1_path: str, manifest2_path: str) -> None:
62+
self.manifest1 = load_manifest(manifest1_path)
63+
self.manifest2 = load_manifest(manifest2_path)
64+
pass
65+
66+
def compare_products(self) -> None:
67+
product_name1 = self.manifest1["product_name"]
68+
product_name2 = self.manifest2["product_name"]
69+
if product_name1 != product_name2:
70+
print("Product names differ")
71+
72+
def compare_rules(self) -> None:
73+
rules1 = set(self.manifest1["rules"].keys())
74+
rules2 = set(self.manifest2["rules"].keys())
75+
compare_sets(rules1, rules2, "Rules in benchmark:", "rules")
76+
77+
def _get_reports(self, rules: list) -> list:
78+
rule_reports = []
79+
for rule_id in rules:
80+
rule1 = self.manifest1["rules"][rule_id]
81+
rule2 = self.manifest2["rules"][rule_id]
82+
content1 = set(rule1["content"])
83+
platforms1 = set(rule1["platform_names"])
84+
content2 = set(rule2["content"])
85+
platforms2 = set(rule2["platform_names"])
86+
content_added = content2 - content1
87+
content_removed = content1 - content2
88+
platforms_added = platforms2 - platforms1
89+
platforms_removed = platforms1 - platforms2
90+
if (content_added or content_removed or platforms_added
91+
or platforms_removed):
92+
msgs = []
93+
if content_added:
94+
msgs.append("adds " + ", ".join(content_added))
95+
if content_removed:
96+
msgs.append("removes " + ", ".join(content_removed))
97+
if platforms_added:
98+
msgs.append("adds platform " + ", ".join(platforms_added))
99+
if platforms_removed:
100+
msgs.append(
101+
"removes platform " + ", ".join(platforms_removed))
102+
rule_report = "Rule " + rule_id + " " + ", ".join(msgs) + "."
103+
rule_reports.append(rule_report)
104+
return rule_reports
105+
106+
def compare_rule_details(self) -> None:
107+
rules1 = set(self.manifest1["rules"].keys())
108+
rules2 = set(self.manifest2["rules"].keys())
109+
rules_intersection = sorted(rules1 & rules2)
110+
rule_reports = self._get_reports(rules_intersection)
111+
if len(rule_reports) > 0:
112+
print("Differences in rules:")
113+
for report in rule_reports:
114+
print(" - " + report)
115+
print()
116+
117+
def compare_profiles(self) -> None:
118+
profiles1 = set(self.manifest1["profiles"].keys())
119+
profiles2 = set(self.manifest2["profiles"].keys())
120+
compare_sets(
121+
profiles1, profiles2, "Profiles in benchmark:", "profiles")
122+
profiles_intersection = sorted(profiles1 & profiles2)
123+
for profile_id in profiles_intersection:
124+
rules1 = set(self.manifest1["profiles"][profile_id]["rules"])
125+
rules2 = set(self.manifest2["profiles"][profile_id]["rules"])
126+
compare_sets(
127+
rules1, rules2, f"Profile {profile_id} differs:", "rules")
128+
129+
def compare(self) -> None:
130+
self.compare_products()
131+
self.compare_rules()
132+
self.compare_rule_details()
133+
self.compare_profiles()
134+
135+
136+
def compare_manifests(args: argparse.Namespace) -> None:
137+
comparator = ManifestComparator(args.manifest1, args.manifest2)
138+
comparator.compare()
139+
140+
141+
def clone_git(tag: str, target_directory_path: str) -> None:
142+
cac_uri = "https://github.com/ComplianceAsCode/content.git"
143+
cmd = ["git", "clone", cac_uri, "-b", tag, target_directory_path]
144+
subprocess.run(cmd, check=True, capture_output=True)
145+
146+
147+
def build_product(cac_root: str, product: str) -> None:
148+
cmd = ["./build_product", product]
149+
subprocess.run(cmd, cwd=cac_root, check=True, capture_output=True)
150+
151+
152+
def generate_manifest(build_root: str, manifest_file_path: str) -> None:
153+
cmd = [
154+
"python3", "build-scripts/generate_manifest.py",
155+
"--build-root", build_root, "--output", manifest_file_path]
156+
subprocess.run(cmd, check=True, capture_output=True)
157+
158+
159+
def prepare(tmpdirname: str, tag: str, product: str) -> str:
160+
git_dir_path = os.path.join(tmpdirname, tag)
161+
clone_git(tag, git_dir_path)
162+
build_product(git_dir_path, product)
163+
build_dir_path = os.path.join(git_dir_path, "build", product)
164+
manifest_file_path = os.path.join(tmpdirname, tag + ".manifest.json")
165+
generate_manifest(build_dir_path, manifest_file_path)
166+
return manifest_file_path
167+
168+
169+
def compare_tags(args: argparse.Namespace) -> None:
170+
with tempfile.TemporaryDirectory() as tmpdirname:
171+
manifest1 = prepare(tmpdirname, args.tag1, args.product)
172+
manifest2 = prepare(tmpdirname, args.tag2, args.product)
173+
comparator = ManifestComparator(manifest1, manifest2)
174+
comparator.compare()
175+
176+
177+
def main() -> None:
178+
args = parse_args()
179+
args.func(args)
180+
181+
182+
if __name__ == "__main__":
183+
main()

0 commit comments

Comments
 (0)