Skip to content

Commit f72d093

Browse files
authored
Fix --bom-profile bugs. (#44)
* Fix --bom-profile bugs. Signed-off-by: Caroline Russell <[email protected]> * Dedupe components. Signed-off-by: Caroline Russell <[email protected]> * Add minimal bom-diff template to MANIFEST.in. Signed-off-by: Caroline Russell <[email protected]> * Typing. Signed-off-by: Caroline Russell <[email protected]> * Bump version. Signed-off-by: Caroline Russell <[email protected]> --------- Signed-off-by: Caroline Russell <[email protected]>
1 parent d604e28 commit f72d093

File tree

8 files changed

+784
-38
lines changed

8 files changed

+784
-38
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
include custom_json_diff/lib/bom_diff_template.j2
2+
include custom_json_diff/lib/bom_diff_template_minimal.j2
23
include custom_json_diff/lib/csaf_diff_template.j2

custom_json_diff/cli.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,9 @@ def main():
129129
preset_type = args.preset_type.lower()
130130
if preset_type and preset_type not in ("bom", "csaf"):
131131
raise ValueError("Preconfigured type must be either bom or csaf.")
132-
if args.bom_profile and args.bom_profile not in ("gn", "gnv", "nv"):
133-
raise ValueError("BOM profile must be either gn, gnv, or nv.")
132+
if args.bom_profile:
133+
if args.bom_profile not in ("gn", "gnv", "nv"):
134+
raise ValueError("BOM profile must be either gn, gnv, or nv.")
134135
options = Options(
135136
allow_new_versions=args.allow_new_versions,
136137
allow_new_data=args.allow_new_data,
@@ -142,7 +143,8 @@ def main():
142143
file_2=args.input[1],
143144
output=args.output,
144145
report_template=args.report_template,
145-
include_empty=args.include_empty
146+
include_empty=args.include_empty,
147+
bom_profile=args.bom_profile
146148
)
147149
result, j1, j2 = compare_dicts(options)
148150
if preset_type == "bom":

custom_json_diff/lib/bom_diff_template.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -869,7 +869,7 @@
869869
{% if not misc_data_1 %}
870870
<td></td>
871871
{% endif %}
872-
{% if misc_data_1 %}
872+
{% if misc_data_2 %}
873873
<td>{{ misc_data_2 }}</td>
874874
{% endif %}
875875
{% if not misc_data_2 %}

custom_json_diff/lib/bom_diff_template_minimal.j2

Lines changed: 645 additions & 0 deletions
Large diffs are not rendered by default.

custom_json_diff/lib/custom_diff.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -108,26 +108,43 @@ def generate_counts(data: Dict) -> Dict:
108108

109109

110110
def generate_bom_diff(bom: BomDicts, commons: BomDicts, common_refs: Dict) -> Dict:
111-
diff_summary = {
112-
"components": {"applications": [], "frameworks": [], "libraries": [],
113-
"other_components": []},
111+
return {
112+
"components": get_unique_components(bom, common_refs),
114113
"dependencies": [i.to_dict() for i in bom.dependencies if i.ref not in common_refs["dependencies"]],
115114
"services": [i.to_dict() for i in bom.services if i.search_key not in common_refs["services"]],
116-
"vulnerabilities": [i.to_dict() for i in bom.vdrs if i.bom_ref not in common_refs["vdrs"]]
115+
"vulnerabilities": [i.to_dict() for i in bom.vdrs if i.bom_ref not in common_refs["vdrs"]],
116+
"misc_data": (bom.misc_data - commons.misc_data).to_dict()
117117
}
118-
for i in bom.components:
119-
if i.bom_ref not in common_refs["components"]:
120-
match i.component_type:
121-
case "application":
122-
diff_summary["components"]["applications"].append(i.to_dict()) #type: ignore
123-
case "framework":
124-
diff_summary["components"]["frameworks"].append(i.to_dict()) #type: ignore
125-
case "library":
126-
diff_summary["components"]["libraries"].append(i.to_dict()) #type: ignore
118+
119+
120+
def get_unique_components(bom: BomDicts, common_refs: Dict):
121+
components: Dict[str, List] = {"applications": [], "frameworks": [], "libraries": [], "other_components": []}
122+
if bom.options.bom_profile:
123+
for i in bom.components:
124+
match bom.options.bom_profile:
125+
case "nv":
126+
key = f"{i.name}@{i.version}"
127+
case "gn":
128+
key = f"{i.group}/{i.name}"
127129
case _:
128-
diff_summary["components"]["other_components"].append(i.to_dict()) #type: ignore
129-
diff_summary["misc_data"] = (bom.misc_data - commons.misc_data).to_dict()
130-
return diff_summary
130+
key = f"{i.group}/{i.name}@{i.version}"
131+
if key not in common_refs["components"]:
132+
components["other_components"].append(i.to_dict())
133+
return components
134+
for i in bom.components:
135+
key = i.bom_ref if "components.[].bom_ref" not in bom.options.exclude else f"{i.group}/{i.name}@{i.version}"
136+
if key in common_refs["components"]:
137+
continue
138+
match i.component_type:
139+
case "application":
140+
components["applications"].append(i.to_dict()) # type: ignore
141+
case "framework":
142+
components["frameworks"].append(i.to_dict()) # type: ignore
143+
case "library":
144+
components["libraries"].append(i.to_dict()) # type: ignore
145+
case _:
146+
components["other_components"].append(i.to_dict()) # type: ignore
147+
return components
131148

132149

133150
def generate_csaf_diff(csaf: CsafDicts, commons: CsafDicts, common_refs: Dict[str, Set]) -> Dict:

custom_json_diff/lib/custom_diff_classes.py

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -391,18 +391,18 @@ def vdrs(self, value):
391391
_, _, _, _, self._vdrs = import_bom_dict(self.options, {}, vulnerabilities=value)
392392

393393
def intersection(self, other, title: str = "") -> "BomDicts":
394-
components = []
395-
dependencies = []
396-
services = []
397-
vulnerabilities = []
394+
components = Array([])
395+
dependencies = Array([])
396+
services = Array([])
397+
vulnerabilities = Array([])
398398
if self.components and other.components:
399-
components = [i for i in self.components if i in other.components]
399+
components = Array([i for i in self.components if i in other.components])
400400
if self.services and other.services:
401-
services = [i for i in self.services if i in other.services]
401+
services = Array([i for i in self.services if i in other.services])
402402
if self.dependencies and other.dependencies:
403-
dependencies = [i for i in self.dependencies if i in other.dependencies]
403+
dependencies = Array([i for i in self.dependencies if i in other.dependencies])
404404
if self.vdrs and other.vdrs:
405-
vulnerabilities = [i for i in self.vdrs if i in other.vdrs]
405+
vulnerabilities = Array([i for i in self.vdrs if i in other.vdrs])
406406
other_data = self.misc_data.intersection(other.misc_data)
407407
options = deepcopy(self.options)
408408
return BomDicts(
@@ -436,12 +436,21 @@ def generate_comp_counts(self) -> Dict:
436436
"vulnerabilities": len(self.vdrs)}
437437

438438
def get_refs(self) -> Dict:
439-
return {
440-
"components": {i.bom_ref for i in self.components},
441-
"dependencies": {i.ref for i in self.dependencies},
442-
"services": {i.search_key for i in self.services},
443-
"vdrs": {i.bom_ref for i in self.vdrs}
444-
}
439+
refs = {
440+
"dependencies": {i.ref for i in self.dependencies},
441+
"services": {i.search_key for i in self.services},
442+
"vdrs": {i.bom_ref for i in self.vdrs}
443+
}
444+
match self.options.bom_profile:
445+
case "gnv":
446+
refs |= {"components": {f"{i.group}/{i.name}@{i.version}" for i in self.components}}
447+
case "gn":
448+
refs |= {"components": {f"{i.group}/{i.name}" for i in self.components}}
449+
case "nv":
450+
refs |= {"components": {f"{i.name}@{i.version}" for i in self.components}}
451+
case _:
452+
refs |= {"components": {i.bom_ref for i in self.components}}
453+
return refs
445454

446455
def to_dict(self) -> Dict:
447456
return {
@@ -1031,7 +1040,7 @@ def import_bom_dict(
10311040
if not value:
10321041
elements[i] = []
10331042
components, services, dependencies, vulnerabilities = elements
1034-
return other_data, Array(components), Array(services), Array(dependencies), Array(vulnerabilities) # type: ignore
1043+
return other_data, Array(dedupe_components(components)), Array(services), Array(dependencies), Array(vulnerabilities) # type: ignore
10351044

10361045

10371046
def import_csaf(options: "Options", original_data: Dict | None = None, document: FlatDicts | None = None,
@@ -1092,3 +1101,11 @@ def parse_bom_dict(original_data: Dict, options: Options) -> Tuple[FlatDicts, Li
10921101
if key not in {"components", "dependencies", "services", "vulnerabilities"}:
10931102
other_data |= {key: value}
10941103
return FlatDicts(other_data), components, services, dependencies, vulnerabilities
1104+
1105+
1106+
def dedupe_components(components: List) -> List:
1107+
deduped = []
1108+
for component in components:
1109+
if component not in deduped:
1110+
deduped.append(component)
1111+
return deduped

custom_json_diff/lib/utils.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import re
55
import sys
66
from datetime import date, datetime
7-
from typing import Any, Dict, List, TYPE_CHECKING
7+
from typing import Any, Dict, List, Tuple, TYPE_CHECKING
88

99
import packageurl
1010
import semver
@@ -264,6 +264,8 @@ def manual_version_compare_noeq(v1: List, v2: List, comparator: str) -> bool:
264264

265265

266266
def render_bom_template(diffs, jinja_tmpl, options, stats_summary, status):
267+
if options.bom_profile:
268+
return render_minimal_bom_template(diffs, jinja_tmpl, options, stats_summary, status)
267269
return jinja_tmpl.render(
268270
common_lib=diffs["common_summary"].get("components", {}).get("libraries", []),
269271
common_frameworks=diffs["common_summary"].get("components", {}).get("frameworks", []),
@@ -296,6 +298,68 @@ def render_bom_template(diffs, jinja_tmpl, options, stats_summary, status):
296298
)
297299

298300

301+
def render_minimal_bom_template(diffs, jinja_tmpl, options, stats_summary, status):
302+
common_components, diff_components_1, diff_components_2 = get_minimal_components_lists(diffs, options)
303+
return jinja_tmpl.render(
304+
common_services=diffs["common_summary"].get("services", []),
305+
common_deps=diffs["common_summary"].get("dependencies", []),
306+
common_other=common_components,
307+
common_vdrs=diffs["common_summary"].get("vulnerabilities", []),
308+
common_misc_data=json.dumps(diffs["common_summary"]["misc_data"]).replace("\\n", " ") if diffs["common_summary"].get("misc_data") else None,
309+
diff_other_1=diff_components_1,
310+
diff_other_2=diff_components_2,
311+
diff_services_1=diffs["diff_summary"].get(options.file_1, {}).get("services", []),
312+
diff_services_2=diffs["diff_summary"].get(options.file_2, {}).get("services", []),
313+
diff_deps_1=diffs["diff_summary"].get(options.file_1, {}).get("dependencies", []),
314+
diff_deps_2=diffs["diff_summary"].get(options.file_2, {}).get("dependencies", []),
315+
diff_vdrs_1=diffs["diff_summary"].get(options.file_1, {}).get("vulnerabilities", []),
316+
diff_vdrs_2=diffs["diff_summary"].get(options.file_2, {}).get("vulnerabilities", []),
317+
misc_data_1=json.dumps(diffs["diff_summary"][options.file_1]["misc_data"]).replace("\\n", " ") if diffs["diff_summary"].get(options.file_1, {}).get("misc_data", {}) else None,
318+
misc_data_2=json.dumps(diffs["diff_summary"][options.file_2]["misc_data"]).replace("\\n", " ") if diffs["diff_summary"].get(options.file_2, {}).get("misc_data", {}) else None,
319+
bom_1=options.file_1,
320+
bom_2=options.file_2,
321+
stats=stats_summary,
322+
diff_status=status,
323+
)
324+
325+
326+
def get_minimal_components_lists(diffs: Dict, options: "Options") -> Tuple[List, List, List]:
327+
match options.bom_profile:
328+
case "gn":
329+
common_components = [f"{i.get('group')}/{i.get('name')}".lstrip("/") for i in
330+
diffs["common_summary"].get("components", {}).get(
331+
"other_components", [])]
332+
diff_components_1 = [f"{i.get('group')}/{i.get('name')}".lstrip("/") for i in
333+
diffs["diff_summary"].get(options.file_1, {}).get(
334+
"components", {}).get("other_components", [])]
335+
diff_components_2 = [f"{i.get('group')}/{i.get('name')}".lstrip("/") for i in
336+
diffs["diff_summary"].get(options.file_2, {}).get(
337+
"components", {}).get("other_components", [])]
338+
case "nv":
339+
common_components = [f"{i.get('name')}@{i.get('version')}".rstrip("@") for i in
340+
diffs["common_summary"].get("components", {}).get(
341+
"other_components", [])]
342+
diff_components_1 = [f"{i.get('name')}@{i.get('version')}".rstrip("@") for i in
343+
diffs["diff_summary"].get(options.file_1, {}).get(
344+
"components", {}).get("other_components", [])]
345+
diff_components_2 = [f"{i.get('name')}@{i.get('version')}".rstrip("@") for i in
346+
diffs["diff_summary"].get(options.file_2, {}).get(
347+
"components", {}).get("other_components", [])]
348+
case _:
349+
common_components = [
350+
f"{i.get('group')}/{i.get('name')}@{i.get('version')}".lstrip("/").rstrip("@") for
351+
i in diffs["common_summary"].get("components", {}).get("other_components", [])]
352+
diff_components_1 = [
353+
f"{i.get('group')}/{i.get('name')}@{i.get('version')}".lstrip("/").rstrip("@") for
354+
i in diffs["diff_summary"].get(options.file_1, {}).get("components", {}).get(
355+
"other_components", [])]
356+
diff_components_2 = [
357+
f"{i.get('group')}/{i.get('name')}@{i.get('version')}".lstrip("/").rstrip("@") for
358+
i in diffs["diff_summary"].get(options.file_2, {}).get("components", {}).get(
359+
"other_components", [])]
360+
return common_components, diff_components_1, diff_components_2
361+
362+
299363
def render_csaf_template(diffs, jinja_tmpl, options, status):
300364
return jinja_tmpl.render(
301365
common_document=diffs["common_summary"].get("document", {}),
@@ -353,7 +417,7 @@ def sort_list(lst: List, sort_keys: List[str]) -> List:
353417
return lst
354418

355419

356-
def split_bom_ref(bom_ref: str):
420+
def split_bom_ref(bom_ref: str) -> Tuple[str, str]:
357421
if "@" not in bom_ref:
358422
return bom_ref, ""
359423
if bom_ref.count("@") == 1:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "custom-json-diff"
3-
version = "2.1.3"
3+
version = "2.1.4"
44
description = "CycloneDx BOM and Oasis CSAF diffing and comparison tool."
55
authors = [
66
{ name = "Caroline Russell", email = "[email protected]" },

0 commit comments

Comments
 (0)