Skip to content

Commit e987f35

Browse files
committed
Skeleton support for 'author' + v1.1 and v1.0 for JSON added (along with tests).
1 parent 0d2c355 commit e987f35

File tree

5 files changed

+121
-24
lines changed

5 files changed

+121
-24
lines changed

cyclonedx/model/cyclonedx.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
class ComponentType(Enum):
77
"""
8-
Enum object that defines the permissable 'types' for a Component according to the CycloneDX
8+
Enum object that defines the permissible 'types' for a Component according to the CycloneDX
99
schemas.
1010
"""
1111
APPLICATION = 'application'
@@ -27,12 +27,18 @@ class Component:
2727
_version: str
2828
_qualifiers: str
2929

30-
def __init__(self, name: str, version: str, qualifiers: str = None, type: ComponentType = ComponentType.LIBRARY):
30+
_author: str = None
31+
32+
def __init__(self, name: str, version: str, qualifiers: str = None,
33+
component_type: ComponentType = ComponentType.LIBRARY):
3134
self._name = name
3235
self._version = version
33-
self._type = type
36+
self._type = component_type
3437
self._qualifiers = qualifiers
3538

39+
def get_author(self) -> str:
40+
return self._author
41+
3642
def get_name(self) -> str:
3743
return self._name
3844

@@ -48,6 +54,9 @@ def get_type(self) -> ComponentType:
4854
def get_version(self) -> str:
4955
return self._version
5056

57+
def set_author(self, author: str):
58+
self._author = author
59+
5160
def __eq__(self, other):
5261
return other.get_purl() == self.get_purl()
5362

cyclonedx/output/json.py

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,40 @@ def output_to_file(self, filename: str):
1414
pass
1515

1616
def _get_json(self) -> dict:
17-
components = list(map(Json._get_component_as_dict, self.get_bom().get_components()))
17+
components = list(map(self._get_component_as_dict, self.get_bom().get_components()))
1818

