contrib: add a tool to generate an SPDX formatted software bill of materials (SBOM) using information available in our package database.

This commit is contained in:
Ad Schellevis
2026-08-12 17:35:51 +02:00
parent 14de8b0235
commit 3758bd8db9
11 changed files with 597 additions and 0 deletions
+26
View File
@@ -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 <package-name>
```
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
+26
View File
@@ -0,0 +1,26 @@
"""
Copyright (c) 2026 Ad Schellevis <[email protected]>
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.
"""
+101
View File
@@ -0,0 +1,101 @@
"""
Copyright (c) 2026 Ad Schellevis <[email protected]>
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
+93
View File
@@ -0,0 +1,93 @@
"""
Copyright (c) 2026 Ad Schellevis <[email protected]>
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
}
+33
View File
@@ -0,0 +1,33 @@
"""
Copyright (c) 2026 Ad Schellevis <[email protected]>
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'
@@ -0,0 +1,46 @@
"""
Copyright (c) 2026 Ad Schellevis <[email protected]>
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'
}
@@ -0,0 +1,71 @@
"""
Copyright (c) 2026 Ad Schellevis <[email protected]>
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]
@@ -0,0 +1,44 @@
"""
Copyright (c) 2026 Ad Schellevis <[email protected]>
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
@@ -0,0 +1,43 @@
"""
Copyright (c) 2026 Ad Schellevis <[email protected]>
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
@@ -0,0 +1,43 @@
"""
Copyright (c) 2026 Ad Schellevis <[email protected]>
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
+71
View File
@@ -0,0 +1,71 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2026 Ad Schellevis <[email protected]>
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))