Skip to content

Commit 4079323

Browse files
authored
Merge pull request #34 from CycloneDX/fix/issue-33-pipfile-lock-parse-failure
BUG: Fixe for `Pipfile.lock` parsing + accidental data sharing issues identified during testing
2 parents 298318f + 00cd1ca commit 4079323

File tree

7 files changed

+109
-85
lines changed

7 files changed

+109
-85
lines changed

cyclonedx/model/__init__.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@
2828

2929

3030
def sha1sum(filename: str) -> str:
31+
"""
32+
Generate a SHA1 hash of the provided file.
33+
34+
Args:
35+
filename:
36+
Absolute path to file to hash as `str`
37+
38+
Returns:
39+
SHA-1 hash
40+
"""
3141
h = hashlib.sha1()
3242
b = bytearray(128 * 1024)
3343
mv = memoryview(b)
@@ -67,9 +77,6 @@ class HashType:
6777
See the CycloneDX Schema for hashType: https://cyclonedx.org/docs/1.3/#type_hashType
6878
"""
6979

70-
_algorithm: HashAlgorithm
71-
_value: str
72-
7380
@staticmethod
7481
def from_composite_str(composite_hash: str):
7582
"""
@@ -84,7 +91,7 @@ def from_composite_str(composite_hash: str):
8491
Returns:
8592
An instance of `HashType` when possible, else `None`.
8693
"""
87-
algorithm: HashAlgorithm = None
94+
algorithm = None
8895
parts = composite_hash.split(':')
8996

9097
algorithm_prefix = parts[0].lower()
@@ -110,6 +117,9 @@ def get_algorithm(self) -> HashAlgorithm:
110117
def get_hash_value(self) -> str:
111118
return self._value
112119

120+
def __repr__(self):
121+
return f'<Hash {self._algorithm.value}:{self._value}>'
122+
113123

114124
class ExternalReferenceType(Enum):
115125
"""
@@ -144,20 +154,13 @@ class ExternalReference:
144154
.. note::
145155
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.3/#type_externalReference
146156
"""
147-
_reference_type: ExternalReferenceType
148-
_url: str
149-
_comment: str
150-
_hashes: List[HashType] = []
151157

152158
def __init__(self, reference_type: ExternalReferenceType, url: str, comment: str = None,
153159
hashes: List[HashType] = None):
154-
self._reference_type = reference_type
160+
self._reference_type: ExternalReferenceType = reference_type
155161
self._url = url
156162
self._comment = comment
157-
if not hashes:
158-
self._hashes.clear()
159-
else:
160-
self._hashes = hashes
163+
self._hashes: List[HashType] = hashes if hashes else []
161164

162165
def add_hash(self, our_hash: HashType):
163166
"""
@@ -204,3 +207,6 @@ def get_url(self) -> str:
204207
URI as a `str`.
205208
"""
206209
return self._url
210+
211+
def __repr__(self):
212+
return f'<ExternalReference {self._reference_type.name}, {self._url}> {self._hashes}'

cyclonedx/model/bom.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,11 @@ class Tool:
3737
See the CycloneDX Schema for toolType: https://cyclonedx.org/docs/1.3/#type_toolType
3838
"""
3939

40-
_vendor: str = None
41-
_name: str = None
42-
_version: str = None
43-
_hashes: List[HashType] = []
44-
4540
def __init__(self, vendor: str, name: str, version: str, hashes: List[HashType] = []):
4641
self._vendor = vendor
4742
self._name = name
4843
self._version = version
49-
self._hashes = hashes
44+
self._hashes: List[HashType] = hashes
5045

5146
def get_hashes(self) -> List[HashType]:
5247
"""
@@ -107,15 +102,11 @@ class BomMetaData:
107102
See the CycloneDX Schema for Bom metadata: https://cyclonedx.org/docs/1.3/#type_metadata
108103
"""
109104

110-
_timestamp: datetime.datetime
111-
_tools: List[Tool] = []
112-
113105
def __init__(self, tools: List[Tool] = []):
114106
self._timestamp = datetime.datetime.now(tz=datetime.timezone.utc)
115-
self._tools.clear()
107+
self._tools: List[Tool] = tools
116108
if len(tools) == 0:
117109
tools.append(ThisTool)
118-
self._tools = tools
119110

120111
def add_tool(self, tool: Tool):
121112
"""
@@ -158,10 +149,6 @@ class Bom:
158149
`cyclonedx.output.BaseOutput` to produce a CycloneDX document according to a specific schema version and format.
159150
"""
160151

