Skip to content

Commit bba7c08

Browse files
baprustyfmoessbauer
authored andcommitted
feat: add delta command to show components added in target SBOM
Introduce a `delta` command that compares base/reference and target SBOMs and identifies components present only in the target. The command generates a new SBOM containing these additional components, making it easier to track previously cleared components and identify new ones requiring license clearance. [Felix: major code cleanup and refactoring to use more debsbom internal infrastructure] Signed-off-by: Badrikesh Prusty <badrikesh.prusty@siemens.com> Signed-off-by: Felix Moessbauer <felix.moessbauer@siemens.com>
1 parent 3049e5d commit bba7c08

File tree

13 files changed

+454
-2
lines changed

13 files changed

+454
-2
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,20 @@ Source packages are especially relevant for security as CVEs in the Debian ecosy
1414
Please refer to the [debsbom documentation](https://siemens.github.io/debsbom/).
1515

1616
```
17-
usage: debsbom [-h] [--version] [-v] [--progress | --json] {generate,merge,download,source-merge,repack,export} ...
17+
usage: debsbom [-h] [--version] [-v] [--progress | --json] {generate,merge,download,source-merge,repack,export,delta} ...
1818
1919
SBOM tool for Debian systems.
2020
2121
positional arguments:
22-
{generate,merge,download,source-merge,repack,export}
22+
{generate,merge,download,source-merge,repack,export,delta}
2323
sub command help
2424
generate generate a SBOM for a Debian system
2525
merge merge multiple SBOMs
2626
download download referenced packages
2727
source-merge merge referenced source packages
2828
repack repack sources and sbom
2929
export export SBOM as graph
30+
delta list components added in target SBOM
3031
3132
options:
3233
-h, --help show this help message and exit

docs/source/commands.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ Commands
1010
commands/repack
1111
commands/export
1212
commands/merge
13+
commands/delta
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
The ``delta`` command compares two SBOMs and produces a new SBOM containing only the
2+
components that are present in the target SBOM but not in the base (reference) SBOM.
3+
4+
The most common use-case is identifying new or added components between two builds, images, or
5+
distribution states (for example, comparing a previous release SBOM against a newer one),
6+
including filtering out already license-cleared components to generate an SBOM containing only
7+
components pending license clearance.
8+
9+
The comparison is directional:
10+
11+
* Base SBOM – treated as the reference
12+
* Target SBOM – treated as the new or updated SBOM
13+
14+
Given the following structure:
15+
16+
Base SBOM
17+
18+
.. code-block::
19+
20+
base-root
21+
|- binary-dep1
22+
| |- source-dep1
23+
|- binary-dep2
24+
25+
Target SBOM
26+
27+
.. code-block::
28+
29+
target-root
30+
|- binary-dep1
31+
| |- source-dep1
32+
|- binary-dep2
33+
|- binary-dep3
34+
| |- source-dep3
35+
36+
Running delta would produce:
37+
38+
.. code-block::
39+
40+
delta-doc-root
41+
|- binary-dep3
42+
| |- source-dep3
43+
44+
Components are considered the same if they share the same PURL (Package URL). Only components
45+
that are new in the target SBOM, along with their nested dependencies, are included in the
46+
resulting SBOM.

docs/source/commands/delta.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
``delta`` command
2+
=================
3+
4+
.. include:: delta-description.inc
5+
6+
.. note::
7+
Only SBOMs of the same type can be compared. Specifying both SPDX and CDX SBOMs will cause an error.

docs/source/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
("man/debsbom-generate", "debsbom-generate", "debsbom generate command", [author], 1),
7272
("man/debsbom-merge", "debsbom-merge", "debsbom merge command", [author], 1),
7373
("man/debsbom-repack", "debsbom-repack", "debsbom repack command", [author], 1),
74+
("man/debsbom-delta", "debsbom-delta", "debsbom delta command", [author], 1),
7475
(
7576
"man/debsbom-source-merge",
7677
"debsbom-source-merge",

docs/source/examples.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,26 @@ And the same list of packages can be repacked:
185185
sbom.cdx.json \
186186
sbom.cdx.repacked.json
187187
188+
Delta SBOMs
189+
~~~~~~~~~~~
190+
191+
The :doc:`/commands/delta` compares a base (reference) SBOM with a target (new) SBOM and produces
192+
a new SBOM containing only the components present in the target. The typical use-case is identifying
193+
newly added or changed components between two builds or releases.
194+
195+
Use ``debsbom delta`` when you only want to see changed or added components, e.g., to generate an
196+
SBOM for license clearance.
197+
198+
.. code-block:: bash
199+
200+
debsbom delta sbom.old.cdx.json sbom.cdx.json extras.cdx.json
201+
202+
You can also pass SBOMs via stdin, but you also have to pass the SBOM type in this case:
203+
204+
.. code-block:: bash
205+
206+
cat sbom.old.spdx.json sbom.spdx.json | debsbom delta -t spdx - - -o -
207+
188208
Export as Graph
189209
~~~~~~~~~~~~~~~
190210

docs/source/man/debsbom-delta.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
:orphan:
2+
3+
debsbom delta
4+
=============
5+
6+
.. argparse::
7+
:module: debsbom.cli
8+
:func: setup_parser
9+
:prog: debsbom
10+
:path: delta
11+
:manpage:
12+
13+
.. automodule:: debsbom.commands.delta.DeltaCmd
14+
:noindex:
15+
16+
.. include:: ../commands/delta-description.inc
17+
18+
SEE ALSO
19+
--------
20+
21+
:manpage:`debsbom-generate(1)`
22+
23+
.. include:: _debsbom-man-footer.inc

src/debsbom/cli.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from .commands.source_merge import SourceMergeCmd
1919
from .commands.repack import RepackCmd
2020
from .commands.export import ExportCmd
21+
from .commands.delta import DeltaCmd
2122

2223
# Attempt to import optional download dependencies to check their availability.
2324
# The success or failure of these imports determines if download features are enabled.
@@ -64,6 +65,9 @@ def setup_parser():
6465
)
6566
RepackCmd.setup_parser(subparser.add_parser("repack", help="repack sources and sbom"))
6667
ExportCmd.setup_parser(subparser.add_parser("export", help="export SBOM as graph"))
68+
DeltaCmd.setup_parser(
69+
subparser.add_parser("delta", help="list components changed in target SBOM")
70+
)
6771

6872
return parser
6973

@@ -97,6 +101,8 @@ def main():
97101
ExportCmd.run(args)
98102
elif args.cmd == "merge":
99103
MergeCmd.run(args)
104+
elif args.cmd == "delta":
105+
DeltaCmd.run(args)
100106
except DistroArchUnknownError as e:
101107
logger.error(f"debsbom: error: {e}. Set --distro-arch to dpkg architecture (e.g. amd64)")
102108
sys.exit(-2)

src/debsbom/commands/delta.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright (C) 2025 Siemens
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
from ..delta.delta import DeltaGenerator
6+
from .output import SbomOutput
7+
from .input import GenerateInput, SbomInput
8+
from ..bomreader.bomreader import BomReader
9+
10+
11+
class DeltaCmd(GenerateInput, SbomInput):
12+
"""
13+
Compute the delta between base and target SBOMs, producing a new SBOM containing only
14+
additional components from the target.
15+
"""
16+
17+
@classmethod
18+
def run(cls, args):
19+
readers = cls.create_sbom_processors(
20+
args, BomReader, sbom_args=["base_sbom", "target_sbom"], sbom_allow_multiple=True
21+
)
22+
if len(readers) != 2:
23+
raise ValueError("can only compare exactly two SBOMs")
24+
25+
sbom_types = set([r.sbom_type() for r in readers])
26+
if len(sbom_types) > 1:
27+
raise ValueError("can not compare mixed SPDX and CycloneDX documents")
28+
sbom_type = readers[0].sbom_type()
29+
30+
docs = [r.read() for r in readers]
31+
32+
delta_generator = DeltaGenerator.create(
33+
sbom_type=sbom_type,
34+
distro_name=args.distro_name,
35+
distro_supplier=args.distro_supplier,
36+
distro_version=args.distro_version,
37+
base_distro_vendor=args.base_distro_vendor,
38+
spdx_namespace=args.spdx_namespace,
39+
cdx_serialnumber=args.cdx_serialnumber,
40+
timestamp=args.timestamp,
41+
)
42+
bom = delta_generator.delta(base_sbom=docs[0], target_sbom=docs[1])
43+
SbomOutput.write_out_arg(bom, sbom_type, args.out, args.validate)
44+
45+
@classmethod
46+
def setup_parser(cls, parser):
47+
cls.parser_add_generate_input_args(parser, default_out="extras")
48+
cls.parser_add_sbom_input_args(
49+
parser, required=True, sbom_args=["base_sbom", "target_sbom"]
50+
)

src/debsbom/commands/output.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22
#
33
# SPDX-License-Identifier: MIT
44

5+
import logging
56
from pathlib import Path
67
import sys
78

89
from ..sbom import SBOMType
910
from ..bomwriter.bomwriter import BomWriter
1011

12+
logger = logging.getLogger(__name__)
13+
1114

1215
class SbomOutput:
1316
"""
@@ -18,8 +21,10 @@ class SbomOutput:
1821
def write_out_arg(cls, bom, bomtype: SBOMType, out: str, validate: bool):
1922
writer = BomWriter.create(bomtype)
2023
if out == "-":
24+
logger.info("Emit SBOM on stdout")
2125
writer.write_to_stream(bom, sys.stdout, validate=validate)
2226
else:
2327
if not out.endswith(f".{bomtype}.json"):
2428
out += f".{bomtype}.json"
29+
logger.info(f"Write SBOM to file {out}")
2530
writer.write_to_file(bom, Path(out), validate=validate)

0 commit comments

Comments
 (0)