From 3758bd8db9e09b2500e8a4f808673881be1a70c1 Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Mon, 10 Aug 2026 21:33:53 +0200 Subject: [PATCH] contrib: add a tool to generate an SPDX formatted software bill of materials (SBOM) using information available in our package database. --- contrib/spdx-sbom/README.md | 26 +++++ contrib/spdx-sbom/lib/__init__.py | 26 +++++ contrib/spdx-sbom/lib/pkg.py | 101 ++++++++++++++++++ contrib/spdx-sbom/lib/spdx/__init__.py | 93 ++++++++++++++++ contrib/spdx-sbom/lib/spdx/spdx_agent.py | 33 ++++++ .../spdx-sbom/lib/spdx/spdx_creation_info.py | 46 ++++++++ contrib/spdx-sbom/lib/spdx/spdx_element.py | 71 ++++++++++++ contrib/spdx-sbom/lib/spdx/spdx_license.py | 44 ++++++++ contrib/spdx-sbom/lib/spdx/spdx_package.py | 43 ++++++++ .../spdx-sbom/lib/spdx/spdx_relationship.py | 43 ++++++++ contrib/spdx-sbom/spdx-generator.py | 71 ++++++++++++ 11 files changed, 597 insertions(+) create mode 100644 contrib/spdx-sbom/README.md create mode 100644 contrib/spdx-sbom/lib/__init__.py create mode 100644 contrib/spdx-sbom/lib/pkg.py create mode 100644 contrib/spdx-sbom/lib/spdx/__init__.py create mode 100644 contrib/spdx-sbom/lib/spdx/spdx_agent.py create mode 100644 contrib/spdx-sbom/lib/spdx/spdx_creation_info.py create mode 100644 contrib/spdx-sbom/lib/spdx/spdx_element.py create mode 100644 contrib/spdx-sbom/lib/spdx/spdx_license.py create mode 100644 contrib/spdx-sbom/lib/spdx/spdx_package.py create mode 100644 contrib/spdx-sbom/lib/spdx/spdx_relationship.py create mode 100755 contrib/spdx-sbom/spdx-generator.py diff --git a/contrib/spdx-sbom/README.md b/contrib/spdx-sbom/README.md new file mode 100644 index 0000000000..6ac59092b2 --- /dev/null +++ b/contrib/spdx-sbom/README.md @@ -0,0 +1,26 @@ +# SPDX Generator + +A small tool for generating **SPDX 3** files for software packages. + +## Usage + +Provide the package name: + +```bash +spdx-generator.py +``` + +Example: + +```bash +spdx-generator.py opnsense +``` + +The tool generates an SPDX formatted software bill of materials for the package offered as parameter. + + +# TODO + +* normalize licenseExpression to spdx standards +* implement creationInfo tag properly + diff --git a/contrib/spdx-sbom/lib/__init__.py b/contrib/spdx-sbom/lib/__init__.py new file mode 100644 index 0000000000..225742df4e --- /dev/null +++ b/contrib/spdx-sbom/lib/__init__.py @@ -0,0 +1,26 @@ +""" + Copyright (c) 2026 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +""" diff --git a/contrib/spdx-sbom/lib/pkg.py b/contrib/spdx-sbom/lib/pkg.py new file mode 100644 index 0000000000..522bf31dac --- /dev/null +++ b/contrib/spdx-sbom/lib/pkg.py @@ -0,0 +1,101 @@ +""" + Copyright (c) 2026 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + -------------------------------------------------------------------------------------------------------------------- + Interface to pkg +""" +import subprocess +import sqlite3 + + +class SimpleContainer: + def __init__(self, payload:dict): + self._payload = payload + self._deps = [] + + def add_dependency(self, container:"SimpleContainer"): + self._deps.append(container) + + def __iter__(self): + """ recursively collect all packages and their dependencies + """ + if self._deps: + yield self, self._deps + for dep in self._deps: + yield from dep + else: + yield self, None + + def __getattr__(self, prop): + """ convenient wrapper access _payload properties + """ + return self._payload[prop] + + def get_data(self): + return self._payload + + +class Pkg: + def __init__(self): + self._pkg_map = {} + self._index_pkg() + + @staticmethod + def _fetch_details(): + result = {} + dbdir = subprocess.run(['pkg', 'config', 'PKG_DBDIR'], text=True, capture_output=True).stdout.strip() + dburi = "file:%s/local.sqlite?mode=ro" % dbdir + con = sqlite3.connect(dburi, uri=True) + con.row_factory = sqlite3.Row + for row in con.execute('select name, desc, comment from packages'): + result[row['name']] = row + return result + + def _index_pkg(self): + pkg_details = self._fetch_details() + args = ['/usr/sbin/pkg', 'rquery', '%n|%v|%o|%L|%m|%l|%w'] + for line in subprocess.run(args, text=True, capture_output=True).stdout.split("\n"): + parts = line.split('|') + if len(parts) > 1: + pkgname = parts[0] + self._pkg_map[pkgname] = SimpleContainer({ + 'id': '%s:%s' % (pkgname, parts[1]), + 'name': pkgname, + 'packageVersion': parts[1], + 'origin': parts[2], + 'licenseExpression': (' %s ' % parts[5]).join([x.strip() for x in parts[3].split(',')]), + 'maintainer': parts[4], + 'homepage': parts[5], + 'comment': pkg_details[pkgname]['comment'] if pkgname in pkg_details else '', + 'description': pkg_details[pkgname]['desc'] if pkgname in pkg_details else '', + 'primaryPurpose': 'library' if pkgname.startswith('lib') else 'application' + }) + args = ['/usr/sbin/pkg', 'rquery', '%n|%dn'] + for line in subprocess.run(args, text=True, capture_output=True).stdout.split("\n"): + parts = line.split('|') + if len(parts) == 2: + self._pkg_map[parts[0]].add_dependency(self._pkg_map[parts[1]]) + + def query(self, pkg): + return self._pkg_map[pkg] if pkg in self._pkg_map else None diff --git a/contrib/spdx-sbom/lib/spdx/__init__.py b/contrib/spdx-sbom/lib/spdx/__init__.py new file mode 100644 index 0000000000..3a638226c9 --- /dev/null +++ b/contrib/spdx-sbom/lib/spdx/__init__.py @@ -0,0 +1,93 @@ +""" + Copyright (c) 2026 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + -------------------------------------------------------------------------------------------------------------------- + Hightly simplified wrapper around the SPDX format used to generate software bill of materials (SBOM) + More information about the format can be found at https://spdx.github.io/spdx-spec/v3.0.1/ +""" +from .spdx_agent import SpdxAgent +from .spdx_creation_info import SpdxCreationInfo +from .spdx_element import SpdxElement +from .spdx_license import SpdxLicenseExpression +from .spdx_package import SpdxPackage +from .spdx_relationship import SpdxRelationship + + +class SpdxSoftwareSbom: + @staticmethod + def spdx_type(): + return 'software_Sbom' + + def get_data(self): + # XXX: temp static block + return { + 'spdxId': 'urn:opnsense:sbom', + 'type': 'software_Sbom', + 'creationInfo': '_:b0', + 'profileConformance': [ + 'core', + 'software' + ], + 'rootElement': self._rootElement.spdxId + } + + def __init__(self, root:SpdxElement): + self._rootElement = root + + +class SpdxDocument: + def __init__(self, root:SpdxElement): + self._contents = { + '@id': SpdxCreationInfo(), # XXX: replace + 'urn:opnsense:sbom': root # XXX: static + } + # XXX static data from SpdxCreationInfo + self.add_object(SpdxAgent(spdxId='urn:opnsense:organization:opnsense', name='OPNsense')) + + def add_object(self, obj:SpdxElement): + self._contents[obj.spdxId] = obj + return obj + + def get_data(self): + # XXX: temp static block + return { + 'spdxId': 'urn:opnsense:document', + 'type': 'SpdxDocument', + 'creationInfo': '_:b0', + 'profileConformance': [ + 'core', + 'software' + ], + 'rootElement': 'urn:opnsense:sbom' + } + + def dump(self): + items = [self.get_data()] + for item in self._contents.values(): + items.append(item.get_data()) + + return { + '@context': 'https://spdx.org/rdf/3.0.1/spdx-context.jsonld', + '@graph': items + } diff --git a/contrib/spdx-sbom/lib/spdx/spdx_agent.py b/contrib/spdx-sbom/lib/spdx/spdx_agent.py new file mode 100644 index 0000000000..813c8acde6 --- /dev/null +++ b/contrib/spdx-sbom/lib/spdx/spdx_agent.py @@ -0,0 +1,33 @@ +""" + Copyright (c) 2026 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +""" +from .spdx_element import SpdxElement + + +class SpdxAgent(SpdxElement): + @staticmethod + def spdx_type(): + return 'Agent' diff --git a/contrib/spdx-sbom/lib/spdx/spdx_creation_info.py b/contrib/spdx-sbom/lib/spdx/spdx_creation_info.py new file mode 100644 index 0000000000..541a878017 --- /dev/null +++ b/contrib/spdx-sbom/lib/spdx/spdx_creation_info.py @@ -0,0 +1,46 @@ +""" + Copyright (c) 2026 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + -------------------------------------------------------------------------------------------------------------------- + Hightly simplified wrapper around the SPDX format used to generate software bill of materials (SBOM) +""" +import datetime + + +class SpdxCreationInfo: + @property + def id(self): + return '_:b0' + + def get_data(self): + return { + '@id': self.id, + 'type': 'CreationInfo', + 'created': datetime.datetime.now(datetime.timezone.utc).isoformat(), + 'createdBy': [ + 'urn:opnsense:organization:opnsense' + ], + 'specVersion': '3.0.1' + } + diff --git a/contrib/spdx-sbom/lib/spdx/spdx_element.py b/contrib/spdx-sbom/lib/spdx/spdx_element.py new file mode 100644 index 0000000000..d47e1b515f --- /dev/null +++ b/contrib/spdx-sbom/lib/spdx/spdx_element.py @@ -0,0 +1,71 @@ +""" + Copyright (c) 2026 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +""" +import uuid + + +class SpdxElement: + _all_elements = {} # elements should be unique per spdxId + + @staticmethod + def spdx_type(): + return 'Element' + + @classmethod + def spdx_id_constructor(cls, id): + return 'urn:opnsense:%s:%s' % (cls.__name__.lstrip('Spdx').lower(), id) + + @classmethod + def _get_properties(cls): + return { + 'spdxId': '', + 'comment': '', + 'name': '', + 'description': '', + 'creationInfo': '_:b0' # XXX: should replace + } + + def __init__(self, **kwargs): + payload = {} + for k,v in self._get_properties().items(): + val = kwargs[k] if k in kwargs else v + payload[k] = v(val) if val and callable(v) else val + + if 'spdxId' not in kwargs: + # generate spdxId when not offered + payload['spdxId'] = self.spdx_id_constructor(kwargs.get('id', str(uuid.uuid4()))) + + payload['type'] = self.spdx_type() + self._spdxId = payload['spdxId'] + self._all_elements[self._spdxId] = payload + + def __getattr__(self, prop): + """ convenient wrapper to make Spdx attributes directly accessible + """ + return self._all_elements[self._spdxId][prop] + + def get_data(self): + return self._all_elements[self._spdxId] diff --git a/contrib/spdx-sbom/lib/spdx/spdx_license.py b/contrib/spdx-sbom/lib/spdx/spdx_license.py new file mode 100644 index 0000000000..c888991700 --- /dev/null +++ b/contrib/spdx-sbom/lib/spdx/spdx_license.py @@ -0,0 +1,44 @@ +""" + Copyright (c) 2026 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +""" +from .spdx_element import SpdxElement + + +class SpdxAnyLicenseInfo(SpdxElement): + pass + +class SpdxLicenseExpression(SpdxAnyLicenseInfo): + @staticmethod + def spdx_type(): + return 'simplelicensing_LicenseExpression' + + @classmethod + def _get_properties(cls): + result = super()._get_properties() + result.update({ + 'licenseExpression': '' + }) + return result \ No newline at end of file diff --git a/contrib/spdx-sbom/lib/spdx/spdx_package.py b/contrib/spdx-sbom/lib/spdx/spdx_package.py new file mode 100644 index 0000000000..f01277f44c --- /dev/null +++ b/contrib/spdx-sbom/lib/spdx/spdx_package.py @@ -0,0 +1,43 @@ +""" + Copyright (c) 2026 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +""" +from .spdx_element import SpdxElement + + +class SpdxPackage(SpdxElement): + @staticmethod + def spdx_type(): + return 'software_Package' + + @classmethod + def _get_properties(cls): + result = super()._get_properties() + result.update({ + 'homePage': '', + 'packageVersion': '', + 'primaryPurpose': '' + }) + return result diff --git a/contrib/spdx-sbom/lib/spdx/spdx_relationship.py b/contrib/spdx-sbom/lib/spdx/spdx_relationship.py new file mode 100644 index 0000000000..83a88c6296 --- /dev/null +++ b/contrib/spdx-sbom/lib/spdx/spdx_relationship.py @@ -0,0 +1,43 @@ +""" + Copyright (c) 2026 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +""" +from .spdx_element import SpdxElement + + +class SpdxRelationship(SpdxElement): + @staticmethod + def spdx_type(): + return 'Relationship' + + @classmethod + def _get_properties(cls): + result = super()._get_properties() + result.update({ + 'relationshipType': '', + 'from': '', + 'to': '' + }) + return result diff --git a/contrib/spdx-sbom/spdx-generator.py b/contrib/spdx-sbom/spdx-generator.py new file mode 100755 index 0000000000..673061da11 --- /dev/null +++ b/contrib/spdx-sbom/spdx-generator.py @@ -0,0 +1,71 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2026 Ad Schellevis + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. + +""" +import argparse +import json +from lib.pkg import Pkg +from lib.spdx import * + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('pkg', help='package name to inspect') + inputargs = parser.parse_args() + pkg = Pkg().query(inputargs.pkg) + doc = SpdxDocument( + SpdxSoftwareSbom(SpdxPackage(**pkg.get_data())) + ) + + for frm, to in pkg: + pkg = doc.add_object(SpdxPackage(**frm.get_data())) + lic = doc.add_object( + SpdxLicenseExpression(**{ + 'id': frm.licenseExpression.replace(' ', '_').lower(), + 'licenseExpression': frm.licenseExpression + }) + ) + doc.add_object( + SpdxRelationship(**{ + 'relationshipType': 'hasConcludedLicense', + 'id': '%s_lic' % frm.id, + 'from': pkg.spdxId, + 'to': [lic.spdxId] + }) + ) + + if to: + doc.add_object( + SpdxRelationship(**{ + 'relationshipType': 'dependsOn', + 'id': '%s-%s' % (frm.id, '_'.join([x.id for x in to])), + 'from': pkg.spdxId, + 'to': [SpdxPackage.spdx_id_constructor(x.id) for x in to] + }) + ) + + print(json.dumps(doc.dump(), indent=4))