Skip to content

Commit

Permalink
Add support for yocto CVE backports
Browse files Browse the repository at this point in the history
Take CVE backport patches from the Yocto recipes into account by
uploading a VEX file resolving these.

Add support for CVE_CHECK_IGNORE variable

Take CVEs marked as ignored into account.

TODO: In newer versions of yocto, CVE_CHECK_IGNORE is deprecated in
favour of CVE_STATUS

Signed-off-by: Jasper Orschulko <jasper@fancydomain.eu>
  • Loading branch information
Jasper-Ben committed Jan 18, 2024
1 parent 70b34dc commit 3d98e63
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 33 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# meta-dependencytrack

`meta-dependencytrack` is a [Yocto](https://www.yoctoproject.org/) meta-layer which produces a [CycloneDX](https://cyclonedx.org/) Software Bill of Materials (aka [SBOM](https://www.ntia.gov/SBOM)) from your root filesystem and then uploads it to a [Dependency-Track](https://dependencytrack.org/) server against the project of your choice.
`meta-dependencytrack` is a [Yocto](https://www.yoctoproject.org/) meta-layer which produces a [CycloneDX](https://cyclonedx.org/) Software Bill of Materials (aka [SBOM](https://www.ntia.gov/SBOM)) from your root filesystem, as well as a Vulnerability Exploitability eXchange document (aka VEX) containing patched CVEs from component recipes and then uploads them to a [Dependency-Track](https://dependencytrack.org/) server against the project of your choice.

## Installation

Expand Down Expand Up @@ -42,6 +42,12 @@ INHERIT += "dependency-track"

![API Key](docs/api-key.png)

### Add required API permissions

For uploading the VEX document containing the Yocto patches, the additional `VULNERABILITY_ANALYSIS` permission is required.

![API_PERMISSIONS](docs/api-permissions.png)

## Building and Uploading

Once everything is configured simply build your image as you normally would. The final CycloneDX SBOM is saved as `tmp/deploy/dependency-track/bom.json` and, after buiding is complete, you should be able to simply refresh the project in Dependency Track to see the results of the scan.
Once everything is configured simply build your image as you normally would. The final CycloneDX SBOM and VEX are saved as `tmp/deploy/dependency-track/bom.json` and `tmp/deploy/dependency-track/bom.json` respectively and, after building is complete, you should be able to simply refresh the project in Dependency Track to see the results of the scan.
157 changes: 126 additions & 31 deletions classes/dependency-track.bbclass
Original file line number Diff line number Diff line change
Expand Up @@ -8,59 +8,115 @@ CVE_VERSION ??= "${PV}"

DEPENDENCYTRACK_DIR ??= "${DEPLOY_DIR}/dependency-track"
DEPENDENCYTRACK_SBOM ??= "${DEPENDENCYTRACK_DIR}/bom.json"
DEPENDENCYTRACK_VEX ??= "${DEPENDENCYTRACK_DIR}/vex.json"
DEPENDENCYTRACK_TMP ??= "${TMPDIR}/dependency-track"
DEPENDENCYTRACK_LOCK ??= "${DEPENDENCYTRACK_TMP}/bom.lock"

DEPENDENCYTRACK_PROJECT ??= ""
DEPENDENCYTRACK_API_URL ??= "http://localhost:8081/api"
DEPENDENCYTRACK_API_KEY ??= ""
DEPENDENCYTRACK_SBOM_PROCESSING_TIMEOUT ??= "1200"

python do_dependencytrack_init() {
import uuid
from datetime import datetime

sbom_dir = d.getVar("DEPENDENCYTRACK_DIR")
bb.debug(2, "Creating cyclonedx directory: %s" % sbom_dir)
bb.utils.mkdirhier(sbom_dir)

timestamp = datetime.now().astimezone().isoformat()
bom_serial_number = str(uuid.uuid4())
dependencytrack_dir = d.getVar("DEPENDENCYTRACK_DIR")
bb.debug(2, "Creating dependencytrack directory: %s" % dependencytrack_dir)
bb.utils.mkdirhier(dependencytrack_dir)
bb.debug(2, "Creating empty sbom")
write_sbom(d, {
write_json(d.getVar("DEPENDENCYTRACK_SBOM"), {
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:" + str(uuid.uuid4()),
"serialNumber": f"urn:uuid:{bom_serial_number}",
"version": 1,
"metadata": {
"timestamp": datetime.now().isoformat(),
"timestamp": timestamp
},
"components": []
})

bb.debug(2, "Creating empty patched CVEs VEX file")
write_json(d.getVar("DEPENDENCYTRACK_VEX"), {
"bomFormat": "CycloneDX",
"specVersion": "1.4",
"serialNumber": "urn:uuid:" + str(uuid.uuid4()),
"version": 1,
"metadata": {
"timestamp": timestamp
},
"vulnerabilities": []
})
}

addhandler do_dependencytrack_init
do_dependencytrack_init[eventmask] = "bb.event.BuildStarted"

python do_dependencytrack_collect() {
import json
import uuid
import oe.cve_check
from pathlib import Path

# load the bom
name = d.getVar("CVE_PRODUCT")
version = d.getVar("CVE_VERSION")
sbom = read_sbom(d)
sbom = read_json(d.getVar("DEPENDENCYTRACK_SBOM"))
vex = read_json(d.getVar("DEPENDENCYTRACK_VEX"))

# update it with the new package info
names = name.split()
for index, cpe in enumerate(oe.cve_check.get_cpe_ids(name, version)):
bb.debug(2, f"Collecting pagkage {name}@{version} ({cpe})")
if not next((c for c in sbom["components"] if c["cpe"] == cpe), None):
bom_ref = str(uuid.uuid4())

sbom["components"].append({
"name": names[index],
"version": version,
"cpe": cpe
"cpe": cpe,
"bom-ref": bom_ref
})

# populate vex file with patched CVEs
for _, patched_cve in enumerate(oe.cve_check.get_patched_cves(d)):
bb.debug(2, f"Found patch for CVE {patched_cve} in {name}@{version}")
vex["vulnerabilities"].append({
"id": patched_cve,
# vex documents require a valid source, see https://github.com/DependencyTrack/dependency-track/issues/2977
# this should always be NVD for yocto CVEs.
"source": {"name": "NVD", "url": "https://nvd.nist.gov/"},
"analysis": {"state": "resolved"},
# ref needs to be in bom-link format, however the uuid does not actually have to match the SBOM document uuid,
# see https://github.com/DependencyTrack/dependency-track/issues/1872#issuecomment-1254265425
# This is not ideal, as "resolved" will be applied to all components within the project containing the CVE,
# however component specific resolving seems not to work at the moment.
"affects": [{"ref": f"urn:cdx:{str(uuid.uuid4())}/1#{bom_ref}"}]
})
# populate vex file with ignored CVEs defined in CVE_CHECK_IGNORE
# TODO: In newer versions of Yocto CVE_CHECK_IGNORE is deprecated in favour of CVE_STATUS, which we should also take into account here
cve_check_ignore = d.getVar("CVE_CHECK_IGNORE")
if cve_check_ignore is not None:
for ignored_cve in cve_check_ignore.split():
bb.debug(2, f"Found ignore statement for CVE {ignored_cve} in {name}@{version}")
vex["vulnerabilities"].append({
"id": ignored_cve,
# vex documents require a valid source, see https://github.com/DependencyTrack/dependency-track/issues/2977
# this should always be NVD for yocto CVEs.
"source": {"name": "NVD", "url": "https://nvd.nist.gov/"},
# setting not-affected state for ignored CVEs
"analysis": {"state": "not_affected"},
# ref needs to be in bom-link format, however the uuid does not actually have to match the SBOM document uuid,
# see https://github.com/DependencyTrack/dependency-track/issues/1872#issuecomment-1254265425
# This is not ideal, as "resolved" will be applied to all components within the project containing the CVE,
# however component specific resolving seems not to work at the moment.
"affects": [{"ref": f"urn:cdx:{str(uuid.uuid4())}/1#{bom_ref}"}]
})
# write it back to the deploy directory
write_sbom(d, sbom)
write_json(d.getVar("DEPENDENCYTRACK_SBOM"), sbom)
write_json(d.getVar("DEPENDENCYTRACK_VEX"), vex)
}

addtask dependencytrack_collect before do_build after do_fetch
Expand All @@ -72,11 +128,19 @@ python do_dependencytrack_upload () {
import json
import base64
import urllib
import time
from pathlib import Path

sbom_path = d.getVar("DEPENDENCYTRACK_SBOM")
vex_path = d.getVar("DEPENDENCYTRACK_VEX")
dt_project = d.getVar("DEPENDENCYTRACK_PROJECT")
dt_url = f"{d.getVar('DEPENDENCYTRACK_API_URL')}/v1/bom"
dt_sbom_url = f"{d.getVar('DEPENDENCYTRACK_API_URL')}/v1/bom"
dt_vex_url = f"{d.getVar('DEPENDENCYTRACK_API_URL')}/v1/vex"

headers = {
"Content-Type": "application/json",
"X-API-Key": d.getVar("DEPENDENCYTRACK_API_KEY")
}

bb.debug(2, f"Loading final SBOM: {sbom_path}")
sbom = Path(sbom_path).read_text()
Expand All @@ -85,38 +149,69 @@ python do_dependencytrack_upload () {
"project": dt_project,
"bom": base64.b64encode(sbom.encode()).decode('ascii')
}).encode()
bb.debug(2, f"Uploading SBOM to project {dt_project} at {dt_url}")

headers = {
"Content-Type": "application/json",
"X-API-Key": d.getVar("DEPENDENCYTRACK_API_KEY")
}
bb.debug(2, f"Uploading SBOM to project {dt_project} at {dt_sbom_url}")

req = urllib.request.Request(
dt_url,
dt_sbom_url,
data=payload,
headers=headers,
method="PUT")

try:
urllib.request.urlopen(req)
res = urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
bb.error(f"Failed to upload SBOM to Dependency Track server at {dt_url}. [HTTP Error] {e.code}; Reason: {e.reason}")
except urllib.error.URLError as e:
bb.error(f"Failed to upload SBOM to Dependency Track server at {dt_url}. [URL Error] Reason: {e.reason}")
else:
bb.debug(2, f"SBOM successfully uploaded to {dt_url}")
bb.error(f"Failed to upload SBOM for project {dt_project} to Dependency Track server at {dt_sbom_url}. [HTTP Error] {e.code}; Reason: {e.reason}")
token = json.load(res)['token']
bb.debug(2, "Waiting for SBOM to be processed")

req = urllib.request.Request(
f"{dt_sbom_url}/token/{token}",
headers={ "X-API-Key": d.getVar("DEPENDENCYTRACK_API_KEY") },
method="GET")

timeout = 0
while True:
try:
res = urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
bb.error(f"Failed to check for SBOM processing status. [HTTP Error] {e.code}; Reason: {e.reason}")
if json.load(res)['processing'] is False:
break
elif timeout > int(d.getVar("DEPENDENCYTRACK_SBOM_PROCESSING_TIMEOUT")):
raise Exception('Timeout reached while processing SBOM')
timeout += 5
time.sleep(5)

bb.debug(2, f"Loading final patched CVEs VEX: {vex_path}")
vex = Path(vex_path).read_text()

payload = json.dumps({
"project": dt_project,
"vex": base64.b64encode(vex.encode()).decode('ascii')
}).encode()

bb.debug(2, f"Uploading patched CVEs VEX to project {dt_project} at {dt_vex_url}")
req = urllib.request.Request(
dt_vex_url,
data=payload,
headers=headers,
method="PUT")

try:
urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
bb.error(f"Failed to upload VEX for project {dt_project} to Dependency Track server at {dt_vex_url}. [HTTP Error] {e.code}; Reason: {e.reason}")
}

addhandler do_dependencytrack_upload
do_dependencytrack_upload[eventmask] = "bb.event.BuildCompleted"

def read_sbom(d):
def read_json(path):
import json
from pathlib import Path
return json.loads(Path(d.getVar("DEPENDENCYTRACK_SBOM")).read_text())
return json.loads(Path(path).read_text())

def write_sbom(d, sbom):
def write_json(path, content):
import json
from pathlib import Path
Path(d.getVar("DEPENDENCYTRACK_SBOM")).write_text(
json.dumps(sbom, indent=2)
)
Path(path).write_text(json.dumps(content, indent=2))
Binary file added docs/api-permissions.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 3d98e63

Please sign in to comment.