Skip to content

Commit bd29c78

Browse files
committed
feat: add support for Conda
Signed-off-by: Paul Horton <[email protected]>
1 parent f132c92 commit bd29c78

File tree

8 files changed

+798
-0
lines changed

8 files changed

+798
-0
lines changed

cyclonedx/parser/conda.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# encoding: utf-8
2+
3+
# This file is part of CycloneDX Python Lib
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# SPDX-License-Identifier: Apache-2.0
18+
# Copyright (c) OWASP Foundation. All Rights Reserved.
19+
import json
20+
from abc import abstractmethod
21+
from typing import List
22+
23+
from . import BaseParser
24+
from ..model import ExternalReference, ExternalReferenceType
25+
from ..model.component import Component
26+
from ..utils.conda import parse_conda_json_to_conda_package, parse_conda_list_str_to_conda_package, CondaPackage
27+
28+
29+
class _BaseCondaParser(BaseParser):
30+
"""
31+
Internal abstract parser - not for programatic use.
32+
33+
"""
34+
def __init__(self, conda_data: str):
35+
super().__init__()
36+
self._conda_packages: List[CondaPackage] = []
37+
self._parse_to_conda_packages(data_str=conda_data)
38+
self._conda_packages_to_components()
39+
40+
@abstractmethod
41+
def _parse_to_conda_packages(self, data_str: str):
42+
"""
43+
Abstract method for implementation by concrete Conda Parsers
44+
45+
Params:
46+
data_str:
47+
`str` data passed into the Parser
48+
49+
Returns:
50+
A `list` of `CondaPackage` instances parsed.
51+
"""
52+
pass
53+
54+
def _conda_packages_to_components(self):
55+
"""
56+
Converts the parsed `CondaPackage` instances into `Component` instances.
57+
58+
"""
59+
for conda_package in self._conda_packages:
60+
c = Component(
61+
name=conda_package['name'], version=str(conda_package['version'])
62+
)
63+
c.add_external_reference(ExternalReference(
64+
reference_type=ExternalReferenceType.DISTRIBUTION,
65+
url=conda_package['base_url'],
66+
comment=f"Distribution name {conda_package['dist_name']}"
67+
))
68+
69+
self._components.append(c)
70+
71+
72+
class CondaListJsonParser(_BaseCondaParser):
73+
"""
74+
This parser is intended to receive the output from the command `conda list --json`.
75+
"""
76+
77+
def _parse_to_conda_packages(self, data_str: str):
78+
conda_list_content = json.loads(data_str)
79+
80+
for package in conda_list_content:
81+
conda_package = parse_conda_json_to_conda_package(conda_json_str=json.dumps(package))
82+
if conda_package:
83+
self._conda_packages.append(conda_package)
84+
85+
86+
class CondaListExplicitParser(_BaseCondaParser):
87+
"""
88+
This parser is intended to receive the output from the command `conda list --explicit` or
89+
`conda list --explicit --md5`.
90+
"""
91+
92+
def _parse_to_conda_packages(self, data_str: str):
93+
for line in data_str.replace('\r\n', '\n').split('\n'):
94+
line = line.strip()
95+
conda_package = parse_conda_list_str_to_conda_package(conda_list_str=line)
96+
if conda_package:
97+
self._conda_packages.append(conda_package)

cyclonedx/utils/__init__.py

Whitespace-only changes.

