github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/test/compare/compare.py (about) 1 #!/usr/bin/env python3 2 import sys 3 import json 4 import collections 5 6 INDENT = " " 7 8 9 Metadata = collections.namedtuple("Metadata", "metadata sources") 10 Package = collections.namedtuple("Package", "name type version") 11 12 13 class Syft: 14 def __init__(self, report_path): 15 self.report_path = report_path 16 17 def _enumerate_section(self, section): 18 with open(self.report_path) as json_file: 19 data = json.load(json_file) 20 for entry in data[section]: 21 yield entry 22 23 def packages(self): 24 packages = set() 25 metadata = collections.defaultdict(dict) 26 for entry in self._enumerate_section(section="artifacts"): 27 package = Package( 28 name=entry["name"], type=entry["type"], version=entry["version"] 29 ) 30 31 packages.add(package) 32 metadata[package.type][package] = Metadata( 33 # note: the metadata entry is optional 34 metadata=repr(entry.get("metadata", "")), sources=repr(entry["locations"]) 35 ) 36 return packages, metadata 37 38 39 def print_rows(rows): 40 if not rows: 41 return 42 widths = [] 43 for col, _ in enumerate(rows[0]): 44 width = max(len(row[col]) for row in rows) + 2 # padding 45 widths.append(width) 46 for row in rows: 47 print("".join(word.ljust(widths[col_idx]) for col_idx, word in enumerate(row))) 48 49 50 def main(baseline_report, new_report): 51 report1_obj = Syft(report_path=baseline_report) 52 report1_packages, report1_metadata = report1_obj.packages() 53 54 report2_obj = Syft(report_path=new_report) 55 report2_packages, report2_metadata = report2_obj.packages() 56 57 if len(report2_packages) == 0 or len(report1_packages) == 0: 58 # we are purposefully selecting test images that are guaranteed to have packages, so this should never happen 59 print(colors.bold + colors.fg.red + "no packages found!", colors.reset) 60 return 1 61 62 same_packages = report2_packages & report1_packages 63 percent_overlap_packages = ( 64 float(len(same_packages)) / float(len(report1_packages)) 65 ) * 100.0 66 67 extra_packages = report2_packages - report1_packages 68 missing_packages = report1_packages - report2_packages 69 70 report1_metadata_set = set() 71 for package in report1_packages: 72 metadata = report1_metadata[package.type][package] 73 report1_metadata_set.add((package, metadata)) 74 75 report2_metadata_set = set() 76 for package in report2_packages: 77 metadata = report2_metadata[package.type][package] 78 report2_metadata_set.add((package, metadata)) 79 80 same_metadata = report2_metadata_set & report1_metadata_set 81 percent_overlap_metadata = 0 82 if len(report1_metadata_set) > 0: 83 percent_overlap_metadata = ( 84 float(len(same_metadata)) / float(len(report1_metadata_set)) 85 ) * 100.0 86 87 if extra_packages: 88 rows = [] 89 print(colors.bold + "Extra packages:", colors.reset) 90 for package in sorted(list(extra_packages)): 91 rows.append([INDENT, repr(package)]) 92 print_rows(rows) 93 print() 94 95 if missing_packages: 96 rows = [] 97 print(colors.bold + "Missing packages:", colors.reset) 98 for package in sorted(list(missing_packages)): 99 rows.append([INDENT, repr(package)]) 100 print_rows(rows) 101 print() 102 103 print(colors.bold+"Summary:", colors.reset) 104 print(" Baseline Packages: %d" % len(report1_packages)) 105 print(" New Packages: %d" % len(report2_packages)) 106 print( 107 " Baseline Packages Matched: %.2f %% (%d/%d packages)" 108 % (percent_overlap_packages, len(same_packages), len(report1_packages)) 109 ) 110 print( 111 " Baseline Metadata Matched: %.2f %% (%d/%d metadata)" 112 % (percent_overlap_metadata, len(same_metadata), len(report1_metadata_set)) 113 ) 114 115 if len(report1_packages) != len(report2_packages): 116 print(colors.bold + " Quality Gate: " + colors.fg.red + "FAILED (requires exact name & version match)\n", colors.reset) 117 return 1 118 else: 119 print(colors.bold + " Quality Gate: " + colors.fg.green + "pass\n", colors.reset) 120 return 0 121 122 123 class colors: 124 reset='\033[0m' 125 bold='\033[01m' 126 disable='\033[02m' 127 underline='\033[04m' 128 reverse='\033[07m' 129 strikethrough='\033[09m' 130 invisible='\033[08m' 131 class fg: 132 black='\033[30m' 133 red='\033[31m' 134 green='\033[32m' 135 orange='\033[33m' 136 blue='\033[34m' 137 purple='\033[35m' 138 cyan='\033[36m' 139 lightgrey='\033[37m' 140 darkgrey='\033[90m' 141 lightred='\033[91m' 142 lightgreen='\033[92m' 143 yellow='\033[93m' 144 lightblue='\033[94m' 145 pink='\033[95m' 146 lightcyan='\033[96m' 147 class bg: 148 black='\033[40m' 149 red='\033[41m' 150 green='\033[42m' 151 orange='\033[43m' 152 blue='\033[44m' 153 purple='\033[45m' 154 cyan='\033[46m' 155 lightgrey='\033[47m' 156 157 158 if __name__ == "__main__": 159 print("\nComparing two Syft reports...\n") 160 if len(sys.argv) != 3: 161 sys.exit("please provide two Syft json files") 162 163 rc = main(sys.argv[1], sys.argv[2]) 164 sys.exit(rc)