Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix --bom-profile bugs. #44

Merged
merged 5 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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