Skip to content

Commit d45f75b

Browse files
authored
Merge pull request #36 from CycloneDX/feat/add-license-support
Add support for parsing package licenses from installed packages
2 parents 91f9a8b + c414eaf commit d45f75b

File tree

8 files changed

+108
-2
lines changed

8 files changed

+108
-2
lines changed

cyclonedx/output/json.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,15 @@ def _get_component_as_dict(self, component: Component) -> dict:
6565
})
6666
c['hashes'] = hashes
6767

68+
if component.get_license():
69+
c['licenses'] = [
70+
{
71+
"license": {
72+
"name": component.get_license()
73+
}
74+
}
75+
]
76+
6877
if self.component_supports_author() and component.get_author():
6978
c['author'] = component.get_author()
7079

cyclonedx/output/xml.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ def _get_component_as_xml_element(self, component: Component) -> ElementTree.Ele
102102
# purl
103103
ElementTree.SubElement(component_element, 'purl').text = component.get_purl()
104104

105+
# licenses
106+
if component.get_license():
107+
licenses_e = ElementTree.SubElement(component_element, 'licenses')
108+
license_e = ElementTree.SubElement(licenses_e, 'license')
109+
ElementTree.SubElement(license_e, 'name').text = component.get_license()
110+
105111
# externalReferences
106112
if self.component_supports_external_references() and len(component.get_external_references()) > 0:
107113
external_references_e = ElementTree.SubElement(component_element, 'externalReferences')

cyclonedx/parser/environment.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,13 @@ def __init__(self):
6060
if 'Author' in i_metadata.keys():
6161
c.set_author(author=i_metadata.get('Author'))
6262

63-
if 'License' in i_metadata.keys():
63+
if 'License' in i_metadata.keys() and i_metadata.get('License') != 'UNKNOWN':
6464
c.set_license(license_str=i_metadata.get('License'))
6565

66+
for classifier in i_metadata.get_all('Classifier'):
67+
if str(classifier).startswith('License :: OSI Approved :: '):
68+
c.set_license(license_str=str(classifier).replace('License :: OSI Approved :: ', '').strip())
69+
6670
self._components.append(c)
6771

6872
@staticmethod
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.3",
4+
"serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
5+
"version": 1,
6+
"metadata": {
7+
"timestamp": "2021-09-01T10:50:42.051979+00:00",
8+
"tools": [
9+
{
10+
"vendor": "CycloneDX",
11+
"name": "cyclonedx-python-lib",
12+
"version": "VERSION"
13+
}
14+
]
15+
},
16+
"components": [
17+
{
18+
"type": "library",
19+
"name": "toml",
20+
"version": "0.10.2",
21+
"purl": "pkg:pypi/[email protected]?extension=tar.gz",
22+
"licenses": [
23+
{
24+
"license": {
25+
"name": "MIT License"
26+
}
27+
}
28+
]
29+
}
30+
]
31+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.3" version="1">
3+
<metadata>
4+
<timestamp>2021-09-01T10:50:42.051979+00:00</timestamp>
5+
<tools>
6+
<tool>
7+
<vendor>CycloneDX</vendor>
8+
<name>cyclonedx-python-lib</name>
9+
<version>VERSION</version>
10+
</tool>
11+
</tools>
12+
</metadata>
13+
<components>
14+
<component type="library" bom-ref="pkg:pypi/[email protected]?extension=tar.gz">
15+
<name>toml</name>
16+
<version>0.10.2</version>
17+
<purl>pkg:pypi/[email protected]?extension=tar.gz</purl>
18+
<licenses>
19+
<license>
20+
<name>MIT License</name>
21+
</license>
22+
</licenses>
23+
</component>
24+
</components>
25+
</bom>

tests/test_output_json.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,15 @@ def test_bom_v1_3_with_component_external_references(self):
8484
'fixtures/bom_v1.3_toml_with_component_external_references.json')) as expected_json:
8585
self.assertEqualJsonBom(a=outputter.output_as_string(), b=expected_json.read())
8686
expected_json.close()
87+
88+
def test_bom_v1_3_with_component_license(self):
89+
bom = Bom()
90+
c = Component(name='toml', version='0.10.2', qualifiers='extension=tar.gz')
91+
c.set_license('MIT License')
92+
bom.add_component(c)
93+
outputter: Json = get_instance(bom=bom, output_format=OutputFormat.JSON)
94+
self.assertIsInstance(outputter, JsonV1Dot3)
95+
with open(join(dirname(__file__),
96+
'fixtures/bom_v1.3_toml_with_component_license.json')) as expected_json:
97+
self.assertEqualJsonBom(a=outputter.output_as_string(), b=expected_json.read())
98+
expected_json.close()

tests/test_output_xml.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,16 @@ def test_bom_v1_3_with_component_external_references(self):
165165
self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(),
166166
namespace=outputter.get_target_namespace())
167167
expected_xml.close()
168+
169+
def test_with_component_license(self):
170+
bom = Bom()
171+
c = Component(name='toml', version='0.10.2', qualifiers='extension=tar.gz')
172+
c.set_license('MIT License')
173+
bom.add_component(c)
174+
outputter: Xml = get_instance(bom=bom)
175+
self.assertIsInstance(outputter, XmlV1Dot3)
176+
with open(join(dirname(__file__),
177+
'fixtures/bom_v1.3_toml_with_component_license.xml')) as expected_xml:
178+
self.assertEqualXmlBom(a=outputter.output_as_string(), b=expected_xml.read(),
179+
namespace=outputter.get_target_namespace())
180+
expected_xml.close()

tests/test_parser_environment.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@
2020
from unittest import TestCase
2121

2222
from cyclonedx.parser.environment import EnvironmentParser
23+
from cyclonedx.model.component import Component
2324

2425

25-
class TestRequirementsParser(TestCase):
26+
class TestEnvironmentParser(TestCase):
2627

2728
def test_simple(self):
2829
"""
@@ -33,3 +34,8 @@ def test_simple(self):
3334
"""
3435
parser = EnvironmentParser()
3536
self.assertGreater(parser.component_count(), 1)
37+
38+
# We can only be sure that tox is in the environment, for example as we use tox to run tests
39+
c_tox: Component = [x for x in parser.get_components() if x.get_name() == 'tox'][0]
40+
self.assertIsNotNone(c_tox.get_license())
41+
self.assertEqual('MIT License', c_tox.get_license())

0 commit comments

Comments
 (0)