19-
return {
19+
response = {
2020
"bomFormat": "CycloneDX",
2121
"specVersion": str(self._get_schema_version()),
2222
"serialNumber": self.get_bom().get_urn_uuid(),
2323
"version": 1,
24-
"metadata": self._get_metadata_as_dict(),
2524
"components": components
2625
}
2726

28-
@staticmethod
29-
def _get_component_as_dict(component: Component) -> dict:
30-
return {
27+
if self._bom_supports_metadata():
28+
response['metadata'] = self._get_metadata_as_dict()
29+
30+
return response
31+
32+
def _get_component_as_dict(self, component: Component) -> dict:
33+
c = {
3134
"type": component.get_type().value,
3235
"name": component.get_name(),
3336
"version": component.get_version(),
3437
"purl": component.get_purl()
3538
}
3639

40+
if self._component_supports_author() and component.get_author() is not None:
41+
c['author'] = component.get_author()
42+
43+
return c
44+
45+
def _bom_supports_metadata(self) -> bool:
46+
return True
47+
48+
def _component_supports_author(self) -> bool:
49+
return True
50+
3751
def _get_metadata_as_dict(self) -> dict:
3852
metadata = self.get_bom().get_metadata()
3953
return {
@@ -45,6 +59,30 @@ def _get_schema_version(self) -> str:
4559
pass
4660

4761

62+
class JsonV1Dot0(Json):
63+
64+
def _get_schema_version(self) -> str:
65+
return '1.0'
66+
67+
def _bom_supports_metadata(self) -> bool:
68+
return False
69+
70+
def _component_supports_author(self) -> bool:
71+
return False
72+
73+
74+
class JsonV1Dot1(Json):
75+
76+
def _get_schema_version(self) -> str:
77+
return '1.1'
78+
79+
def _bom_supports_metadata(self) -> bool:
80+
return False
81+
82+
def _component_supports_author(self) -> bool:
83+
return False
84+
85+
4886
class JsonV1Dot2(Json):
4987

5088
def _get_schema_version(self) -> str:

cyclonedx/output/xml.py

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,19 @@ def _get_component_as_xml_element(self, component: Component) -> ElementTree.Ele
6262
if self._component_supports_bom_ref_attribute():
6363
element_attributes['bom-ref'] = component.get_purl()
6464

65-
element = ElementTree.Element('component', element_attributes)
65+
component_element = ElementTree.Element('component', element_attributes)
66+
67+
if self._component_supports_author() and component.get_author() is not None:
68+
ElementTree.SubElement(component_element, 'author').text = component.get_author()
6669

6770
# if publisher and publisher != "UNKNOWN":
6871
# ElementTree.SubElement(component, "publisher").text = re.sub(RE_XML_ILLEGAL, "?", publisher)
6972

7073
# if name and name != "UNKNOWN":
71-
ElementTree.SubElement(element, 'name').text = component.get_name()
74+
ElementTree.SubElement(component_element, 'name').text = component.get_name()
7275

7376
# if version and version != "UNKNOWN":
74-
ElementTree.SubElement(element, 'version').text = component.get_version()
77+
ElementTree.SubElement(component_element, 'version').text = component.get_version()
7578

7679
# if description and description != "UNKNOWN":
7780
# ElementTree.SubElement(component, "description").text = re.sub(RE_XML_ILLEGAL, "?", description)
@@ -90,11 +93,11 @@ def _get_component_as_xml_element(self, component: Component) -> ElementTree.Ele
9093
# component_license.license.name)
9194

9295
# if purl:
93-
ElementTree.SubElement(element, 'purl').text = component.get_purl()
96+
ElementTree.SubElement(component_element, 'purl').text = component.get_purl()
9497

9598
# ElementTree.SubElement(component, "modified").text = modified if modified else "false"
9699

97-
return element
100+
return component_element
98101

99102
def _add_metadata(self, bom: ElementTree.Element) -> ElementTree.Element:
100103
metadata_e = ElementTree.SubElement(bom, 'metadata')
@@ -104,6 +107,9 @@ def _add_metadata(self, bom: ElementTree.Element) -> ElementTree.Element:
104107
def _bom_supports_metadata(self) -> bool:
105108
return True
106109

110+
def _component_supports_author(self) -> bool:
111+
return True
112+
107113
def _component_supports_bom_ref_attribute(self) -> bool:
108114
return True
109115

@@ -126,6 +132,9 @@ def _bom_supports_metadata(self) -> bool:
126132
def _component_supports_bom_ref_attribute(self) -> bool:
127133
return False
128134

135+
def _component_supports_author(self) -> bool:
136+
return False
137+
129138

130139
class XmlV1Dot1(Xml):
131140

@@ -135,6 +144,9 @@ def _get_schema_version(self) -> str:
135144
def _bom_supports_metadata(self) -> bool:
136145
return False
137146

147+
def _component_supports_author(self) -> bool:
148+
return False
149+
138150

139151
class XmlV1Dot2(Xml):
140152

cyclonedx/parser/environment.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,14 @@ class EnvironmentParser(BaseParser):
1212

1313
def __init__(self):
1414
import pkg_resources
15+
from importlib.metadata import metadata
16+
17+
i: pkg_resources.DistInfoDistribution
1518
for i in iter(pkg_resources.working_set):
16-
self._components.append(Component(name=i.project_name, version=i.version, type='pypi'))
19+
c = Component(name=i.project_name, version=i.version)
20+
i_metadata = metadata(i.project_name)
21+
22+
if 'Author' in i_metadata.keys():
23+
c.set_author(i_metadata.get('Author'))
24+
25+
self._components.append(c)

tests/test_e2e_environment.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,45 @@
1+
import json
12
from unittest import TestCase
3+
from xml.etree import ElementTree
24

5+
from cyclonedx.model.bom import Bom
6+
from cyclonedx.output import get_instance, OutputFormat
7+
from cyclonedx.output.json import Json
8+
from cyclonedx.output.xml import Xml
39
from cyclonedx.parser.environment import EnvironmentParser
410

511

6-
class TestRequirementsParser(TestCase):
12+
class TestE2EEnvironment(TestCase):
713

8-
def test_simple(self):
9-
"""
10-
@todo This test is a vague as it will detect the unique environment where tests are being executed -
11-
so is this valid?
14+
def test_json_defaults(self):
15+
outputter: Json = get_instance(bom=Bom.from_parser(EnvironmentParser()), output_format=OutputFormat.JSON)
16+
bom_json = json.loads(outputter.output_as_string())
17+
component_this_library = next(
18+
(x for x in bom_json['components'] if x['purl'] == 'pkg:pypi/[email protected]'), None
19+
)
1220

13-
:return:
14-
"""
15-
parser = EnvironmentParser()
16-
self.assertGreater(parser.component_count(), 1)
21+
self.assertTrue('author' in component_this_library.keys(), 'author is missing from JSON BOM')
22+
self.assertEqual(component_this_library['author'], 'Sonatype Community')
23+
self.assertEqual(component_this_library['name'], 'cyclonedx-python-lib')
24+
self.assertEqual(component_this_library['version'], '0.0.1')
25+
26+
def test_xml_defaults(self):
27+
outputter: Xml = get_instance(bom=Bom.from_parser(EnvironmentParser()))
28+
29+
# Check we have cyclonedx-python-lib version 0.0.1 with Author, Name and Version
30+
bom_xml_e = ElementTree.fromstring(outputter.output_as_string())
31+
component_this_library = bom_xml_e.find('./{{{}}}components/{{{}}}component[@bom-ref=\'pkg:pypi/{}\']'.format(
32+
outputter.get_target_namespace(), outputter.get_target_namespace(), '[email protected]'
33+
))
34+
35+
author = component_this_library.find('./{{{}}}author'.format(outputter.get_target_namespace()))
36+
self.assertIsNotNone(author, 'No author element but one was expected.')
37+
self.assertEqual(author.text, 'Sonatype Community')
38+
39+
name = component_this_library.find('./{{{}}}name'.format(outputter.get_target_namespace()))
40+
self.assertIsNotNone(name, 'No name element but one was expected.')
41+
self.assertEqual(name.text, 'cyclonedx-python-lib')
42+
43+
version = component_this_library.find('./{{{}}}version'.format(outputter.get_target_namespace()))
44+
self.assertIsNotNone(version, 'No version element but one was expected.')
45+
self.assertEqual(version.text, '0.0.1')

0 commit comments

Comments
 (0)