-
Notifications
You must be signed in to change notification settings - Fork 188
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial CycloneDX JSON output support
- supports single or multiple images in output - includes basic component information (name, version, hashes, license) - includes license evidence - groups components by image - adds packageurl-python as a dependency Signed-off-by: Patrick Dwyer <patrick.dwyer@owasp.org>
- Loading branch information
1 parent
75bd6ac
commit 0a6a132
Showing
9 changed files
with
301 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,3 +16,4 @@ debian-inspector>=21.5 | |
regex>=2021.7 | ||
GitPython~=3.1 | ||
prettytable~=2.1 | ||
packageurl-python>=0.9.4 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright (c) 2019 VMware, Inc. All Rights Reserved. | ||
# SPDX-License-Identifier: BSD-2-Clause |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright (c) 2019-2020 VMware, Inc. All Rights Reserved. | ||
# SPDX-License-Identifier: BSD-2-Clause | ||
|
||
from tern.classes.template import Template | ||
|
||
|
||
class CycloneDX(Template): | ||
''' This is the CycloneDX Template class | ||
It provides mappings for the CycloneDX document format ''' | ||
|
||
def file_data(self): | ||
return {} | ||
|
||
def package(self): | ||
return {} | ||
|
||
def image_layer(self): | ||
return {} | ||
|
||
def image(self): | ||
return {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright (c) 2021 VMware, Inc. All Rights Reserved. | ||
# SPDX-License-Identifier: BSD-2-Clause | ||
|
||
""" | ||
Common functions that are useful for CycloneDX document creation | ||
""" | ||
|
||
import datetime | ||
import uuid | ||
from tern.utils import general | ||
|
||
|
||
################### | ||
# General Helpers # | ||
################### | ||
|
||
|
||
# document level tool information | ||
metadata_tool = { | ||
'vendor': 'Tern Tools', | ||
'name': 'Tern', | ||
'version': general.get_git_rev_or_version()[1] | ||
} | ||
|
||
|
||
# keys are what Tern uses, values what CycloneDX uses | ||
hash_type_mapping = { | ||
'md5': 'MD5', | ||
'sha1': 'SHA-1', | ||
'sha256': 'SHA-256', | ||
} | ||
|
||
|
||
pkg_format_purl_type_mapping = { | ||
'deb': 'deb', | ||
'rpm': 'rpm', | ||
'apk': 'apk', | ||
'pip': 'pip', | ||
'gem': 'gem', | ||
'npm': 'npm', | ||
'go.mod': 'go', | ||
} | ||
|
||
|
||
purl_types_with_namespaces = [ | ||
'deb', | ||
'rpm', | ||
'apk', | ||
] | ||
|
||
|
||
# map Tern OS guesses to package URL namespace | ||
os_guess_purl_namespace_mapping = { | ||
'debian': 'debian', | ||
'ubuntu': 'ubuntu', | ||
'alpine linux': 'alpine', | ||
'centos': 'centos', | ||
'fedora': 'fedora', | ||
'opensuse': 'opensuse', | ||
'rhel': 'rhel', | ||
# Arch Linux | ||
# Photon | ||
} | ||
|
||
|
||
def get_serial_number(): | ||
''' Return a randomly generated CycloneDX BOM serial number ''' | ||
return 'urn:uuid:' + str(uuid.uuid4()) | ||
|
||
|
||
def get_timestamp(): | ||
''' Return a timestamp suitable for the BOM timestamp ''' | ||
return datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') | ||
|
||
|
||
def get_hash(checksum_type, checksum): | ||
''' Return a CycloneDX hash object from Tern checksum values ''' | ||
hash_algorithm = hash_type_mapping.get(checksum_type, None) | ||
return None if hash_algorithm is None else {'alg': hash_algorithm, 'content': checksum} | ||
|
||
|
||
def get_property(name, value): | ||
''' Return a CycloneDX property object ''' | ||
return {'name': name, 'value': value} | ||
|
||
|
||
def get_purl_type(pkg_format): | ||
return pkg_format_purl_type_mapping.get(pkg_format.lower()) | ||
|
||
|
||
def get_purl_namespace(os_guess, pkg_format): | ||
if pkg_format in purl_types_with_namespaces: | ||
for os in os_guess_purl_namespace_mapping: | ||
if os_guess.lower().startswith(os): | ||
return os_guess_purl_namespace_mapping.get(os) | ||
return None | ||
|
||
|
||
def get_os_guess(image_obj): | ||
for layer in image_obj.layers: | ||
if layer.os_guess: | ||
return layer.os_guess | ||
return None | ||
|
||
|
||
def get_license_from_name(name): | ||
return {'license': {'name': name}} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright (c) 2021 VMware, Inc. All Rights Reserved. | ||
# SPDX-License-Identifier: BSD-2-Clause |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright (c) 2021 VMware, Inc. All Rights Reserved. | ||
# SPDX-License-Identifier: BSD-2-Clause | ||
|
||
''' | ||
CycloneDX JSON document generator | ||
''' | ||
|
||
import json | ||
import logging | ||
|
||
from tern.utils import constants | ||
from tern.formats import generator | ||
from tern.formats.cyclonedx.cyclonedx import CycloneDX | ||
from tern.formats.cyclonedx import cyclonedx_common | ||
from tern.formats.cyclonedx.cyclonedxjson import image_helpers as mhelpers | ||
from tern.formats.cyclonedx.cyclonedxjson import package_helpers as phelpers | ||
|
||
|
||
# global logger | ||
logger = logging.getLogger(constants.logger_name) | ||
|
||
|
||
def get_document_dict(image_obj_list, template): # pylint: disable=[unused-argument] | ||
''' Return document info as a dictionary ''' | ||
docu_dict = { | ||
'bomFormat': 'CycloneDX', | ||
'specVersion': '1.3', | ||
'serialNumber': cyclonedx_common.get_serial_number(), | ||
'version': 1, | ||
'metadata': { | ||
'timestamp': cyclonedx_common.get_timestamp(), | ||
'tools': [cyclonedx_common.metadata_tool], | ||
}, | ||
'components': [], | ||
} | ||
|
||
# if representing a single image populate top level BOM metadata component | ||
# else representing multiple images so list them as components | ||
if len(image_obj_list) == 1: | ||
docu_dict['metadata']['component'] = mhelpers.get_image_dict(image_obj_list[0]) | ||
docu_dict['components'] = phelpers.get_packages_list(image_obj_list[0]) | ||
else: | ||
for image_obj in image_obj_list: | ||
image_componet = mhelpers.get_image_dict(image_obj) | ||
image_componet['components'] = phelpers.get_packages_list(image_obj) | ||
docu_dict['components'].append(image_componet) | ||
|
||
return docu_dict | ||
|
||
|
||
class CycloneDXJSON(generator.Generate): | ||
def generate(self, image_obj_list, print_inclusive=False): | ||
''' Generate a CycloneDX document | ||
The whole document should be stored in a dictionary which can be | ||
converted to JSON and dumped to a file using the write_report function | ||
in report.py. ''' | ||
logger.debug('Generating CycloneDX JSON document...') | ||
|
||
template = CycloneDX() | ||
report = get_document_dict(image_obj_list, template) | ||
|
||
return json.dumps(report, indent=2) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright (c) 2021 VMware, Inc. All Rights Reserved. | ||
# SPDX-License-Identifier: BSD-2-Clause | ||
|
||
''' | ||
Helper functions for image level JSON CycloneDX document dictionaries | ||
''' | ||
|
||
|
||
from tern.formats.cyclonedx import cyclonedx_common | ||
from packageurl import PackageURL | ||
|
||
|
||
def get_image_dict(image_obj): | ||
''' Given an image object return the CycloneDX document dictionary for the | ||
given image. For CycloneDX, the image is a component and hence follows the | ||
JSON spec for components. ''' | ||
image_dict = { | ||
'type': 'container', | ||
'scope': 'required', | ||
'name': image_obj.name, | ||
'version': image_obj.checksum_type + ':' + image_obj.checksum, | ||
'hashes': [], | ||
'properties': [] | ||
} | ||
|
||
purl = PackageURL('docker', None, image_dict['name'], image_dict['version']) | ||
image_dict['purl'] = str(purl) | ||
|
||
if image_obj.repotags: | ||
for repotag in image_obj.repotags: | ||
image_dict['properties'].append(cyclonedx_common.get_property('tern:repotag', repotag)) | ||
|
||
os_guess = cyclonedx_common.get_os_guess(image_obj) | ||
if os_guess: | ||
image_dict['properties'].append(cyclonedx_common.get_property('tern:os_guess', os_guess)) | ||
|
||
cdx_hash = cyclonedx_common.get_hash(image_obj.checksum_type, image_obj.checksum) | ||
if cdx_hash is not None: | ||
image_dict['hashes'].append(cdx_hash) | ||
|
||
return image_dict |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
# -*- coding: utf-8 -*- | ||
# | ||
# Copyright (c) 2021 VMware, Inc. All Rights Reserved. | ||
# SPDX-License-Identifier: BSD-2-Clause | ||
|
||
''' | ||
Helper functions for packages in CycloneDX JSON document creation | ||
''' | ||
|
||
from tern.formats.cyclonedx import cyclonedx_common | ||
from packageurl import PackageURL | ||
|
||
|
||
def get_package_dict(os_guess, package): | ||
''' Given a package format, namespace and package object return a | ||
CycloneDX JSON dictionary representation of the package. ''' | ||
package_dict = { | ||
'name': package.name, | ||
'version': package.version, | ||
'type': 'application', | ||
} | ||
|
||
purl_type = cyclonedx_common.get_purl_type(package.pkg_format) | ||
purl_namespace = cyclonedx_common.get_purl_namespace(os_guess, package.pkg_format) | ||
if purl_type: | ||
purl = PackageURL(purl_type, purl_namespace, package.name, package.version) | ||
package_dict['purl'] = str(purl) | ||
|
||
if package.pkg_license: | ||
package_dict['licenses'] = [cyclonedx_common.get_license_from_name(package.pkg_license)] | ||
|
||
if package.pkg_licenses: | ||
package_dict['evidence'] = {'licenses': []} | ||
for pkg_license in package.pkg_licenses: | ||
package_dict['evidence']['licenses'].append(cyclonedx_common.get_license_from_name(pkg_license)) | ||
|
||
return package_dict | ||
|
||
|
||
def get_packages_list(image_obj): | ||
''' Given an image object return a list of CycloneDX dictionary | ||
representations for each of the packages in the image ''' | ||
package_dicts = [] | ||
|
||
os_guess = cyclonedx_common.get_os_guess(image_obj) | ||
|
||
for layer_obj in image_obj.layers: | ||
for package in layer_obj.packages: | ||
package_dicts.append(get_package_dict(os_guess, package)) | ||
|
||
return package_dicts |