Skip to content

Commit

Permalink
Fix --bom-profile bugs. (#44)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
cerrussell authored Nov 26, 2024
1 parent d604e28 commit f72d093
Show file tree
Hide file tree
Showing 8 changed files with 784 additions and 38 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
include custom_json_diff/lib/bom_diff_template.j2
include custom_json_diff/lib/bom_diff_template_minimal.j2
include custom_json_diff/lib/csaf_diff_template.j2
8 changes: 5 additions & 3 deletions custom_json_diff/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,9 @@ def main():
preset_type = args.preset_type.lower()
if preset_type and preset_type not in ("bom", "csaf"):
raise ValueError("Preconfigured type must be either bom or csaf.")
if args.bom_profile and args.bom_profile not in ("gn", "gnv", "nv"):
raise ValueError("BOM profile must be either gn, gnv, or nv.")
if args.bom_profile:
if args.bom_profile not in ("gn", "gnv", "nv"):
raise ValueError("BOM profile must be either gn, gnv, or nv.")
options = Options(
allow_new_versions=args.allow_new_versions,
allow_new_data=args.allow_new_data,
Expand All @@ -142,7 +143,8 @@ def main():
file_2=args.input[1],
output=args.output,
report_template=args.report_template,
include_empty=args.include_empty
include_empty=args.include_empty,
bom_profile=args.bom_profile
)
result, j1, j2 = compare_dicts(options)
if preset_type == "bom":
Expand Down
2 changes: 1 addition & 1 deletion custom_json_diff/lib/bom_diff_template.j2
Original file line number Diff line number Diff line change
Expand Up @@ -869,7 +869,7 @@
{% if not misc_data_1 %}
<td></td>
{% endif %}
{% if misc_data_1 %}
{% if misc_data_2 %}
<td>{{ misc_data_2 }}</td>
{% endif %}
{% if not misc_data_2 %}
Expand Down
645 changes: 645 additions & 0 deletions custom_json_diff/lib/bom_diff_template_minimal.j2

Large diffs are not rendered by default.

49 changes: 33 additions & 16 deletions custom_json_diff/lib/custom_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,26 +108,43 @@ def generate_counts(data: Dict) -> Dict:


def generate_bom_diff(bom: BomDicts, commons: BomDicts, common_refs: Dict) -> Dict:
diff_summary = {
"components": {"applications": [], "frameworks": [], "libraries": [],
"other_components": []},
return {
"components": get_unique_components(bom, common_refs),
"dependencies": [i.to_dict() for i in bom.dependencies if i.ref not in common_refs["dependencies"]],
"services": [i.to_dict() for i in bom.services if i.search_key not in common_refs["services"]],
"vulnerabilities": [i.to_dict() for i in bom.vdrs if i.bom_ref not in common_refs["vdrs"]]
"vulnerabilities": [i.to_dict() for i in bom.vdrs if i.bom_ref not in common_refs["vdrs"]],
"misc_data": (bom.misc_data - commons.misc_data).to_dict()
}
for i in bom.components:
if i.bom_ref not in common_refs["components"]:
match i.component_type:
case "application":
diff_summary["components"]["applications"].append(i.to_dict()) #type: ignore
case "framework":
diff_summary["components"]["frameworks"].append(i.to_dict()) #type: ignore
case "library":
diff_summary["components"]["libraries"].append(i.to_dict()) #type: ignore


def get_unique_components(bom: BomDicts, common_refs: Dict):
components: Dict[str, List] = {"applications": [], "frameworks": [], "libraries": [], "other_components": []}
if bom.options.bom_profile:
for i in bom.components:
match bom.options.bom_profile:
case "nv":
key = f"{i.name}@{i.version}"
case "gn":
key = f"{i.group}/{i.name}"
case _:
diff_summary["components"]["other_components"].append(i.to_dict()) #type: ignore
diff_summary["misc_data"] = (bom.misc_data - commons.misc_data).to_dict()
return diff_summary
key = f"{i.group}/{i.name}@{i.version}"
if key not in common_refs["components"]:
components["other_components"].append(i.to_dict())
return components
for i in bom.components:
key = i.bom_ref if "components.[].bom_ref" not in bom.options.exclude else f"{i.group}/{i.name}@{i.version}"
if key in common_refs["components"]:
continue
match i.component_type:
case "application":
components["applications"].append(i.to_dict()) # type: ignore
case "framework":
components["frameworks"].append(i.to_dict()) # type: ignore
case "library":
components["libraries"].append(i.to_dict()) # type: ignore
case _:
components["other_components"].append(i.to_dict()) # type: ignore
return components


def generate_csaf_diff(csaf: CsafDicts, commons: CsafDicts, common_refs: Dict[str, Set]) -> Dict:
Expand Down
47 changes: 32 additions & 15 deletions custom_json_diff/lib/custom_diff_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,18 +391,18 @@ def vdrs(self, value):
_, _, _, _, self._vdrs = import_bom_dict(self.options, {}, vulnerabilities=value)

def intersection(self, other, title: str = "") -> "BomDicts":
components = []
dependencies = []
services = []
vulnerabilities = []
components = Array([])
dependencies = Array([])
services = Array([])
vulnerabilities = Array([])
if self.components and other.components:
components = [i for i in self.components if i in other.components]
components = Array([i for i in self.components if i in other.components])
if self.services and other.services:
services = [i for i in self.services if i in other.services]
services = Array([i for i in self.services if i in other.services])
if self.dependencies and other.dependencies:
dependencies = [i for i in self.dependencies if i in other.dependencies]
dependencies = Array([i for i in self.dependencies if i in other.dependencies])
if self.vdrs and other.vdrs:
vulnerabilities = [i for i in self.vdrs if i in other.vdrs]
vulnerabilities = Array([i for i in self.vdrs if i in other.vdrs])
other_data = self.misc_data.intersection(other.misc_data)
options = deepcopy(self.options)
return BomDicts(
Expand Down Expand Up @@ -436,12 +436,21 @@ def generate_comp_counts(self) -> Dict:
"vulnerabilities": len(self.vdrs)}

def get_refs(self) -> Dict:
return {
"components": {i.bom_ref for i in self.components},
"dependencies": {i.ref for i in self.dependencies},
"services": {i.search_key for i in self.services},
"vdrs": {i.bom_ref for i in self.vdrs}
}
refs = {
"dependencies": {i.ref for i in self.dependencies},
"services": {i.search_key for i in self.services},
"vdrs": {i.bom_ref for i in self.vdrs}
}
match self.options.bom_profile:
case "gnv":
refs |= {"components": {f"{i.group}/{i.name}@{i.version}" for i in self.components}}
case "gn":
refs |= {"components": {f"{i.group}/{i.name}" for i in self.components}}
case "nv":
refs |= {"components": {f"{i.name}@{i.version}" for i in self.components}}
case _:
refs |= {"components": {i.bom_ref for i in self.components}}
return refs

def to_dict(self) -> Dict:
return {
Expand Down Expand Up @@ -1031,7 +1040,7 @@ def import_bom_dict(
if not value:
elements[i] = []
components, services, dependencies, vulnerabilities = elements
return other_data, Array(components), Array(services), Array(dependencies), Array(vulnerabilities) # type: ignore
return other_data, Array(dedupe_components(components)), Array(services), Array(dependencies), Array(vulnerabilities) # type: ignore


def import_csaf(options: "Options", original_data: Dict | None = None, document: FlatDicts | None = None,
Expand Down Expand Up @@ -1092,3 +1101,11 @@ def parse_bom_dict(original_data: Dict, options: Options) -> Tuple[FlatDicts, Li
if key not in {"components", "dependencies", "services", "vulnerabilities"}:
other_data |= {key: value}
return FlatDicts(other_data), components, services, dependencies, vulnerabilities


def dedupe_components(components: List) -> List:
deduped = []
for component in components:
if component not in deduped:
deduped.append(component)
return deduped
68 changes: 66 additions & 2 deletions custom_json_diff/lib/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import re
import sys
from datetime import date, datetime
from typing import Any, Dict, List, TYPE_CHECKING
from typing import Any, Dict, List, Tuple, TYPE_CHECKING

import packageurl
import semver
Expand Down Expand Up @@ -264,6 +264,8 @@ def manual_version_compare_noeq(v1: List, v2: List, comparator: str) -> bool:


def render_bom_template(diffs, jinja_tmpl, options, stats_summary, status):
if options.bom_profile:
return render_minimal_bom_template(diffs, jinja_tmpl, options, stats_summary, status)
return jinja_tmpl.render(
common_lib=diffs["common_summary"].get("components", {}).get("libraries", []),
common_frameworks=diffs["common_summary"].get("components", {}).get("frameworks", []),
Expand Down Expand Up @@ -296,6 +298,68 @@ def render_bom_template(diffs, jinja_tmpl, options, stats_summary, status):
)


def render_minimal_bom_template(diffs, jinja_tmpl, options, stats_summary, status):
common_components, diff_components_1, diff_components_2 = get_minimal_components_lists(diffs, options)
return jinja_tmpl.render(
common_services=diffs["common_summary"].get("services", []),
common_deps=diffs["common_summary"].get("dependencies", []),
common_other=common_components,
common_vdrs=diffs["common_summary"].get("vulnerabilities", []),
common_misc_data=json.dumps(diffs["common_summary"]["misc_data"]).replace("\\n", " ") if diffs["common_summary"].get("misc_data") else None,
diff_other_1=diff_components_1,
diff_other_2=diff_components_2,
diff_services_1=diffs["diff_summary"].get(options.file_1, {}).get("services", []),
diff_services_2=diffs["diff_summary"].get(options.file_2, {}).get("services", []),
diff_deps_1=diffs["diff_summary"].get(options.file_1, {}).get("dependencies", []),
diff_deps_2=diffs["diff_summary"].get(options.file_2, {}).get("dependencies", []),
diff_vdrs_1=diffs["diff_summary"].get(options.file_1, {}).get("vulnerabilities", []),
diff_vdrs_2=diffs["diff_summary"].get(options.file_2, {}).get("vulnerabilities", []),
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,
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,
bom_1=options.file_1,
bom_2=options.file_2,
stats=stats_summary,
diff_status=status,
)


def get_minimal_components_lists(diffs: Dict, options: "Options") -> Tuple[List, List, List]:
match options.bom_profile:
case "gn":
common_components = [f"{i.get('group')}/{i.get('name')}".lstrip("/") for i in
diffs["common_summary"].get("components", {}).get(
"other_components", [])]
diff_components_1 = [f"{i.get('group')}/{i.get('name')}".lstrip("/") for i in
diffs["diff_summary"].get(options.file_1, {}).get(
"components", {}).get("other_components", [])]
diff_components_2 = [f"{i.get('group')}/{i.get('name')}".lstrip("/") for i in
diffs["diff_summary"].get(options.file_2, {}).get(
"components", {}).get("other_components", [])]
case "nv":
common_components = [f"{i.get('name')}@{i.get('version')}".rstrip("@") for i in
diffs["common_summary"].get("components", {}).get(
"other_components", [])]
diff_components_1 = [f"{i.get('name')}@{i.get('version')}".rstrip("@") for i in
diffs["diff_summary"].get(options.file_1, {}).get(
"components", {}).get("other_components", [])]
diff_components_2 = [f"{i.get('name')}@{i.get('version')}".rstrip("@") for i in
diffs["diff_summary"].get(options.file_2, {}).get(
"components", {}).get("other_components", [])]
case _:
common_components = [
f"{i.get('group')}/{i.get('name')}@{i.get('version')}".lstrip("/").rstrip("@") for
i in diffs["common_summary"].get("components", {}).get("other_components", [])]
diff_components_1 = [
f"{i.get('group')}/{i.get('name')}@{i.get('version')}".lstrip("/").rstrip("@") for
i in diffs["diff_summary"].get(options.file_1, {}).get("components", {}).get(
"other_components", [])]
diff_components_2 = [
f"{i.get('group')}/{i.get('name')}@{i.get('version')}".lstrip("/").rstrip("@") for
i in diffs["diff_summary"].get(options.file_2, {}).get("components", {}).get(
"other_components", [])]
return common_components, diff_components_1, diff_components_2


def render_csaf_template(diffs, jinja_tmpl, options, status):
return jinja_tmpl.render(
common_document=diffs["common_summary"].get("document", {}),
Expand Down Expand Up @@ -353,7 +417,7 @@ def sort_list(lst: List, sort_keys: List[str]) -> List:
return lst


def split_bom_ref(bom_ref: str):
def split_bom_ref(bom_ref: str) -> Tuple[str, str]:
if "@" not in bom_ref:
return bom_ref, ""
if bom_ref.count("@") == 1:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "custom-json-diff"
version = "2.1.3"
version = "2.1.4"
description = "CycloneDx BOM and Oasis CSAF diffing and comparison tool."
authors = [
{ name = "Caroline Russell", email = "[email protected]" },
Expand Down

0 comments on commit f72d093

Please sign in to comment.