Skip to content

Commit d604e28

Browse files
authored
Feat: Add --bom-profile argument for limited comparison. (#43)
Signed-off-by: Caroline Russell <[email protected]>
1 parent cb563a4 commit d604e28

File tree

7 files changed

+68
-22
lines changed

7 files changed

+68
-22
lines changed

README.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ options:
3737

3838
preset-diff usage
3939
```
40-
usage: custom-json-diff preset-diff [-h] [--allow-new-versions] [--allow-new-data] [--type PRESET_TYPE] [-r REPORT_TEMPLATE] [--include-extra INCLUDE]
40+
usage: custom-json-diff preset-diff [-h] [--allow-new-versions] [--allow-new-data] [--type PRESET_TYPE] [-r REPORT_TEMPLATE] [--include-extra INCLUDE] [--include-empty] [--bom-profile BOM_PROFILE]
4141
4242
options:
4343
-h, --help show this help message and exit
@@ -50,6 +50,9 @@ options:
5050
Jinja2 template to use for report generation.
5151
--include-extra INCLUDE
5252
BOM only - include properties/evidence/licenses/hashes/externalReferences (list which with comma, no space, inbetween).
53+
--include-empty, -e Include keys with empty values in summary.
54+
--bom-profile BOM_PROFILE, -b BOM_PROFILE
55+
Beta feature. Options: gn, gnv, nv -> only compare bom group/name/version.
5356
5457
```
5558
## Preset Diffs

custom_json_diff/cli.py

+15-8
Original file line numberDiff line numberDiff line change
@@ -52,52 +52,57 @@ def build_args() -> argparse.Namespace:
5252
dest="config"
5353
)
5454
subparsers = parser.add_subparsers(help="subcommand help")
55-
parser_pc_diff = subparsers.add_parser("preset-diff", help="Compare CycloneDX BOMs or Oasis CSAFs")
56-
parser_pc_diff.set_defaults(preset_type="")
57-
parser_pc_diff.add_argument(
55+
parser_ps_diff = subparsers.add_parser("preset-diff", help="Compare CycloneDX BOMs or Oasis CSAFs")
56+
parser_ps_diff.set_defaults(preset_type="")
57+
parser_ps_diff.add_argument(
5858
"--allow-new-versions",
5959
"-anv",
6060
action="store_true",
6161
help="BOM only - allow newer versions in second BOM to pass.",
6262
dest="allow_new_versions",
6363
default=False,
6464
)
65-
parser_pc_diff.add_argument(
65+
parser_ps_diff.add_argument(
6666
"--allow-new-data",
6767
"-and",
6868
action="store_true",
6969
help="Allow populated values in newer BOM or CSAF to pass against empty values in original BOM/CSAF.",
7070
dest="allow_new_data",
7171
default=False,
7272
)
73-
parser_pc_diff.add_argument(
73+
parser_ps_diff.add_argument(
7474
"--type",
7575
action="store",
7676
help="Either bom or csaf",
7777
dest="preset_type",
7878
)
79-
parser_pc_diff.add_argument(
79+
parser_ps_diff.add_argument(
8080
"-r",
8181
"--report-template",
8282
action="store",
8383
help="Jinja2 template to use for report generation.",
8484
dest="report_template",
8585
default="",
8686
)
87-
parser_pc_diff.add_argument(
87+
parser_ps_diff.add_argument(
8888
"--include-extra",
8989
action="store",
9090
help="BOM only - include properties/evidence/licenses/hashes/externalReferences (list which with comma, no space, inbetween).",
9191
dest="include",
9292
)
93-
parser_pc_diff.add_argument(
93+
parser_ps_diff.add_argument(
9494
"--include-empty",
9595
"-e",
9696
action="store_true",
9797
default=False,
9898
dest="include_empty",
9999
help="Include keys with empty values in summary.",
100100
)
101+
parser_ps_diff.add_argument(
102+
"--bom-profile",
103+
"-b",
104+
help="Beta feature. Options: gn, gnv, nv -> only compare bom group/name/version."
105+
)
101106
parser.add_argument(
102107
"-x",
103108
"--exclude",
@@ -124,6 +129,8 @@ def main():
124129
preset_type = args.preset_type.lower()
125130
if preset_type and preset_type not in ("bom", "csaf"):
126131
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.")
127134
options = Options(
128135
allow_new_versions=args.allow_new_versions,
129136
allow_new_data=args.allow_new_data,

custom_json_diff/lib/custom_diff.py

+22
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,32 @@ def compare_dicts(options: "Options") -> Tuple[int, "BomDicts|CsafDicts|FlatDict
7171

7272

7373
def filter_dict(data: Dict, options: "Options") -> FlatDicts:
74+
if options.bom_profile:
75+
match options.bom_profile:
76+
case "gnv":
77+
data = filter_on_bom_profile(data, {"group", "name", "version"})
78+
case "gn":
79+
data = filter_on_bom_profile(data, {"group", "name"})
80+
case "nv":
81+
data = filter_on_bom_profile(data, {"name", "version"})
7482
data = flatten(sort_dict_lists(data, options.sort_keys))
7583
return FlatDicts(data).filter_out_keys(options.exclude)
7684

7785

86+
def filter_on_bom_profile(data: Dict, profile_fields: Set) -> Dict:
87+
if not data.get("components"):
88+
return data
89+
new_components = []
90+
for comp in data["components"]:
91+
ncomp = {}
92+
for key, value in comp.items():
93+
if key in profile_fields:
94+
ncomp[key] = value
95+
new_components.append(ncomp)
96+
data["components"] = new_components
97+
return data
98+
99+
78100
def generate_counts(data: Dict) -> Dict:
79101
return {"libraries": len(data.get("components", {}).get("libraries", [])),
80102
"frameworks": len(data.get("components", {}).get("frameworks", [])),

custom_json_diff/lib/custom_diff_classes.py

+6-9
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class Options: # type: ignore
4646
svc_keys: List = field(default_factory=list)
4747
doc_num: int = 1
4848
include_empty: bool = False
49+
bom_profile: str = ""
4950

5051
def __post_init__(self):
5152
if self.config:
@@ -60,9 +61,9 @@ def __post_init__(self):
6061
self.include = toml_data.get("settings", {}).get("include_extra", [])
6162
self.include_empty = toml_data.get("settings", {}).get("include_empty", False)
6263
if self.preconfig_type == "bom":
63-
tmp_exclude, tmp_bom_key_fields, tmp_service_key_fields, self.do_advanced = (
64-
get_cdxgen_excludes(self.include, self.allow_new_versions, self.allow_new_data))
65-
self.comp_keys.extend(tmp_bom_key_fields)
64+
tmp_exclude, tmp_service_key_fields, self.do_advanced = (
65+
get_cdxgen_excludes(self.include, self.allow_new_data))
66+
# self.comp_keys.extend(tmp_bom_key_fields)
6667
self.svc_keys.extend(tmp_service_key_fields)
6768
self.exclude.extend(tmp_exclude)
6869
self.sort_keys.extend(["purl", "bom-ref", "content", "cve", "id", "url", "text", "ref", "name", "value", "location"])
@@ -71,6 +72,7 @@ def __post_init__(self):
7172
self.sort_keys.extend(["text", "title", "product_id", "url"])
7273
self.exclude = list(set(self.exclude))
7374
self.include = list(set(self.include))
75+
# deprecated
7476
self.comp_keys = list(set(self.comp_keys))
7577
self.svc_keys = list(set(self.svc_keys))
7678
self.sort_keys = list(set(self.sort_keys))
@@ -980,7 +982,7 @@ def create_search_key(key: str, value: str) -> str:
980982
return combined_key
981983

982984

983-
def get_cdxgen_excludes(includes: List[str], allow_new_versions: bool, allow_new_data: bool) -> Tuple[List[str], List[str], List[str], bool]:
985+
def get_cdxgen_excludes(includes: List[str], allow_new_data: bool) -> Tuple[List[str], List[str], bool]:
984986
excludes = {'metadata.timestamp': 'metadata.timestamp', 'serialNumber': 'serialNumber',
985987
'metadata.tools.components.[].version': 'metadata.tools.components.[].version',
986988
'metadata.tools.components.[].purl': 'metadata.tools.components.[].purl',
@@ -990,17 +992,12 @@ def get_cdxgen_excludes(includes: List[str], allow_new_versions: bool, allow_new
990992
'externalReferences': 'components.[].externalReferences',
991993
'externalreferences': 'components.[].externalReferences'}
992994
if allow_new_data:
993-
component_keys = []
994995
service_keys = []
995996
else:
996-
component_keys = ['name', 'author', 'publisher', 'group', 'type', 'scope', 'description']
997997
service_keys = ['name', 'authenticated', 'x-trust-boundary', 'endpoints']
998-
if not allow_new_versions:
999-
component_keys.extend([i for i in ('version', 'purl', 'bom-ref', 'version') if i not in excludes])
1000998

1001999
return (
10021000
[v for k, v in excludes.items() if k not in includes],
1003-
[v for v in component_keys if v not in excludes],
10041001
[v for v in service_keys if v not in excludes],
10051002
allow_new_data,
10061003
)

custom_json_diff/lib/utils.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,10 @@ def export_html_report(outfile: str, diffs: Dict, options: "Options", status: in
101101
stats_summary: Dict | None = None) -> None:
102102
if options.report_template:
103103
template_file = options.report_template
104+
elif options.bom_profile:
105+
template_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "bom_diff_template_minimal.j2")
104106
else:
105-
template_file = options.report_template or os.path.join(os.path.dirname(os.path.realpath(__file__)), f"{options.preconfig_type}_diff_template.j2")
107+
template_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), f"{options.preconfig_type}_diff_template.j2")
106108
template = file_read(template_file)
107109
jinja_env = Environment(autoescape=True)
108110
jinja_tmpl = jinja_env.from_string(str(template))
@@ -112,7 +114,7 @@ def export_html_report(outfile: str, diffs: Dict, options: "Options", status: in
112114
else:
113115
report_result = render_csaf_template(diffs, jinja_tmpl, options, status)
114116
except TypeError:
115-
logger.warning(f"Could not render html report for {options.file_1} and {options.file_2} BOM diff. Likely an expected key is missing.")
117+
logger.warning(f"Could not render html report for {options.file_1} and {options.file_2} {options.preconfig_type} diff. Likely an expected key is missing.")
116118
return
117119
file_write(outfile, report_result, error_msg=f"Unable to generate HTML report at {outfile}.",
118120
success_msg=f"HTML report generated: {outfile}")

pyproject.toml

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

test/test_custom_json_diff.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import pytest
44

55
from custom_json_diff.lib.custom_diff import (
6-
compare_dicts, get_bom_status, get_diff, json_to_class
6+
compare_dicts, filter_on_bom_profile, get_bom_status, get_diff, json_to_class
77
)
88
from custom_json_diff.lib.custom_diff_classes import Options
99

@@ -90,3 +90,18 @@ def test_get_bom_status():
9090
assert max(get_bom_status(diff_summary_1), get_bom_status(diff_summary_2)) == 2
9191
diff_summary_1["services"] = [{"name": "test"}]
9292
assert max(get_bom_status(diff_summary_1), get_bom_status(diff_summary_2)) == 3
93+
94+
95+
def test_filter_on_bom_profile():
96+
data = {"components": [{"name": "component1", "version": "1.0", "group": "group1"},
97+
{"name": "component2", "version": "2.0"}]}
98+
assert filter_on_bom_profile(data, {"name", "version"}) == {'components': [{'name': 'component1', 'version': '1.0'},
99+
{'name': 'component2', 'version': '2.0'}]}
100+
data = {"components": [{"name": "component1", "version": "1.0", "group": "group1"}]}
101+
assert filter_on_bom_profile(data, {"name", "group"}) == {"components": [{"name": "component1", "group": "group1"}]}
102+
assert filter_on_bom_profile({"components": []}, {"name"}) == {"components": []}
103+
assert filter_on_bom_profile({}, {"name"}) == {}
104+
assert filter_on_bom_profile( {"components": []}, {"name"}) == {"components": []}
105+
data = {"metadata": {"author": "test"},
106+
"components": [{"name": "component1", "version": "1.0"}]}
107+
assert filter_on_bom_profile(data, {"name"}) == {"metadata": {"author": "test"}, "components": [{"name": "component1"}]}

0 commit comments

Comments
 (0)