161-
_uuid: str
162-
_metadata: BomMetaData = None
163-
_components: List[Component] = []
164-
165152
@staticmethod
166153
def from_parser(parser: BaseParser):
167154
"""
@@ -185,8 +172,8 @@ def __init__(self):
185172
New, empty `cyclonedx.model.bom.Bom` instance.
186173
"""
187174
self._uuid = uuid4()
188-
self._metadata = BomMetaData(tools=[])
189-
self._components.clear()
175+
self._metadata: BomMetaData = BomMetaData(tools=[])
176+
self._components: List[Component] = []
190177

191178
def add_component(self, component: Component):
192179
"""

cyclonedx/model/component.py

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,6 @@ class Component:
5050
.. note::
5151
See the CycloneDX Schema definition: https://cyclonedx.org/docs/1.3/#type_component
5252
"""
53-
_type: ComponentType
54-
_package_url_type: str
55-
_namespace: str
56-
_name: str
57-
_version: str
58-
_qualifiers: str
59-
_subpath: str
60-
61-
_author: str = None
62-
_description: str = None
63-
_license: str = None
64-
65-
_hashes: List[HashType] = []
66-
_vulnerabilites: List[Vulnerability] = []
67-
_external_references: List[ExternalReference] = []
6853

6954
@staticmethod
7055
def for_file(absolute_file_path: str, path_for_bom: str = None):
@@ -96,7 +81,7 @@ def for_file(absolute_file_path: str, path_for_bom: str = None):
9681
)
9782

9883
def __init__(self, name: str, version: str, namespace: str = None, qualifiers: str = None, subpath: str = None,
99-
hashes: List[HashType] = None,
84+
hashes: List[HashType] = None, author: str = None, description: str = None, license_str: str = None,
10085
component_type: ComponentType = ComponentType.LIBRARY, package_url_type: str = 'pypi'):
10186
self._package_url_type = package_url_type
10287
self._namespace = namespace
@@ -106,11 +91,13 @@ def __init__(self, name: str, version: str, namespace: str = None, qualifiers: s
10691
self._qualifiers = qualifiers
10792
self._subpath = subpath
10893

109-
self._hashes.clear()
110-
if hashes:
111-
self._hashes = hashes
112-
self._vulnerabilites.clear()
113-
self._external_references.clear()
94+
self._author: str = author
95+
self._description: str = description
96+
self._license: str = license_str
97+
98+
self._hashes: List[HashType] = hashes if hashes else []
99+
self._vulnerabilites: List[Vulnerability] = []
100+
self._external_references: List[ExternalReference] = []
114101

115102
def add_external_reference(self, reference: ExternalReference):
116103
"""
@@ -122,15 +109,15 @@ def add_external_reference(self, reference: ExternalReference):
122109
"""
123110
self._external_references.append(reference)
124111

125-
def add_hash(self, hash: HashType):
112+
def add_hash(self, a_hash: HashType):
126113
"""
127114
Adds a hash that pins/identifies this Component.
128115
129116
Args:
130-
hash:
117+
a_hash:
131118
`HashType` instance
132119
"""
133-
self._hashes.append(hash)
120+
self._hashes.append(a_hash)
134121

135122
def add_vulnerability(self, vulnerability: Vulnerability):
136123
"""

cyclonedx/model/vulnerability.py

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ def get_from_cvss_scores(scores: tuple = None):
119119
if scores is None:
120120
return VulnerabilitySeverity.UNKNOWN
121121

122-
max_cvss_score = max(scores)
122+
max_cvss_score: float = max(scores)
123123

124124
if max_cvss_score >= 9.0:
125125
return VulnerabilitySeverity.CRITICAL
@@ -140,12 +140,6 @@ class VulnerabilityRating:
140140
.. note::
141141
See `scoreType` in https://github.com/CycloneDX/specification/blob/master/schema/ext/vulnerability-1.0.xsd
142142
"""
143-
_score_base: float
144-
_score_impact: float
145-
_score_exploitability: float
146-
_severity: VulnerabilitySeverity
147-
_method: VulnerabilitySourceType
148-
_vector: str
149143

150144
def __init__(self, score_base: float = None, score_impact: float = None, score_exploitability=None,
151145
severity: VulnerabilitySeverity = None, method: VulnerabilitySourceType = None,
@@ -216,26 +210,18 @@ class Vulnerability:
216210
"""
217211
Represents <xs:complexType name="vulnerability">
218212
"""
219-
_id: str
220-
_source_name: str
221-
_source_url: ParseResult
222-
_ratings: List[VulnerabilityRating] = []
223-
_cwes: List[int] = []
224-
_description: str = None
225-
_recommendations: List[str] = []
226-
_advisories: List[str] = []
227213

228214
def __init__(self, id: str, source_name: str = None, source_url: str = None,
229-
ratings: List[VulnerabilityRating] = [], cwes: List[int] = [], description: str = None,
230-
recommendations: List[str] = [], advisories: List[str] = []):
215+
ratings: List[VulnerabilityRating] = None, cwes: List[int] = None, description: str = None,
216+
recommendations: List[str] = None, advisories: List[str] = None):
231217
self._id = id
232218
self._source_name = source_name
233-
self._source_url = urlparse(source_url) if source_url else None
234-
self._ratings = ratings
235-
self._cwes = cwes
219+
self._source_url: ParseResult = urlparse(source_url) if source_url else None
220+
self._ratings: List[VulnerabilityRating] = ratings
221+
self._cwes: List[int] = cwes
236222
self._description = description
237-
self._recommendations = recommendations
238-
self._advisories = advisories
223+
self._recommendations: List[str] = recommendations
224+
self._advisories: List[str] = advisories
239225

240226
def get_id(self) -> str:
241227
return self._id

cyclonedx/parser/pipenv.py

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,18 @@ def __init__(self, pipenv_contents: str):
3535
name=package_name, version=str(package_data['version']).strip('='),
3636
)
3737

