]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Attempt to generate SBOMs after building packages
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 25 Mar 2024 13:51:05 +0000 (14:51 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 26 Mar 2024 08:06:15 +0000 (09:06 +0100)
builder-support/dockerfiles/Dockerfile.rpmsbom [new file with mode: 0644]
builder-support/dockerfiles/Dockerfile.rpmtest [new file with mode: 0644]
builder-support/dockerfiles/Dockerfile.target.amazon-2
builder-support/dockerfiles/Dockerfile.target.amazon-2023
builder-support/dockerfiles/Dockerfile.target.centos-7
builder-support/dockerfiles/Dockerfile.target.centos-8
builder-support/dockerfiles/Dockerfile.target.centos-9-stream
builder-support/dockerfiles/Dockerfile.target.el-9
builder-support/dockerfiles/Dockerfile.target.oraclelinux-8
builder-support/helpers/generate-sbom-dnf.py [new file with mode: 0755]

diff --git a/builder-support/dockerfiles/Dockerfile.rpmsbom b/builder-support/dockerfiles/Dockerfile.rpmsbom
new file mode 100644 (file)
index 0000000..00e6ea5
--- /dev/null
@@ -0,0 +1,23 @@
+RUN touch /var/lib/rpm/* && \
+    yum upgrade -y && yum install -y python3
+ADD builder-support/helpers/ /pdns/builder-support/helpers/
+
+@IF [ -n "$M_recursor$M_all" ]
+RUN cd /pdns/builder-support/helpers/ && python3 generate-sbom-dnf.py /dist/pdns-recursor-${BUILDER_VERSION}-${BUILDER_TARGET}.cyclonedx.json pdns-recursor rust.json
+@ENDIF
+
+@IF [ -n "$M_dnsdist$M_all" ]
+RUN cd /pdns/builder-support/helpers/; \
+    if ! $(grep -q 'release 7' /etc/redhat-release); then \
+       python3 generate-sbom-dnf.py /dist/dnsdist-${BUILDER_VERSION}-${BUILDER_TARGET}.cyclonedx.json dnsdist rust.json quiche.json h2o.json ; \
+    else \
+       python3 generate-sbom-dnf.py /dist/dnsdist-${BUILDER_VERSION}-${BUILDER_TARGET}.cyclonedx.json dnsdist h2o.json ; \
+    fi
+@ENDIF
+
+@IF [ -n "$M_authoritative$M_all" ]
+RUN cd /pdns/builder-support/helpers/ && \
+    for pkg in pdns pdns-backend-geoip pdns-backend-ldap pdns-backend-lmdb pdns-backend-lua2 pdns-backend-mysql pdns-backend-odbc pdns-backend-postgresql pdns-backend-remote pdns-backend-sqlite pdns-backend-tinydns pdns-backend-ixfrdist pdns-tools; do \
+      python3 generate-sbom-dnf.py /dist/${pkg}-${BUILDER_VERSION}-${BUILDER_TARGET}.cyclonedx.json ${pkg}; \
+    done
+@ENDIF
diff --git a/builder-support/dockerfiles/Dockerfile.rpmtest b/builder-support/dockerfiles/Dockerfile.rpmtest
new file mode 100644 (file)
index 0000000..8758749
--- /dev/null
@@ -0,0 +1,14 @@
+# Install the built rpms and test them
+FROM dist-base as dist
+
+# If you want to install extra packages or do generic configuration,
+# do it before the COPY. Either here, or in the dist-base layer.
+
+COPY --from=package-builder /dist /dist
+
+# Install built packages with dependencies
+RUN yum localinstall -y /dist/*/*.rpm
+
+# Generate SBOM
+# We should probably guard this behind "${IS_RELEASE}" = "YES"
+@EXEC [ "$skipsbom" = "" ] && include Dockerfile.rpmsbom
index cac43d12fe913e74040d17247228672894767d96..01933467b28f0f2ff94a6f2a9905b224b404ab34 100644 (file)
@@ -11,5 +11,5 @@ RUN touch /var/lib/rpm/* && amazon-linux-extras install epel -y
 @INCLUDE Dockerfile.rpmbuild
 
 # Do a test install and verify
-# Can be skipped with skiptests=1 in the environment
-# @EXEC [ "$skiptests" = "" ] && include Dockerfile.rpmtest
+# Can be skipped with skippackagetest=1 in the environment
+@EXEC [ "$skippackagetest" = "" ] && include Dockerfile.rpmtest
index f47d6d712ca9bbbb0e80c5fc98189a47b1a525b8..0dfee6b86cd2c957425c22ce3bab78defd3a6768 100644 (file)
@@ -10,5 +10,5 @@ ARG BUILDER_CACHE_BUSTER=
 @INCLUDE Dockerfile.rpmbuild
 
 # Do a test install and verify
-# Can be skipped with skiptests=1 in the environment
-# @EXEC [ "$skiptests" = "" ] && include Dockerfile.rpmtest
+# Can be skipped with skippackagetest=1 in the environment
+@EXEC [ "$skippackagetest" = "" ] && include Dockerfile.rpmtest
index 9bd7c2a1243e76a4915b9c73b9ac0b83771c7e92..24a5dff8062e9905eb4aa10e77e46979212e455f 100644 (file)
@@ -18,5 +18,5 @@ RUN touch /var/lib/rpm/* && yum install -y --nogpgcheck devtoolset-11-gcc-c++
 @INCLUDE Dockerfile.rpmbuild
 
 # Do a test install and verify
-# Can be skipped with skiptests=1 in the environment
-# @EXEC [ "$skiptests" = "" ] && include Dockerfile.rpmtest
+# Can be skipped with skippackagetest=1 in the environment
+@EXEC [ "$skippackagetest" = "" ] && include Dockerfile.rpmtest
index ae0e4c9ab932a57a1483dd78453da34f80572ed0..237ff5ad096ad90c323b2def60547fa3a1701c6a 100644 (file)
@@ -30,5 +30,5 @@ RUN touch /var/lib/rpm/* && dnf install -y epel-release && \
 @INCLUDE Dockerfile.rpmbuild
 
 # Do a test install and verify
-# Can be skipped with skiptests=1 in the environment
-# @EXEC [ "$skiptests" = "" ] && include Dockerfile.rpmtest
+# Can be skipped with skippackagetest=1 in the environment
+@EXEC [ "$skippackagetest" = "" ] && include Dockerfile.rpmtest
index 873f63fde10882f0e74b0f48b73862f25e5de231..6b9cda656b8db43a664121fbb1377620609b53aa 100644 (file)
@@ -15,5 +15,5 @@ RUN touch /var/lib/rpm/* && dnf install -y epel-release && \
 @INCLUDE Dockerfile.rpmbuild
 
 # Do a test install and verify
-# Can be skipped with skiptests=1 in the environment
-# @EXEC [ "$skiptests" = "" ] && include Dockerfile.rpmtest
+# Can be skipped with skippackagetest=1 in the environment
+@EXEC [ "$skippackagetest" = "" ] && include Dockerfile.rpmtest
index c5766a84e4dd510e1661a0f95dfbe1a95ae6ab05..5e35f9193c5decce4f6c06e7ece15ae019e25ebe 100644 (file)
@@ -22,5 +22,5 @@ RUN touch /var/lib/rpm/* && dnf install -y https://dl.fedoraproject.org/pub/epel
 @INCLUDE Dockerfile.rpmbuild
 
 # Do a test install and verify
-# Can be skipped with skiptests=1 in the environment
-# @EXEC [ "$skiptests" = "" ] && include Dockerfile.rpmtest
+# Can be skipped with skippackagetest=1 in the environment
+@EXEC [ "$skippackagetest" = "" ] && include Dockerfile.rpmtest
index 63dac11e52e89bfe01f1826fed4494d4411220eb..923759aa8a879e1091f2eaeb7e0ea3ca8b97d7df 100644 (file)
@@ -22,5 +22,5 @@ RUN touch /var/lib/rpm/* && dnf install -y https://dl.fedoraproject.org/pub/epel
 @INCLUDE Dockerfile.rpmbuild
 
 # Do a test install and verify
-# Can be skipped with skiptests=1 in the environment
-# @EXEC [ "$skiptests" = "" ] && include Dockerfile.rpmtest
+# Can be skipped with skippackagetest=1 in the environment
+@EXEC [ "$skippackagetest" = "" ] && include Dockerfile.rpmtest
diff --git a/builder-support/helpers/generate-sbom-dnf.py b/builder-support/helpers/generate-sbom-dnf.py
new file mode 100755 (executable)
index 0000000..6ba91e5
--- /dev/null
@@ -0,0 +1,159 @@
+#!/usr/bin/env python3
+"""
+This script uses dnf to generate a Software Bill of Materials
+(SBOM) in CycloneDX Protocol Buffer format.
+"""
+import datetime
+import dnf
+import json
+import os
+import sys
+import uuid
+
+def licenseToSPDXIdentifier(licenseName):
+    licenseMap = {
+        'BSD': 'BSD-3-Clause',
+        'GPLv2': 'GPL-2.0-only',
+        'GPLv2+': 'GPL-2.0-or-later',
+        'LGPLv2+': 'LGPL-2.0-or-later',
+        'MIT': 'MIT',
+        'OpenLDAP': 'OLDAP-2.8',
+        }
+    if licenseName in licenseMap:
+        return licenseMap[licenseName]
+    return None
+
+def getPackageDatabase():
+    with dnf.Base() as db:
+        conf = db.conf
+        conf.installroot = '/'
+        conf.substitutions.update_from_etc('/')
+        db.read_all_repos()
+
+        db.fill_sack(load_system_repo='auto', load_available_repos=True)
+        query = db.sack.query()
+        return query.installed()
+
+def getPackageInformations(pkgDB, packageName):
+    matches = pkgDB.filter(name=packageName).run()
+    if len(matches) == 0:
+        print(f'-> Package {packageName} not found')
+        return None
+    return matches[0]
+
+def addDependencyToSBOM(sbom, appInfos, pkg):
+    bomRef = 'lib:' + pkg.name
+    component = { 'name': pkg.name, 'bom-ref': bomRef, 'type': 'library'}
+    if pkg.release:
+        component['version'] = (pkg.version if pkg.epoch == 0 else str(pkg.epoch) + ':' + pkg.version) + '-' + pkg.release
+    else:
+        component['version'] = (pkg.version if pkg.epoch == 0 else str(pkg.epoch) + ':' + pkg.version)
+    if hasattr(pkg, 'vendor') and pkg.vendor is not None:
+        component['supplier'] = {'name': pkg.vendor}
+    if hasattr(pkg, 'publisher') and pkg.publisher is not None:
+        component['publisher'] = pkg.publisher
+    spdxLicense = licenseToSPDXIdentifier(pkg.license)
+    if spdxLicense is None:
+        component['licenses'] = [{'license': {'name': pkg.license}}]
+    else:
+        component['licenses'] = [{'license': {'id': spdxLicense}}]
+    if hasattr(pkg, 'sha256') and pkg.sha256 is not None:
+        component['hashes'] = [{'alg': 'SHA-256', 'content': pkg.sha256}]
+
+    sbom['components'].append(component)
+
+def processDependencies(pkgDB, sbom, appInfos, depRelations):
+    seenDeps = {}
+    for require in appInfos.requires:
+        depName = require.name.split('(')[0]
+        if depName in ['/bin/sh', 'config', 'ld-linux-x86-64.so.2', 'rpmlib', 'rtld']:
+            continue
+        if depName in seenDeps:
+            continue
+        seenDeps[depName] = True
+
+        matches = pkgDB.filter(name=depName).run()
+        if len(matches) == 0:
+            flags = []
+            matches = pkgDB.filter(*flags, provides__glob=[require.name]).run()
+            if len(matches) == 0:
+                print(f'Unable to find a match for {depName}')
+                continue
+        if len(matches) > 1:
+            print(f'Got {len(matches)} matches for {depName}')
+
+        dep = matches[0]
+        depRef = 'lib:' + dep.name
+        if depRef in seenDeps:
+            continue
+        seenDeps[depRef] = True
+
+        addDependencyToSBOM(sbom, appInfos, dep)
+        depRelations['pkg:' + appInfos.name].append(depRef)
+
+class StaticLibDep(object):
+    pass
+
+def processAdditionalDependencies(sbom, appInfos, additionalDeps, depRelations):
+    for additionalDepFile in additionalDeps:
+        with open(additionalDepFile) as depDataFile:
+            depData = json.load(depDataFile)
+            pkg = StaticLibDep()
+            pkg.name = os.path.splitext(os.path.basename(additionalDepFile))[0]
+            pkg.version = depData['version']
+            pkg.epoch = 0
+            pkg.release = None
+            pkg.supplier = 'PowerDNS.COM BV'
+            if 'license' in depData:
+                pkg.license = depData['license']
+            if 'publisher' in depData:
+                pkg.publisher = depData['publisher']
+            if 'SHA256SUM' in depData:
+                pkg.sha256 = depData['SHA256SUM']
+            elif 'SHA256SUM_x86_64' in depData:
+                pkg.sha256 = depData['SHA256SUM_x86_64']
+
+            depRef = 'lib:' + pkg.name
+            addDependencyToSBOM(sbom, appInfos, pkg)
+            depRelations['pkg:' + appInfos.name].append(depRef)
+
+def generateSBOM(packageName, additionalDeps):
+    sbom = { 'bomFormat': 'CycloneDX', 'specVersion': '1.5', 'version': 1 }
+    sbom['serialNumber'] = 'urn:uuid:' + str(uuid.uuid4())
+    depRelations = {}
+
+    pkgDB = getPackageDatabase()
+    appName = packageName
+    appInfos = getPackageInformations(pkgDB, packageName)
+    component = { 'name': appName, 'bom-ref': 'pkg:' + appName, 'type': 'application'}
+    component['version'] = appInfos.version
+    component['supplier'] = {'name': appInfos.vendor if appInfos.vendor != '<NULL>' else 'PowerDNS.COM BV', 'url': ['https://www.powerdns.com']}
+    component['licenses'] = [{'license': {'id': licenseToSPDXIdentifier(appInfos.license)}}]
+    depRelations['pkg:' + appName] = []
+
+    sbom['metadata'] = { 'timestamp': datetime.datetime.now(tz=datetime.timezone.utc).isoformat(),
+                         'authors': [{'name': 'PowerDNS.COM BV'}],
+                         'component': component }
+    sbom['components'] = []
+    sbom['dependencies'] = []
+
+    processDependencies(pkgDB, sbom, appInfos, depRelations)
+    processAdditionalDependencies(sbom, appInfos, additionalDeps, depRelations)
+
+    for pkg, deps in depRelations.items():
+        sbom['dependencies'].append({'ref': pkg, 'dependsOn': deps})
+
+    return sbom
+
+if __name__ == "__main__":
+    if len(sys.argv) < 3:
+        sys.exit('Usage: %s <output file> <package> [static dependencies ...]' % (sys.argv[0]))
+
+    staticDeps = []
+    if len(sys.argv) > 3:
+        staticDeps = sys.argv[3:]
+
+    sbom = generateSBOM(sys.argv[2], staticDeps)
+
+    with open(sys.argv[1], "w") as f:
+        f.write(json.dumps(sbom))