|
| 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