cyclonedx/utils/conda.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# encoding: utf-8
2+
3+
# This file is part of CycloneDX Python Lib
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# SPDX-License-Identifier: Apache-2.0
18+
# Copyright (c) OWASP Foundation. All Rights Reserved.
19+
import json
20+
from json import JSONDecodeError
21+
22+
from typing import TypedDict, Union
23+
from urllib.parse import urlparse
24+
25+
26+
class CondaPackage(TypedDict):
27+
"""
28+
Internal package for unifying Conda package definitions to.
29+
"""
30+
base_url: str
31+
build_number: int
32+
build_string: str
33+
channel: str
34+
dist_name: str
35+
name: str
36+
platform: str
37+
version: str
38+
md5_hash: str
39+
40+
41+
def parse_conda_json_to_conda_package(conda_json_str: str) -> Union[CondaPackage, None]:
42+
try:
43+
package_data = json.loads(conda_json_str)
44+
except JSONDecodeError:
45+
print(f'Failed to decode JSON: {conda_json_str}')
46+
raise ValueError(f'Invalid JSON supplied - cannot be parsed: {conda_json_str}')
47+
48+
if 'md5_hash' not in package_data.keys():
49+
package_data['md5_hash'] = None
50+
51+
if isinstance(package_data, dict):
52+
return CondaPackage(**package_data)
53+
54+
return None
55+
56+
57+
def parse_conda_list_str_to_conda_package(conda_list_str: str) -> Union[CondaPackage, None]:
58+
"""
59+
Helper method for parsing a line of output from `conda list --explicit` into our internal `CondaPackage` object.
60+
61+
Params:
62+
conda_list_str:
63+
Line of output from `conda list --explicit`
64+
65+
Returns:
66+
Instance of `CondaPackage` else `None`.
67+
"""
68+
69+
line = conda_list_str.strip()
70+
71+
if line[0:1] == '#' or line[0:1] == '@' or len(line) == 0:
72+
# Skip comments, @EXPLICT or empty lines
73+
return None
74+
75+
# Remove any hash
76+
package_hash: str = None
77+
if '#' in line:
78+
hash_parts = line.split('#')
79+
if len(hash_parts) > 1:
80+
package_hash = hash_parts.pop()
81+
line = ''.join(hash_parts)
82+
83+
package_parts = line.split('/')
84+
package_name_version_build_string = package_parts.pop()
85+
package_arch = package_parts.pop()
86+
package_url = urlparse('/'.join(package_parts))
87+
88+
try:
89+
package_nvbs_parts = package_name_version_build_string.split('-')
90+
build_number_with_opt_string = package_nvbs_parts.pop()
91+
if '.' in build_number_with_opt_string:
92+
# Remove any .conda at the end if present or other package type eg .tar.gz
93+
pos = build_number_with_opt_string.find('.')
94+
build_number_with_opt_string = build_number_with_opt_string[0:pos]
95+
96+
if '_' in build_number_with_opt_string:
97+
bnbs_parts = build_number_with_opt_string.split('_')
98+
if len(bnbs_parts) == 2:
99+
build_number = int(bnbs_parts.pop())
100+
build_string = build_number_with_opt_string
101+
else:
102+
raise ValueError(f'Unexpected build version string for Conda Package: {conda_list_str}')
103+
else:
104+
build_string = None
105+
build_number = int(build_number_with_opt_string)
106+
107+
build_version = package_nvbs_parts.pop()
108+
package_name = '-'.join(package_nvbs_parts)
109+
except IndexError as e:
110+
raise ValueError(f'Error parsing {package_nvbs_parts} from {conda_list_str} IndexError: {str(e)}')
111+
112+
return CondaPackage(
113+
base_url=package_url.geturl(), build_number=build_number, build_string=build_string,
114+
channel=package_url.path[1:], dist_name=f'{package_name}-{build_version}-{build_string}',
115+
name=package_name, platform=package_arch, version=build_version, md5_hash=package_hash
116+
)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# This file may be used to create an environment using:
2+
# $ conda create --name <env> --file <this file>
3+
# platform: osx-64
4+
@EXPLICIT
5+
https://repo.anaconda.com/pkgs/main/osx-64/ca-certificates-2021.7.5-hecd8cb5_1.conda#c2d0ae65c08dacdcf86770b7b5bbb187
6+
https://repo.anaconda.com/pkgs/main/osx-64/libcxx-10.0.0-1.conda#86574bfd5bcf4921237da41c07534cdc
7+
https://repo.anaconda.com/pkgs/main/noarch/tzdata-2021a-h52ac0ba_0.conda#d42e4db918af84a470286e4c300604a3
8+
https://repo.anaconda.com/pkgs/main/osx-64/xz-5.2.5-h1de35cc_0.conda#f38610dab0f2b0cb05f1b31f354113c5
9+
https://repo.anaconda.com/pkgs/main/osx-64/yaml-0.2.5-haf1e3a3_0.conda#73628ed86f99adf6a0cb81dd20e426cd
10+
https://repo.anaconda.com/pkgs/main/osx-64/zlib-1.2.11-h1de35cc_3.conda#67bb31afee816662edebfc3171360ccf
11+
https://repo.anaconda.com/pkgs/main/osx-64/libffi-3.3-hb1e8313_2.conda#0c959d444ac65555cb836cdbd3e9a2d9
12+
https://repo.anaconda.com/pkgs/main/osx-64/ncurses-6.2-h0a44026_1.conda#649f497ed2ff2704749256d3532d144b
13+
https://repo.anaconda.com/pkgs/main/osx-64/openssl-1.1.1k-h9ed2024_0.conda#2ecbfa7a9684bbaaa057c6dac778abc3
14+
https://repo.anaconda.com/pkgs/main/osx-64/tk-8.6.10-hb0a8c7a_0.conda#2f199f5862f5b000479408673eadb88d
15+
https://repo.anaconda.com/pkgs/main/osx-64/readline-8.1-h9ed2024_0.conda#ce1a650fddb885c47ccdb28c90a2057a
16+
https://repo.anaconda.com/pkgs/main/osx-64/sqlite-3.36.0-hce871da_0.conda#7e47a43e94a61ad1c7a5d910c5e970fd
17+
https://repo.anaconda.com/pkgs/main/osx-64/python-3.9.5-h88f2d9e_3.conda#f10a9a3fd6b0936cc05f9e94e06a2d94
18+
https://repo.anaconda.com/pkgs/main/osx-64/certifi-2021.5.30-py39hecd8cb5_0.conda#48a9d5f197fbcef83387ca18e7216068
19+
https://repo.anaconda.com/pkgs/main/osx-64/chardet-4.0.0-py39hecd8cb5_1003.conda#535b448899dcf60a12dc683a90be5c0c
20+
https://repo.anaconda.com/pkgs/main/noarch/idna-2.10-pyhd3eb1b0_0.tar.bz2#153ff132f593ea80aae2eea61a629c92
21+
https://repo.anaconda.com/pkgs/main/osx-64/pycosat-0.6.3-py39h9ed2024_0.conda#625905706ac243fae350f4dfc63e1b2d
22+
https://repo.anaconda.com/pkgs/main/noarch/pycparser-2.20-py_2.conda#fcfeb621c6f895f3562ff01d9d6ce959
23+
https://repo.anaconda.com/pkgs/main/osx-64/pysocks-1.7.1-py39hecd8cb5_0.conda#4765ca1a39ea5287cbe170734ac83e37
24+
https://repo.anaconda.com/pkgs/main/osx-64/python.app-3-py39h9ed2024_0.conda#8a562918b61f71b3cac387cec842cbd2
25+
https://repo.anaconda.com/pkgs/main/osx-64/ruamel_yaml-0.15.100-py39h9ed2024_0.conda#d7abc88ed5b3a8d9e4af95b8a64d87fe
26+
https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_0.conda#529b369e1accc75b89f2d7e184a898d0
27+
https://repo.anaconda.com/pkgs/main/noarch/tqdm-4.61.2-pyhd3eb1b0_1.conda#f061c1548e813e7544d0017a71651e04
28+
https://repo.anaconda.com/pkgs/main/noarch/wheel-0.36.2-pyhd3eb1b0_0.conda#31029affc034e8c7203202417cebd157
29+
https://repo.anaconda.com/pkgs/main/osx-64/cffi-1.14.6-py39h2125817_0.conda#78ee9d3613c9f3f50a9eb4acd45bc747
30+
https://repo.anaconda.com/pkgs/main/osx-64/conda-package-handling-1.7.3-py39h9ed2024_1.conda#04a0be0e0f5bad8d8fc7ebe4803ea446
31+
https://repo.anaconda.com/pkgs/main/osx-64/setuptools-52.0.0-py39hecd8cb5_0.conda#5c9e48476978303d04650c21ee55f365
32+
https://repo.anaconda.com/pkgs/main/osx-64/brotlipy-0.7.0-py39h9ed2024_1003.conda#a08f6f5f899aff4a07351217b36fae41
33+
https://repo.anaconda.com/pkgs/main/osx-64/cryptography-3.4.7-py39h2fd3fbb_0.conda#822e8758f6e705c84b01480810eb24b6
34+
https://repo.anaconda.com/pkgs/main/osx-64/pip-21.1.3-py39hecd8cb5_0.conda#7bae540cbc7fdc9627b588c760b05e58
35+
https://repo.anaconda.com/pkgs/main/noarch/pyopenssl-20.0.1-pyhd3eb1b0_1.conda#ac62ddccf2e89f7a35867b2478a278af
36+
https://repo.anaconda.com/pkgs/main/noarch/urllib3-1.26.6-pyhd3eb1b0_1.conda#5c72bc4a5a4fc0420b0e73b3acb1f52b
37+
https://repo.anaconda.com/pkgs/main/noarch/requests-2.25.1-pyhd3eb1b0_0.conda#9d30b41b315403c7c74793b9b8d88580
38+
https://repo.anaconda.com/pkgs/main/osx-64/conda-4.10.3-py39hecd8cb5_0.tar.bz2#bc36833ee4a90c212e0695675bcfe120
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# This file may be used to create an environment using:
2+
# $ conda create --name <env> --file <this file>
3+
# platform: osx-64
4+
@EXPLICIT
5+
https://repo.anaconda.com/pkgs/main/osx-64/ca-certificates-2021.7.5-hecd8cb5_1.conda
6+
https://repo.anaconda.com/pkgs/main/osx-64/libcxx-10.0.0-1.conda
7+
https://repo.anaconda.com/pkgs/main/noarch/tzdata-2021a-h52ac0ba_0.conda
8+
https://repo.anaconda.com/pkgs/main/osx-64/xz-5.2.5-h1de35cc_0.conda
9+
https://repo.anaconda.com/pkgs/main/osx-64/yaml-0.2.5-haf1e3a3_0.conda
10+
https://repo.anaconda.com/pkgs/main/osx-64/zlib-1.2.11-h1de35cc_3.conda
11+
https://repo.anaconda.com/pkgs/main/osx-64/libffi-3.3-hb1e8313_2.conda
12+
https://repo.anaconda.com/pkgs/main/osx-64/ncurses-6.2-h0a44026_1.conda
13+
https://repo.anaconda.com/pkgs/main/osx-64/openssl-1.1.1k-h9ed2024_0.conda
14+
https://repo.anaconda.com/pkgs/main/osx-64/tk-8.6.10-hb0a8c7a_0.conda
15+
https://repo.anaconda.com/pkgs/main/osx-64/readline-8.1-h9ed2024_0.conda
16+
https://repo.anaconda.com/pkgs/main/osx-64/sqlite-3.36.0-hce871da_0.conda
17+
https://repo.anaconda.com/pkgs/main/osx-64/python-3.9.5-h88f2d9e_3.conda
18+
https://repo.anaconda.com/pkgs/main/osx-64/certifi-2021.5.30-py39hecd8cb5_0.conda
19+
https://repo.anaconda.com/pkgs/main/osx-64/chardet-4.0.0-py39hecd8cb5_1003.conda
20+
https://repo.anaconda.com/pkgs/main/noarch/idna-2.10-pyhd3eb1b0_0.tar.bz2
21+
https://repo.anaconda.com/pkgs/main/osx-64/pycosat-0.6.3-py39h9ed2024_0.conda
22+
https://repo.anaconda.com/pkgs/main/noarch/pycparser-2.20-py_2.conda
23+
https://repo.anaconda.com/pkgs/main/osx-64/pysocks-1.7.1-py39hecd8cb5_0.conda
24+
https://repo.anaconda.com/pkgs/main/osx-64/python.app-3-py39h9ed2024_0.conda
25+
https://repo.anaconda.com/pkgs/main/osx-64/ruamel_yaml-0.15.100-py39h9ed2024_0.conda
26+
https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_0.conda
27+
https://repo.anaconda.com/pkgs/main/noarch/tqdm-4.61.2-pyhd3eb1b0_1.conda
28+
https://repo.anaconda.com/pkgs/main/noarch/wheel-0.36.2-pyhd3eb1b0_0.conda
29+
https://repo.anaconda.com/pkgs/main/osx-64/cffi-1.14.6-py39h2125817_0.conda
30+
https://repo.anaconda.com/pkgs/main/osx-64/conda-package-handling-1.7.3-py39h9ed2024_1.conda
31+
https://repo.anaconda.com/pkgs/main/osx-64/setuptools-52.0.0-py39hecd8cb5_0.conda
32+
https://repo.anaconda.com/pkgs/main/osx-64/brotlipy-0.7.0-py39h9ed2024_1003.conda
33+
https://repo.anaconda.com/pkgs/main/osx-64/cryptography-3.4.7-py39h2fd3fbb_0.conda
34+
https://repo.anaconda.com/pkgs/main/osx-64/pip-21.1.3-py39hecd8cb5_0.conda
35+
https://repo.anaconda.com/pkgs/main/noarch/pyopenssl-20.0.1-pyhd3eb1b0_1.conda
36+
https://repo.anaconda.com/pkgs/main/noarch/urllib3-1.26.6-pyhd3eb1b0_1.conda
37+
https://repo.anaconda.com/pkgs/main/noarch/requests-2.25.1-pyhd3eb1b0_0.conda
38+
https://repo.anaconda.com/pkgs/main/osx-64/conda-4.10.3-py39hecd8cb5_0.tar.bz2

0 commit comments

Comments
 (0)