Skip to content

Commit ae4860c

Browse files
authored
Add support for CycloneDX 1.4 to the "inspect-manifest" pipeline #583
- CycloneDx `component` can have a `list of components`, those are dumped to extra_data as `nestedComponents`. Furthermore, these lists of components are recursively parsed and treated as normal package. - The Component may have multiple URLs in externalReferences. The first URL of the reference is added to the applicable package_data URL, while the rest are dumped in extra_data as externalReferences. Signed-off-by: Keshav Priyadarshi <git@keshav.space>
1 parent e309536 commit ae4860c

9 files changed

Lines changed: 3122 additions & 0 deletions

File tree

scanpipe/cyclonedx/__init__.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
#
3+
# http://nexb.com and https://github.com/nexB/scancode.io
4+
# The ScanCode.io software is licensed under the Apache License version 2.0.
5+
# Data generated with ScanCode.io is provided as-is without warranties.
6+
# ScanCode is a trademark of nexB Inc.
7+
#
8+
# You may not use this software except in compliance with the License.
9+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
10+
# Unless required by applicable law or agreed to in writing, software distributed
11+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
12+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
13+
# specific language governing permissions and limitations under the License.
14+
#
15+
# Data Generated with ScanCode.io is provided on an "AS IS" BASIS, WITHOUT WARRANTIES
16+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
17+
# ScanCode.io should be considered or used as legal advice. Consult an Attorney
18+
# for any legal advice.
19+
#
20+
# ScanCode.io is a free software code scanning tool from nexB Inc. and others.
21+
# Visit https://github.com/nexB/scancode.io for support and download.
22+
23+
import json
24+
import pathlib
25+
from collections import defaultdict
26+
27+
import jsonschema
28+
from hoppr_cyclonedx_models.cyclonedx_1_4 import Component
29+
from hoppr_cyclonedx_models.cyclonedx_1_4 import (
30+
CyclonedxSoftwareBillOfMaterialsStandard as Bom_1_4,
31+
)
32+
33+
CYCLONEDX_SPEC_VERSION = "1.4"
34+
CYCLONEDX_JSON_SCHEMA_LOCATION = "bom-1.4.schema.json"
35+
CYCLONEDX_JSON_SCHEMA_PATH = (
36+
pathlib.Path(__file__).parent / CYCLONEDX_JSON_SCHEMA_LOCATION
37+
)
38+
CYCLONEDX_JSON_SCHEMA_URL = (
39+
"https://raw.githubusercontent.com/"
40+
"CycloneDX/specification/master/schema/bom-1.4.schema.json"
41+
)
42+
43+
44+
def get_bom(cyclonedx_document):
45+
"""
46+
Return CycloneDX BOM object.
47+
"""
48+
return Bom_1_4(**cyclonedx_document)
49+
50+
51+
def get_components(bom):
52+
"""
53+
Return list of components from CycloneDX BOM.
54+
"""
55+
return recursive_component_collector(bom.components, [])
56+
57+
58+
def bom_attributes_to_dict(cyclonedx_attributes):
59+
"""
60+
Return list of dict from a list of CycloneDX attributes.
61+
"""
62+
if not cyclonedx_attributes:
63+
return []
64+
65+
return [
66+
json.loads(attribute.json(exclude_unset=True, by_alias=True))
67+
for attribute in cyclonedx_attributes
68+
]
69+
70+
71+
def recursive_component_collector(root_component_list, collected):
72+
"""
73+
Return list of components including the nested components.
74+
"""
75+
if not root_component_list:
76+
return
77+
78+
for component in root_component_list:
79+
extra_data = {}
80+
if component.components is not None:
81+
extra_data = bom_attributes_to_dict(component.components)
82+
83+
collected.append({"cdx_package": component, "nested_components": extra_data})
84+
recursive_component_collector(component.components, collected)
85+
return collected
86+
87+
88+
def resolve_license(license):
89+
"""
90+
Return license expression/id/name from license item.
91+
"""
92+
if "expression" in license:
93+
return license["expression"]
94+
elif "id" in license["license"]:
95+
return license["license"]["id"]
96+
else:
97+
return license["license"]["name"]
98+
99+
100+
def get_declared_licenses(licenses):
101+
"""
102+
Return resolved license from list of LicenseChoice.
103+
"""
104+
if not licenses:
105+
return ""
106+
107+
resolved_licenses = [
108+
resolve_license(license) for license in bom_attributes_to_dict(licenses)
109+
]
110+
return "\n".join(resolved_licenses)
111+
112+
113+
def get_checksums(component):
114+
"""
115+
Return dict of all the checksums from a component.
116+
"""
117+
if not component.hashes:
118+
return {}
119+
120+
algorithm_map_cdx_scio = {
121+
"MD5": "md5",
122+
"SHA-1": "sha1",
123+
"SHA-256": "sha256",
124+
"SHA-512": "sha512",
125+
}
126+
return {
127+
algorithm_map_cdx_scio[algo_hash.alg.value]: algo_hash.content.__root__
128+
for algo_hash in component.hashes
129+
if algo_hash.alg.value in algorithm_map_cdx_scio
130+
}
131+
132+
133+
def get_external_references(external_references):
134+
"""
135+
Return dict of reference urls from list of `externalReferences`.
136+
"""
137+
if not external_references:
138+
return {}
139+
140+
refrences = defaultdict(lambda: [])
141+
142+
for ref in external_references:
143+
refrences[ref.type.value].append(ref.url)
144+
145+
return dict(refrences)
146+
147+
148+
def validate_document(document, schema=CYCLONEDX_JSON_SCHEMA_PATH):
149+
"""
150+
CycloneDX document validation.
151+
"""
152+
if isinstance(document, str):
153+
document = json.loads(document)
154+
155+
if isinstance(schema, pathlib.Path):
156+
schema = schema.read_text()
157+
if isinstance(schema, str):
158+
schema = json.loads(schema)
159+
160+
resolver = jsonschema.RefResolver(
161+
base_uri="file://" + str(pathlib.Path(__file__).parent), referrer=schema
162+
)
163+
164+
validator = jsonschema.Draft7Validator(schema=schema, resolver=resolver)
165+
166+
validator.validate(instance=document)

0 commit comments

Comments
 (0)