38-
if package_data['index'] == 'pypi':
38+
if 'index' in package_data.keys() and package_data['index'] == 'pypi':
3939
# Add download location with hashes stored in Pipfile.lock
40-
for pip_hash in package_data['hashes']:
41-
ext_ref = ExternalReference(
42-
reference_type=ExternalReferenceType.DISTRIBUTION,
43-
url=c.get_pypi_url(),
44-
comment='Distribution available from pypi.org'
45-
)
46-
ext_ref.add_hash(HashType.from_composite_str(pip_hash))
47-
c.add_external_reference(ext_ref)
40+
if 'hashes' in package_data.keys():
41+
for pip_hash in package_data['hashes']:
42+
43+
ext_ref = ExternalReference(
44+
reference_type=ExternalReferenceType.DISTRIBUTION,
45+
url=c.get_pypi_url(),
46+
comment='Distribution available from pypi.org'
47+
)
48+
ext_ref.add_hash(HashType.from_composite_str(pip_hash))
49+
c.add_external_reference(ext_ref)
4850

4951
self._components.append(c)
5052

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"_meta": {
3+
"hash": {
4+
"sha256": "8ca3da46acf801a7780c6781bed1d6b7012664226203447640cda114b13aa8aa"
5+
},
6+
"pipfile-spec": 6,
7+
"requires": {
8+
"python_version": "3.9"
9+
},
10+
"sources": [
11+
{
12+
"name": "pypi",
13+
"url": "https://pypi.org/simple",
14+
"verify_ssl": true
15+
}
16+
]
17+
},
18+
"default": {
19+
"anyio": {
20+
"hashes": [
21+
"sha256:56ceaeed2877723578b1341f4f68c29081db189cfb40a97d1922b9513f6d7db6",
22+
"sha256:8eccec339cb4a856c94a75d50fc1d451faf32a05ef406be462e2efc59c9838b0"
23+
],
24+
"markers": "python_full_version >= '3.6.2'",
25+
"version": "==3.3.3"
26+
},
27+
"toml": {
28+
"hashes": [
29+
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
30+
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
31+
],
32+
"index": "pypi",
33+
"version": "==0.10.2"
34+
}
35+
},
36+
"develop": {}
37+
}

tests/test_parser_pipenv.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,22 @@ def test_simple(self):
3535
self.assertEqual('0.10.2', components[0].get_version())
3636
self.assertEqual(len(components[0].get_external_references()), 2)
3737
self.assertEqual(len(components[0].get_external_references()[0].get_hashes()), 1)
38+
39+
def test_with_multiple_and_no_index(self):
40+
tests_pipfile_lock = os.path.join(os.path.dirname(__file__), 'fixtures/pipfile-lock-no-index-example.txt')
41+
42+
parser = PipEnvFileParser(pipenv_lock_filename=tests_pipfile_lock)
43+
self.assertEqual(2, parser.component_count())
44+
components = parser.get_components()
45+
46+
c_anyio = [x for x in components if x.get_name() == 'anyio'][0]
47+
c_toml = [x for x in components if x.get_name() == 'toml'][0]
48+
49+
self.assertEqual('anyio', c_anyio.get_name())
50+
self.assertEqual('3.3.3', c_anyio.get_version())
51+
self.assertEqual(0, len(c_anyio.get_external_references()))
52+
53+
self.assertEqual('toml', c_toml.get_name())
54+
self.assertEqual('0.10.2', c_toml.get_version())
55+
self.assertEqual(len(c_toml.get_external_references()), 2)
56+
self.assertEqual(len(c_toml.get_external_references()[0].get_hashes()), 1)

0 commit comments

Comments
 (0)