diff --git a/cyclonedx_py/client.py b/cyclonedx_py/client.py index fd4fc7984..eeecad8a7 100644 --- a/cyclonedx_py/client.py +++ b/cyclonedx_py/client.py @@ -27,11 +27,12 @@ from cyclonedx.model.bom import Bom from cyclonedx.output import BaseOutput, get_instance, OutputFormat, SchemaVersion from cyclonedx.parser import BaseParser -from cyclonedx.parser.conda import CondaListExplicitParser, CondaListJsonParser -from cyclonedx.parser.environment import EnvironmentParser -from cyclonedx.parser.pipenv import PipEnvParser -from cyclonedx.parser.poetry import PoetryParser -from cyclonedx.parser.requirements import RequirementsParser + +from .parser.conda import CondaListExplicitParser, CondaListJsonParser +from .parser.environment import EnvironmentParser +from .parser.pipenv import PipEnvParser +from .parser.poetry import PoetryParser +from .parser.requirements import RequirementsParser class CycloneDxCmdException(Exception): @@ -67,7 +68,7 @@ def get_output(self) -> BaseOutput: print(f'ERROR: {str(e)}') exit(1) - if parser.has_warnings(): + if parser and parser.has_warnings(): print('') print('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') print('!! Some of your dependencies do not have pinned version !!') diff --git a/cyclonedx_py/exception/__init__.py b/cyclonedx_py/exception/__init__.py new file mode 100644 index 000000000..87d2082d3 --- /dev/null +++ b/cyclonedx_py/exception/__init__.py @@ -0,0 +1,20 @@ +# encoding: utf-8 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +""" +Exceptions that are specific to the CycloneDX Python implementation. +""" diff --git a/cyclonedx_py/exception/parser.py b/cyclonedx_py/exception/parser.py new file mode 100644 index 000000000..e65bb5cdd --- /dev/null +++ b/cyclonedx_py/exception/parser.py @@ -0,0 +1,29 @@ +# encoding: utf-8 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +""" +Exceptions that are specific error scenarios during occuring within Parsers in the CycloneDX library implementation. +""" + +from cyclonedx.exception import CycloneDxException + + +class UnknownHashTypeException(CycloneDxException): + """ + Exception raised when we are unable to determine the type of hash from a composite hash string. + """ + pass diff --git a/cyclonedx_py/parser/__init__.py b/cyclonedx_py/parser/__init__.py new file mode 100644 index 000000000..0f05b2530 --- /dev/null +++ b/cyclonedx_py/parser/__init__.py @@ -0,0 +1,22 @@ +# encoding: utf-8 + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +Set of concrete classes and methods which allow for quick creation of a Bom instance from your environment or Python +project. + +Use a Parser instead of programmatically creating a Bom as a developer. +""" diff --git a/cyclonedx_py/parser/conda.py b/cyclonedx_py/parser/conda.py new file mode 100644 index 000000000..2ffd89f72 --- /dev/null +++ b/cyclonedx_py/parser/conda.py @@ -0,0 +1,97 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +import json +from abc import ABCMeta, abstractmethod +from typing import List + +from cyclonedx.model import ExternalReference, ExternalReferenceType +from cyclonedx.model.component import Component +from cyclonedx.parser import BaseParser + +from ..utils.conda import parse_conda_json_to_conda_package, parse_conda_list_str_to_conda_package, CondaPackage + + +class _BaseCondaParser(BaseParser, metaclass=ABCMeta): + """Internal abstract parser - not for programmatic use. + """ + + def __init__(self, conda_data: str) -> None: + super().__init__() + self._conda_packages: List[CondaPackage] = [] + self._parse_to_conda_packages(data_str=conda_data) + self._conda_packages_to_components() + + @abstractmethod + def _parse_to_conda_packages(self, data_str: str) -> None: + """ + Abstract method for implementation by concrete Conda Parsers. + + Implementation should add a `list` of `CondaPackage` instances to `self._conda_packages` + + Params: + data_str: + `str` data passed into the Parser + """ + pass + + def _conda_packages_to_components(self) -> None: + """ + Converts the parsed `CondaPackage` instances into `Component` instances. + + """ + for conda_package in self._conda_packages: + c = Component( + name=conda_package['name'], version=str(conda_package['version']) + ) + c.add_external_reference(ExternalReference( + reference_type=ExternalReferenceType.DISTRIBUTION, + url=conda_package['base_url'], + comment=f"Distribution name {conda_package['dist_name']}" + )) + + self._components.append(c) + + +class CondaListJsonParser(_BaseCondaParser): + """ + This parser is intended to receive the output from the command `conda list --json`. + """ + + def _parse_to_conda_packages(self, data_str: str) -> None: + conda_list_content = json.loads(data_str) + + for package in conda_list_content: + conda_package = parse_conda_json_to_conda_package(conda_json_str=json.dumps(package)) + if conda_package: + self._conda_packages.append(conda_package) + + +class CondaListExplicitParser(_BaseCondaParser): + """ + This parser is intended to receive the output from the command `conda list --explicit` or + `conda list --explicit --md5`. + """ + + def _parse_to_conda_packages(self, data_str: str) -> None: + for line in data_str.replace('\r\n', '\n').split('\n'): + line = line.strip() + conda_package = parse_conda_list_str_to_conda_package(conda_list_str=line) + if conda_package: + self._conda_packages.append(conda_package) diff --git a/cyclonedx_py/parser/environment.py b/cyclonedx_py/parser/environment.py new file mode 100644 index 000000000..f59e2b1db --- /dev/null +++ b/cyclonedx_py/parser/environment.py @@ -0,0 +1,84 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +""" +Parser classes for reading installed packages in your current Python environment. + +These parsers look at installed packages only - not what you have defined in any dependency tool - see the other Parsers +if you want to derive CycloneDX from declared dependencies. + + +The Environment Parsers support population of the following data about Components: + +""" + +import sys + +from pkg_resources import DistInfoDistribution # type: ignore + +if sys.version_info >= (3, 8): + from importlib.metadata import metadata + from email.message import Message as _MetadataReturn +else: + from importlib_metadata import metadata, PackageMetadata as _MetadataReturn + +from cyclonedx.model import LicenseChoice +from cyclonedx.model.component import Component +from cyclonedx.parser import BaseParser + + +class EnvironmentParser(BaseParser): + """ + This will look at the current Python environment and list out all installed packages. + + Best used when you have virtual Python environments per project. + """ + + def __init__(self) -> None: + super().__init__() + + import pkg_resources + + i: DistInfoDistribution + for i in iter(pkg_resources.working_set): + c = Component(name=i.project_name, version=i.version) + + i_metadata = self._get_metadata_for_package(i.project_name) + if 'Author' in i_metadata: + c.author = i_metadata['Author'] + + if 'License' in i_metadata and i_metadata['License'] != 'UNKNOWN': + c.licenses.append( + LicenseChoice(license_expression=i_metadata['License']) + ) + + if 'Classifier' in i_metadata: + for classifier in i_metadata['Classifier']: + if str(classifier).startswith('License :: OSI Approved :: '): + c.licenses.append( + LicenseChoice( + license_expression=str(classifier).replace('License :: OSI Approved :: ', '').strip() + ) + ) + + self._components.append(c) + + @staticmethod + def _get_metadata_for_package(package_name: str) -> _MetadataReturn: + return metadata(package_name) diff --git a/cyclonedx_py/parser/pipenv.py b/cyclonedx_py/parser/pipenv.py new file mode 100644 index 000000000..1a084aa0d --- /dev/null +++ b/cyclonedx_py/parser/pipenv.py @@ -0,0 +1,59 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +import json +from typing import Any, Dict + +from cyclonedx.model import ExternalReference, ExternalReferenceType, HashType +from cyclonedx.model.component import Component +from cyclonedx.parser import BaseParser + + +class PipEnvParser(BaseParser): + + def __init__(self, pipenv_contents: str) -> None: + super().__init__() + + pipfile_lock_contents = json.loads(pipenv_contents) + pipfile_default: Dict[str, Dict[str, Any]] = pipfile_lock_contents.get('default') or {} + + for (package_name, package_data) in pipfile_default.items(): + c = Component( + name=package_name, + version=str(package_data.get('version') or 'unknown').lstrip('='), + ) + if package_data.get('index') == 'pypi' and isinstance(package_data.get('hashes'), list): + # Add download location with hashes stored in Pipfile.lock + for pip_hash in package_data['hashes']: + ext_ref = ExternalReference( + reference_type=ExternalReferenceType.DISTRIBUTION, + url=c.get_pypi_url(), + comment='Distribution available from pypi.org' + ) + ext_ref.add_hash(HashType.from_composite_str(pip_hash)) + c.add_external_reference(ext_ref) + + self._components.append(c) + + +class PipEnvFileParser(PipEnvParser): + + def __init__(self, pipenv_lock_filename: str) -> None: + with open(pipenv_lock_filename) as r: + super(PipEnvFileParser, self).__init__(pipenv_contents=r.read()) diff --git a/cyclonedx_py/parser/poetry.py b/cyclonedx_py/parser/poetry.py new file mode 100644 index 000000000..3ee2950b7 --- /dev/null +++ b/cyclonedx_py/parser/poetry.py @@ -0,0 +1,59 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +from cyclonedx.model import ExternalReference, ExternalReferenceType, HashType +from cyclonedx.model.component import Component +from cyclonedx.parser import BaseParser +from toml import loads as load_toml + +from ..exception.parser import UnknownHashTypeException + + +class PoetryParser(BaseParser): + + def __init__(self, poetry_lock_contents: str) -> None: + super().__init__() + poetry_lock = load_toml(poetry_lock_contents) + + for package in poetry_lock['package']: + component = Component( + name=package['name'], version=package['version'] + ) + + for file_metadata in poetry_lock['metadata']['files'][package['name']]: + try: + component.add_external_reference(ExternalReference( + reference_type=ExternalReferenceType.DISTRIBUTION, + url=component.get_pypi_url(), + comment=f'Distribution file: {file_metadata["file"]}', + hashes=[HashType.from_composite_str(file_metadata['hash'])] + )) + except UnknownHashTypeException: + # @todo add logging for this type of exception? + pass + + self._components.append(component) + + +class PoetryFileParser(PoetryParser): + + def __init__(self, poetry_lock_filename: str) -> None: + with open(poetry_lock_filename) as r: + super(PoetryFileParser, self).__init__(poetry_lock_contents=r.read()) + r.close() diff --git a/cyclonedx_py/parser/requirements.py b/cyclonedx_py/parser/requirements.py new file mode 100644 index 000000000..08a39fdb7 --- /dev/null +++ b/cyclonedx_py/parser/requirements.py @@ -0,0 +1,60 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +from cyclonedx.model.component import Component +from cyclonedx.parser import BaseParser, ParserWarning +from pkg_resources import parse_requirements as parse_requirements + + +class RequirementsParser(BaseParser): + + def __init__(self, requirements_content: str) -> None: + super().__init__() + + requirements = parse_requirements(requirements_content) + for requirement in requirements: + """ + @todo + Note that the below line will get the first (lowest) version specified in the Requirement and + ignore the operator (it might not be ==). This is passed to the Component. + + For example if a requirement was listed as: "PickyThing>1.6,<=1.9,!=1.8.6", we'll be interpreting this + as if it were written "PickyThing==1.6" + """ + try: + (op, version) = requirement.specs[0] + self._components.append(Component( + name=requirement.project_name, version=version + )) + except IndexError: + self._warnings.append( + ParserWarning( + item=requirement.project_name, + warning='Requirement \'{}\' does not have a pinned version and cannot be included in your ' + 'CycloneDX SBOM.'.format(requirement.project_name) + ) + ) + + +class RequirementsFileParser(RequirementsParser): + + def __init__(self, requirements_file: str) -> None: + with open(requirements_file) as r: + super(RequirementsFileParser, self).__init__(requirements_content=r.read()) + r.close() diff --git a/cyclonedx_py/utils/__init__.py b/cyclonedx_py/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cyclonedx_py/utils/conda.py b/cyclonedx_py/utils/conda.py new file mode 100644 index 000000000..37419676d --- /dev/null +++ b/cyclonedx_py/utils/conda.py @@ -0,0 +1,126 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +import json +import sys +from json import JSONDecodeError +from typing import Optional + +if sys.version_info >= (3, 8): + from typing import TypedDict +else: + from typing_extensions import TypedDict + +from urllib.parse import urlparse + + +class CondaPackage(TypedDict): + """ + Internal package for unifying Conda package definitions to. + """ + base_url: str + build_number: Optional[int] + build_string: str + channel: str + dist_name: str + name: str + platform: str + version: str + md5_hash: Optional[str] + + +def parse_conda_json_to_conda_package(conda_json_str: str) -> Optional[CondaPackage]: + try: + package_data = json.loads(conda_json_str) + except JSONDecodeError as e: + raise ValueError(f'Invalid JSON supplied - cannot be parsed: {conda_json_str}') from e + + if not isinstance(package_data, dict): + return None + + package_data.setdefault('md5_hash', None) + return CondaPackage(package_data) # type: ignore # @FIXME write proper type safe dict at this point + + +def parse_conda_list_str_to_conda_package(conda_list_str: str) -> Optional[CondaPackage]: + """ + Helper method for parsing a line of output from `conda list --explicit` into our internal `CondaPackage` object. + + Params: + conda_list_str: + Line of output from `conda list --explicit` + + Returns: + Instance of `CondaPackage` else `None`. + """ + + line = conda_list_str.strip() + + if line[0:1] == '#' or line[0:1] == '@' or len(line) == 0: + # Skip comments, @EXPLICT or empty lines + return None + + # Remove any hash + package_hash = None + if '#' in line: + hash_parts = line.split('#') + if len(hash_parts) > 1: + package_hash = hash_parts.pop() + line = ''.join(hash_parts) + + package_parts = line.split('/') + package_name_version_build_string = package_parts.pop() + package_arch = package_parts.pop() + package_url = urlparse('/'.join(package_parts)) + + try: + package_nvbs_parts = package_name_version_build_string.split('-') + build_number_with_opt_string = package_nvbs_parts.pop() + if '.' in build_number_with_opt_string: + # Remove any .conda at the end if present or other package type eg .tar.gz + pos = build_number_with_opt_string.find('.') + build_number_with_opt_string = build_number_with_opt_string[0:pos] + + build_string: str + build_number: Optional[int] + + if '_' in build_number_with_opt_string: + bnbs_parts = build_number_with_opt_string.split('_') + # Build number will be the last part - check if it's an integer + # Updated logic given https://github.com/CycloneDX/cyclonedx-python-lib/issues/65 + candidate_build_number: str = bnbs_parts.pop() + if candidate_build_number.isdigit(): + build_number = int(candidate_build_number) + build_string = build_number_with_opt_string + else: + build_number = None + build_string = build_number_with_opt_string + else: + build_string = '' + build_number = int(build_number_with_opt_string) + + build_version = package_nvbs_parts.pop() + package_name = '-'.join(package_nvbs_parts) + except IndexError as e: + raise ValueError(f'Error parsing {package_nvbs_parts} from {conda_list_str}') from e + + return CondaPackage( + base_url=package_url.geturl(), build_number=build_number, build_string=build_string, + channel=package_url.path[1:], dist_name=f'{package_name}-{build_version}-{build_string}', + name=package_name, platform=package_arch, version=build_version, md5_hash=package_hash + ) diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 000000000..e69de29bb diff --git a/tests/fixtures/conda-list-explicit-md5.txt b/tests/fixtures/conda-list-explicit-md5.txt new file mode 100644 index 000000000..4ac812846 --- /dev/null +++ b/tests/fixtures/conda-list-explicit-md5.txt @@ -0,0 +1,38 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: osx-64 +@EXPLICIT +https://repo.anaconda.com/pkgs/main/osx-64/ca-certificates-2021.7.5-hecd8cb5_1.conda#c2d0ae65c08dacdcf86770b7b5bbb187 +https://repo.anaconda.com/pkgs/main/osx-64/libcxx-10.0.0-1.conda#86574bfd5bcf4921237da41c07534cdc +https://repo.anaconda.com/pkgs/main/noarch/tzdata-2021a-h52ac0ba_0.conda#d42e4db918af84a470286e4c300604a3 +https://repo.anaconda.com/pkgs/main/osx-64/xz-5.2.5-h1de35cc_0.conda#f38610dab0f2b0cb05f1b31f354113c5 +https://repo.anaconda.com/pkgs/main/osx-64/yaml-0.2.5-haf1e3a3_0.conda#73628ed86f99adf6a0cb81dd20e426cd +https://repo.anaconda.com/pkgs/main/osx-64/zlib-1.2.11-h1de35cc_3.conda#67bb31afee816662edebfc3171360ccf +https://repo.anaconda.com/pkgs/main/osx-64/libffi-3.3-hb1e8313_2.conda#0c959d444ac65555cb836cdbd3e9a2d9 +https://repo.anaconda.com/pkgs/main/osx-64/ncurses-6.2-h0a44026_1.conda#649f497ed2ff2704749256d3532d144b +https://repo.anaconda.com/pkgs/main/osx-64/openssl-1.1.1k-h9ed2024_0.conda#2ecbfa7a9684bbaaa057c6dac778abc3 +https://repo.anaconda.com/pkgs/main/osx-64/tk-8.6.10-hb0a8c7a_0.conda#2f199f5862f5b000479408673eadb88d +https://repo.anaconda.com/pkgs/main/osx-64/readline-8.1-h9ed2024_0.conda#ce1a650fddb885c47ccdb28c90a2057a +https://repo.anaconda.com/pkgs/main/osx-64/sqlite-3.36.0-hce871da_0.conda#7e47a43e94a61ad1c7a5d910c5e970fd +https://repo.anaconda.com/pkgs/main/osx-64/python-3.9.5-h88f2d9e_3.conda#f10a9a3fd6b0936cc05f9e94e06a2d94 +https://repo.anaconda.com/pkgs/main/osx-64/certifi-2021.5.30-py39hecd8cb5_0.conda#48a9d5f197fbcef83387ca18e7216068 +https://repo.anaconda.com/pkgs/main/osx-64/chardet-4.0.0-py39hecd8cb5_1003.conda#535b448899dcf60a12dc683a90be5c0c +https://repo.anaconda.com/pkgs/main/noarch/idna-2.10-pyhd3eb1b0_0.tar.bz2#153ff132f593ea80aae2eea61a629c92 +https://repo.anaconda.com/pkgs/main/osx-64/pycosat-0.6.3-py39h9ed2024_0.conda#625905706ac243fae350f4dfc63e1b2d +https://repo.anaconda.com/pkgs/main/noarch/pycparser-2.20-py_2.conda#fcfeb621c6f895f3562ff01d9d6ce959 +https://repo.anaconda.com/pkgs/main/osx-64/pysocks-1.7.1-py39hecd8cb5_0.conda#4765ca1a39ea5287cbe170734ac83e37 +https://repo.anaconda.com/pkgs/main/osx-64/python.app-3-py39h9ed2024_0.conda#8a562918b61f71b3cac387cec842cbd2 +https://repo.anaconda.com/pkgs/main/osx-64/ruamel_yaml-0.15.100-py39h9ed2024_0.conda#d7abc88ed5b3a8d9e4af95b8a64d87fe +https://repo.anaconda.com/pkgs/main/noarch/six-1.16.0-pyhd3eb1b0_0.conda#529b369e1accc75b89f2d7e184a898d0 +https://repo.anaconda.com/pkgs/main/noarch/tqdm-4.61.2-pyhd3eb1b0_1.conda#f061c1548e813e7544d0017a71651e04 +https://repo.anaconda.com/pkgs/main/noarch/wheel-0.36.2-pyhd3eb1b0_0.conda#31029affc034e8c7203202417cebd157 +https://repo.anaconda.com/pkgs/main/osx-64/cffi-1.14.6-py39h2125817_0.conda#78ee9d3613c9f3f50a9eb4acd45bc747 +https://repo.anaconda.com/pkgs/main/osx-64/conda-package-handling-1.7.3-py39h9ed2024_1.conda#04a0be0e0f5bad8d8fc7ebe4803ea446 +https://repo.anaconda.com/pkgs/main/osx-64/setuptools-52.0.0-py39hecd8cb5_0.conda#5c9e48476978303d04650c21ee55f365 +https://repo.anaconda.com/pkgs/main/osx-64/brotlipy-0.7.0-py39h9ed2024_1003.conda#a08f6f5f899aff4a07351217b36fae41 +https://repo.anaconda.com/pkgs/main/osx-64/cryptography-3.4.7-py39h2fd3fbb_0.conda#822e8758f6e705c84b01480810eb24b6 +https://repo.anaconda.com/pkgs/main/osx-64/pip-21.1.3-py39hecd8cb5_0.conda#7bae540cbc7fdc9627b588c760b05e58 +https://repo.anaconda.com/pkgs/main/noarch/pyopenssl-20.0.1-pyhd3eb1b0_1.conda#ac62ddccf2e89f7a35867b2478a278af +https://repo.anaconda.com/pkgs/main/noarch/urllib3-1.26.6-pyhd3eb1b0_1.conda#5c72bc4a5a4fc0420b0e73b3acb1f52b +https://repo.anaconda.com/pkgs/main/noarch/requests-2.25.1-pyhd3eb1b0_0.conda#9d30b41b315403c7c74793b9b8d88580 +https://repo.anaconda.com/pkgs/main/osx-64/conda-4.10.3-py39hecd8cb5_0.tar.bz2#bc36833ee4a90c212e0695675bcfe120 diff --git a/tests/fixtures/conda-list-output.json b/tests/fixtures/conda-list-output.json new file mode 100644 index 000000000..a07399651 --- /dev/null +++ b/tests/fixtures/conda-list-output.json @@ -0,0 +1,342 @@ +[ + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 1003, + "build_string": "py39h9ed2024_1003", + "channel": "pkgs/main", + "dist_name": "brotlipy-0.7.0-py39h9ed2024_1003", + "name": "brotlipy", + "platform": "osx-64", + "version": "0.7.0" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 1, + "build_string": "hecd8cb5_1", + "channel": "pkgs/main", + "dist_name": "ca-certificates-2021.7.5-hecd8cb5_1", + "name": "ca-certificates", + "platform": "osx-64", + "version": "2021.7.5" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "py39hecd8cb5_0", + "channel": "pkgs/main", + "dist_name": "certifi-2021.5.30-py39hecd8cb5_0", + "name": "certifi", + "platform": "osx-64", + "version": "2021.5.30" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "py39h2125817_0", + "channel": "pkgs/main", + "dist_name": "cffi-1.14.6-py39h2125817_0", + "name": "cffi", + "platform": "osx-64", + "version": "1.14.6" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 1003, + "build_string": "py39hecd8cb5_1003", + "channel": "pkgs/main", + "dist_name": "chardet-4.0.0-py39hecd8cb5_1003", + "name": "chardet", + "platform": "osx-64", + "version": "4.0.0" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "py39hecd8cb5_0", + "channel": "pkgs/main", + "dist_name": "conda-4.10.3-py39hecd8cb5_0", + "name": "conda", + "platform": "osx-64", + "version": "4.10.3" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 1, + "build_string": "py39h9ed2024_1", + "channel": "pkgs/main", + "dist_name": "conda-package-handling-1.7.3-py39h9ed2024_1", + "name": "conda-package-handling", + "platform": "osx-64", + "version": "1.7.3" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "py39h2fd3fbb_0", + "channel": "pkgs/main", + "dist_name": "cryptography-3.4.7-py39h2fd3fbb_0", + "name": "cryptography", + "platform": "osx-64", + "version": "3.4.7" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "pyhd3eb1b0_0", + "channel": "pkgs/main", + "dist_name": "idna-2.10-pyhd3eb1b0_0", + "name": "idna", + "platform": "noarch", + "version": "2.10" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 1, + "build_string": "1", + "channel": "pkgs/main", + "dist_name": "libcxx-10.0.0-1", + "name": "libcxx", + "platform": "osx-64", + "version": "10.0.0" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 2, + "build_string": "hb1e8313_2", + "channel": "pkgs/main", + "dist_name": "libffi-3.3-hb1e8313_2", + "name": "libffi", + "platform": "osx-64", + "version": "3.3" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 1, + "build_string": "h0a44026_1", + "channel": "pkgs/main", + "dist_name": "ncurses-6.2-h0a44026_1", + "name": "ncurses", + "platform": "osx-64", + "version": "6.2" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "h9ed2024_0", + "channel": "pkgs/main", + "dist_name": "openssl-1.1.1k-h9ed2024_0", + "name": "openssl", + "platform": "osx-64", + "version": "1.1.1k" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "py39hecd8cb5_0", + "channel": "pkgs/main", + "dist_name": "pip-21.1.3-py39hecd8cb5_0", + "name": "pip", + "platform": "osx-64", + "version": "21.1.3" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "py39h9ed2024_0", + "channel": "pkgs/main", + "dist_name": "pycosat-0.6.3-py39h9ed2024_0", + "name": "pycosat", + "platform": "osx-64", + "version": "0.6.3" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 2, + "build_string": "py_2", + "channel": "pkgs/main", + "dist_name": "pycparser-2.20-py_2", + "name": "pycparser", + "platform": "noarch", + "version": "2.20" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 1, + "build_string": "pyhd3eb1b0_1", + "channel": "pkgs/main", + "dist_name": "pyopenssl-20.0.1-pyhd3eb1b0_1", + "name": "pyopenssl", + "platform": "noarch", + "version": "20.0.1" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "py39hecd8cb5_0", + "channel": "pkgs/main", + "dist_name": "pysocks-1.7.1-py39hecd8cb5_0", + "name": "pysocks", + "platform": "osx-64", + "version": "1.7.1" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 3, + "build_string": "h88f2d9e_3", + "channel": "pkgs/main", + "dist_name": "python-3.9.5-h88f2d9e_3", + "name": "python", + "platform": "osx-64", + "version": "3.9.5" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "py39h9ed2024_0", + "channel": "pkgs/main", + "dist_name": "python.app-3-py39h9ed2024_0", + "name": "python.app", + "platform": "osx-64", + "version": "3" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "h9ed2024_0", + "channel": "pkgs/main", + "dist_name": "readline-8.1-h9ed2024_0", + "name": "readline", + "platform": "osx-64", + "version": "8.1" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "pyhd3eb1b0_0", + "channel": "pkgs/main", + "dist_name": "requests-2.25.1-pyhd3eb1b0_0", + "name": "requests", + "platform": "noarch", + "version": "2.25.1" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "py39h9ed2024_0", + "channel": "pkgs/main", + "dist_name": "ruamel_yaml-0.15.100-py39h9ed2024_0", + "name": "ruamel_yaml", + "platform": "osx-64", + "version": "0.15.100" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "py39hecd8cb5_0", + "channel": "pkgs/main", + "dist_name": "setuptools-52.0.0-py39hecd8cb5_0", + "name": "setuptools", + "platform": "osx-64", + "version": "52.0.0" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "pyhd3eb1b0_0", + "channel": "pkgs/main", + "dist_name": "six-1.16.0-pyhd3eb1b0_0", + "name": "six", + "platform": "noarch", + "version": "1.16.0" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "hce871da_0", + "channel": "pkgs/main", + "dist_name": "sqlite-3.36.0-hce871da_0", + "name": "sqlite", + "platform": "osx-64", + "version": "3.36.0" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "hb0a8c7a_0", + "channel": "pkgs/main", + "dist_name": "tk-8.6.10-hb0a8c7a_0", + "name": "tk", + "platform": "osx-64", + "version": "8.6.10" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 1, + "build_string": "pyhd3eb1b0_1", + "channel": "pkgs/main", + "dist_name": "tqdm-4.61.2-pyhd3eb1b0_1", + "name": "tqdm", + "platform": "noarch", + "version": "4.61.2" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "h52ac0ba_0", + "channel": "pkgs/main", + "dist_name": "tzdata-2021a-h52ac0ba_0", + "name": "tzdata", + "platform": "noarch", + "version": "2021a" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 1, + "build_string": "pyhd3eb1b0_1", + "channel": "pkgs/main", + "dist_name": "urllib3-1.26.6-pyhd3eb1b0_1", + "name": "urllib3", + "platform": "noarch", + "version": "1.26.6" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "pyhd3eb1b0_0", + "channel": "pkgs/main", + "dist_name": "wheel-0.36.2-pyhd3eb1b0_0", + "name": "wheel", + "platform": "noarch", + "version": "0.36.2" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "h1de35cc_0", + "channel": "pkgs/main", + "dist_name": "xz-5.2.5-h1de35cc_0", + "name": "xz", + "platform": "osx-64", + "version": "5.2.5" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 0, + "build_string": "haf1e3a3_0", + "channel": "pkgs/main", + "dist_name": "yaml-0.2.5-haf1e3a3_0", + "name": "yaml", + "platform": "osx-64", + "version": "0.2.5" + }, + { + "base_url": "https://repo.anaconda.com/pkgs/main", + "build_number": 3, + "build_string": "h1de35cc_3", + "channel": "pkgs/main", + "dist_name": "zlib-1.2.11-h1de35cc_3", + "name": "zlib", + "platform": "osx-64", + "version": "1.2.11" + } +] diff --git a/tests/fixtures/pipfile-lock-no-index-example.txt b/tests/fixtures/pipfile-lock-no-index-example.txt new file mode 100644 index 000000000..f47b403a4 --- /dev/null +++ b/tests/fixtures/pipfile-lock-no-index-example.txt @@ -0,0 +1,37 @@ +{ + "_meta": { + "hash": { + "sha256": "8ca3da46acf801a7780c6781bed1d6b7012664226203447640cda114b13aa8aa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "anyio": { + "hashes": [ + "sha256:56ceaeed2877723578b1341f4f68c29081db189cfb40a97d1922b9513f6d7db6", + "sha256:8eccec339cb4a856c94a75d50fc1d451faf32a05ef406be462e2efc59c9838b0" + ], + "markers": "python_full_version >= '3.6.2'", + "version": "==3.3.3" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "index": "pypi", + "version": "==0.10.2" + } + }, + "develop": {} +} \ No newline at end of file diff --git a/tests/fixtures/pipfile-lock-simple.txt b/tests/fixtures/pipfile-lock-simple.txt new file mode 100644 index 000000000..c3700a41f --- /dev/null +++ b/tests/fixtures/pipfile-lock-simple.txt @@ -0,0 +1,29 @@ +{ + "_meta": { + "hash": { + "sha256": "8ca3da46acf801a7780c6781bed1d6b7012664226203447640cda114b13aa8aa" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.9" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "index": "pypi", + "version": "==0.10.2" + } + }, + "develop": {} +} diff --git a/tests/fixtures/poetry-lock-simple.txt b/tests/fixtures/poetry-lock-simple.txt new file mode 100644 index 000000000..4047665cf --- /dev/null +++ b/tests/fixtures/poetry-lock-simple.txt @@ -0,0 +1,18 @@ +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "3dc7af43729f7ff1e7cf84103a3e2c1945f233884eaa149c7e8d92cccb593984" + +[metadata.files] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] \ No newline at end of file diff --git a/tests/fixtures/requirements-example-1.txt b/tests/fixtures/requirements-example-1.txt new file mode 100644 index 000000000..ea71d69bb --- /dev/null +++ b/tests/fixtures/requirements-example-1.txt @@ -0,0 +1,3 @@ +packageurl-python>=0.9.4 +requirements_parser>=0.2.0 +setuptools>=50.3.2 \ No newline at end of file diff --git a/tests/fixtures/requirements-multilines-with-comments.txt b/tests/fixtures/requirements-multilines-with-comments.txt new file mode 100644 index 000000000..163e161f9 --- /dev/null +++ b/tests/fixtures/requirements-multilines-with-comments.txt @@ -0,0 +1,3 @@ +certifi==2021.5.30 \ + --hash=sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee +# via requests \ No newline at end of file diff --git a/tests/fixtures/requirements-with-comments.txt b/tests/fixtures/requirements-with-comments.txt new file mode 100644 index 000000000..2d9c4a1ae --- /dev/null +++ b/tests/fixtures/requirements-with-comments.txt @@ -0,0 +1,5 @@ +certifi==2021.5.30 # via requests +chardet==4.0.0 # via requests +idna==2.10 # via requests +requests==2.25.1 # via -r requirements.in +urllib3==1.26.5 # via requests \ No newline at end of file diff --git a/tests/fixtures/requirements-with-hashes.txt b/tests/fixtures/requirements-with-hashes.txt new file mode 100644 index 000000000..03ec11691 --- /dev/null +++ b/tests/fixtures/requirements-with-hashes.txt @@ -0,0 +1,5 @@ +certifi==2021.5.30 --hash=sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee --hash=sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8 # via requests +chardet==4.0.0 --hash=sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa --hash=sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5 # via requests +idna==2.10 --hash=sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6 --hash=sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0 # via requests +requests==2.25.1 --hash=sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804 --hash=sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e # via -r requirements.in +urllib3==1.26.5 --hash=sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c --hash=sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098 # via requests \ No newline at end of file diff --git a/tests/fixtures/requirements-without-pinned-versions.txt b/tests/fixtures/requirements-without-pinned-versions.txt new file mode 100644 index 000000000..b25c0778a --- /dev/null +++ b/tests/fixtures/requirements-without-pinned-versions.txt @@ -0,0 +1,5 @@ +certifi==2021.5.30 # via requests +chardet>=4.0.0 # via requests +idna +requests +urllib3 \ No newline at end of file diff --git a/tests/test_parser_conda.py b/tests/test_parser_conda.py new file mode 100644 index 000000000..5d6696072 --- /dev/null +++ b/tests/test_parser_conda.py @@ -0,0 +1,58 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +import os +from unittest import TestCase + +from cyclonedx.parser.conda import CondaListJsonParser, CondaListExplicitParser + + +class TestCondaParser(TestCase): + + def test_conda_list_json(self) -> None: + conda_list_ouptut_file = os.path.join(os.path.dirname(__file__), 'fixtures/conda-list-output.json') + + with (open(conda_list_ouptut_file, 'r')) as conda_list_ouptut_fh: + parser = CondaListJsonParser(conda_data=conda_list_ouptut_fh.read()) + conda_list_ouptut_fh.close() + + self.assertEqual(34, parser.component_count()) + components = parser.get_components() + + c_noarch = [x for x in components if x.name == 'idna'][0] + self.assertEqual('idna', c_noarch.name) + self.assertEqual('2.10', c_noarch.version) + self.assertEqual(1, len(c_noarch.external_references)) + self.assertEqual(0, len(c_noarch.external_references[0].get_hashes())) + + def test_conda_list_explicit_md5(self) -> None: + conda_list_ouptut_file = os.path.join(os.path.dirname(__file__), 'fixtures/conda-list-explicit-md5.txt') + + with (open(conda_list_ouptut_file, 'r')) as conda_list_ouptut_fh: + parser = CondaListExplicitParser(conda_data=conda_list_ouptut_fh.read()) + conda_list_ouptut_fh.close() + + self.assertEqual(34, parser.component_count()) + components = parser.get_components() + + c_noarch = [x for x in components if x.name == 'idna'][0] + self.assertEqual('idna', c_noarch.name) + self.assertEqual('2.10', c_noarch.version) + self.assertEqual(1, len(c_noarch.external_references)) + self.assertEqual(0, len(c_noarch.external_references[0].get_hashes())) diff --git a/tests/test_parser_environment.py b/tests/test_parser_environment.py new file mode 100644 index 000000000..e542253ab --- /dev/null +++ b/tests/test_parser_environment.py @@ -0,0 +1,41 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +from unittest import TestCase + +from cyclonedx.parser.environment import EnvironmentParser +from cyclonedx.model.component import Component + + +class TestEnvironmentParser(TestCase): + + def test_simple(self) -> None: + """ + @todo This test is a vague as it will detect the unique environment where tests are being executed - + so is this valid? + + :return: + """ + parser = EnvironmentParser() + self.assertGreater(parser.component_count(), 1) + + # We can only be sure that tox is in the environment, for example as we use tox to run tests + c_tox: Component = [x for x in parser.get_components() if x.name == 'tox'][0] + self.assertIsNotNone(c_tox.licenses) + self.assertEqual('MIT', c_tox.licenses[0].expression) diff --git a/tests/test_parser_pipenv.py b/tests/test_parser_pipenv.py new file mode 100644 index 000000000..fed5c210c --- /dev/null +++ b/tests/test_parser_pipenv.py @@ -0,0 +1,57 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +import os +from unittest import TestCase + +from cyclonedx.parser.pipenv import PipEnvFileParser + + +class TestPipEnvParser(TestCase): + + def test_simple(self) -> None: + tests_pipfile_lock = os.path.join(os.path.dirname(__file__), 'fixtures/pipfile-lock-simple.txt') + + parser = PipEnvFileParser(pipenv_lock_filename=tests_pipfile_lock) + self.assertEqual(1, parser.component_count()) + components = parser.get_components() + + self.assertEqual('toml', components[0].name) + self.assertEqual('0.10.2', components[0].version) + self.assertEqual(len(components[0].external_references), 2) + self.assertEqual(len(components[0].external_references[0].get_hashes()), 1) + + def test_with_multiple_and_no_index(self) -> None: + tests_pipfile_lock = os.path.join(os.path.dirname(__file__), 'fixtures/pipfile-lock-no-index-example.txt') + + parser = PipEnvFileParser(pipenv_lock_filename=tests_pipfile_lock) + self.assertEqual(2, parser.component_count()) + components = parser.get_components() + + c_anyio = [x for x in components if x.name == 'anyio'][0] + c_toml = [x for x in components if x.name == 'toml'][0] + + self.assertEqual('anyio', c_anyio.name) + self.assertEqual('3.3.3', c_anyio.version) + self.assertEqual(0, len(c_anyio.external_references)) + + self.assertEqual('toml', c_toml.name) + self.assertEqual('0.10.2', c_toml.version) + self.assertEqual(len(c_toml.external_references), 2) + self.assertEqual(len(c_toml.external_references[0].get_hashes()), 1) diff --git a/tests/test_parser_poetry.py b/tests/test_parser_poetry.py new file mode 100644 index 000000000..998ca6f72 --- /dev/null +++ b/tests/test_parser_poetry.py @@ -0,0 +1,36 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +import os +from unittest import TestCase + +from cyclonedx.parser.poetry import PoetryFileParser + + +class TestPoetryParser(TestCase): + + def test_simple(self) -> None: + tests_poetry_lock_file = os.path.join(os.path.dirname(__file__), 'fixtures/poetry-lock-simple.txt') + + parser = PoetryFileParser(poetry_lock_filename=tests_poetry_lock_file) + self.assertEqual(1, parser.component_count()) + components = parser.get_components() + self.assertEqual('toml', components[0].name) + self.assertEqual('0.10.2', components[0].version) + self.assertEqual(len(components[0].external_references), 2) diff --git a/tests/test_parser_requirements.py b/tests/test_parser_requirements.py new file mode 100644 index 000000000..bb306e51b --- /dev/null +++ b/tests/test_parser_requirements.py @@ -0,0 +1,83 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +import os +import unittest +from unittest import TestCase + +from cyclonedx.parser.requirements import RequirementsParser + + +class TestRequirementsParser(TestCase): + + def test_simple(self) -> None: + with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-simple.txt')) as r: + parser = RequirementsParser( + requirements_content=r.read() + ) + r.close() + self.assertTrue(1, parser.component_count()) + self.assertFalse(parser.has_warnings()) + + def test_example_1(self) -> None: + with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-example-1.txt')) as r: + parser = RequirementsParser( + requirements_content=r.read() + ) + r.close() + self.assertTrue(3, parser.component_count()) + self.assertFalse(parser.has_warnings()) + + def test_example_with_comments(self) -> None: + with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-with-comments.txt')) as r: + parser = RequirementsParser( + requirements_content=r.read() + ) + r.close() + self.assertTrue(5, parser.component_count()) + self.assertFalse(parser.has_warnings()) + + def test_example_multiline_with_comments(self) -> None: + with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-multilines-with-comments.txt')) as r: + parser = RequirementsParser( + requirements_content=r.read() + ) + r.close() + self.assertTrue(5, parser.component_count()) + self.assertFalse(parser.has_warnings()) + + @unittest.skip('Not yet supported') + def test_example_with_hashes(self) -> None: + with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-with-hashes.txt')) as r: + parser = RequirementsParser( + requirements_content=r.read() + ) + r.close() + self.assertTrue(5, parser.component_count()) + self.assertFalse(parser.has_warnings()) + + def test_example_without_pinned_versions(self) -> None: + with open(os.path.join(os.path.dirname(__file__), 'fixtures/requirements-without-pinned-versions.txt')) as r: + parser = RequirementsParser( + requirements_content=r.read() + ) + r.close() + self.assertTrue(2, parser.component_count()) + self.assertTrue(parser.has_warnings()) + self.assertEqual(3, len(parser.get_warnings())) diff --git a/tests/test_utils_conda.py b/tests/test_utils_conda.py new file mode 100644 index 000000000..7b0f05b75 --- /dev/null +++ b/tests/test_utils_conda.py @@ -0,0 +1,127 @@ +# encoding: utf-8 + +# This file is part of CycloneDX Python Lib +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. +from unittest import TestCase + +from cyclonedx_py.utils.conda import parse_conda_json_to_conda_package, parse_conda_list_str_to_conda_package, \ + CondaPackage + + +class TestUtilsConda(TestCase): + + def test_parse_conda_json_no_hash(self) -> None: + cp: CondaPackage = parse_conda_json_to_conda_package( + conda_json_str='{"base_url": "https://repo.anaconda.com/pkgs/main","build_number": 1003,"build_string": ' + '"py39hecd8cb5_1003","channel": "pkgs/main","dist_name": "chardet-4.0.0-py39hecd8cb5_1003",' + '"name": "chardet","platform": "osx-64","version": "4.0.0"}' + ) + + self.assertIsInstance(cp, dict) + self.assertEqual(cp['base_url'], 'https://repo.anaconda.com/pkgs/main') + self.assertEqual(cp['build_number'], 1003) + self.assertEqual(cp['build_string'], 'py39hecd8cb5_1003') + self.assertEqual(cp['channel'], 'pkgs/main') + self.assertEqual(cp['dist_name'], 'chardet-4.0.0-py39hecd8cb5_1003') + self.assertEqual(cp['name'], 'chardet') + self.assertEqual(cp['platform'], 'osx-64') + self.assertEqual(cp['version'], '4.0.0') + self.assertIsNone(cp['md5_hash']) + + def test_parse_conda_list_str_no_hash(self) -> None: + cp: CondaPackage = parse_conda_list_str_to_conda_package( + conda_list_str='https://repo.anaconda.com/pkgs/main/osx-64/chardet-4.0.0-py39hecd8cb5_1003.conda' + ) + + self.assertIsInstance(cp, dict) + self.assertEqual(cp['base_url'], 'https://repo.anaconda.com/pkgs/main') + self.assertEqual(cp['build_number'], 1003) + self.assertEqual(cp['build_string'], 'py39hecd8cb5_1003') + self.assertEqual(cp['channel'], 'pkgs/main') + self.assertEqual(cp['dist_name'], 'chardet-4.0.0-py39hecd8cb5_1003') + self.assertEqual(cp['name'], 'chardet') + self.assertEqual(cp['platform'], 'osx-64') + self.assertEqual(cp['version'], '4.0.0') + self.assertIsNone(cp['md5_hash']) + + def test_parse_conda_list_str_with_hash_1(self) -> None: + cp: CondaPackage = parse_conda_list_str_to_conda_package( + conda_list_str='https://repo.anaconda.com/pkgs/main/noarch/tzdata-2021a-h52ac0ba_0.conda' + '#d42e4db918af84a470286e4c300604a3' + ) + + self.assertIsInstance(cp, dict) + self.assertEqual(cp['base_url'], 'https://repo.anaconda.com/pkgs/main') + self.assertEqual(cp['build_number'], 0) + self.assertEqual(cp['build_string'], 'h52ac0ba_0') + self.assertEqual(cp['channel'], 'pkgs/main') + self.assertEqual(cp['dist_name'], 'tzdata-2021a-h52ac0ba_0') + self.assertEqual(cp['name'], 'tzdata') + self.assertEqual(cp['platform'], 'noarch') + self.assertEqual(cp['version'], '2021a') + self.assertEqual(cp['md5_hash'], 'd42e4db918af84a470286e4c300604a3') + + def test_parse_conda_list_str_with_hash_2(self) -> None: + cp: CondaPackage = parse_conda_list_str_to_conda_package( + conda_list_str='https://repo.anaconda.com/pkgs/main/osx-64/ca-certificates-2021.7.5-hecd8cb5_1.conda' + '#c2d0ae65c08dacdcf86770b7b5bbb187' + ) + + self.assertIsInstance(cp, dict) + self.assertEqual(cp['base_url'], 'https://repo.anaconda.com/pkgs/main') + self.assertEqual(cp['build_number'], 1) + self.assertEqual(cp['build_string'], 'hecd8cb5_1') + self.assertEqual(cp['channel'], 'pkgs/main') + self.assertEqual(cp['dist_name'], 'ca-certificates-2021.7.5-hecd8cb5_1') + self.assertEqual(cp['name'], 'ca-certificates') + self.assertEqual(cp['platform'], 'osx-64') + self.assertEqual(cp['version'], '2021.7.5') + self.assertEqual(cp['md5_hash'], 'c2d0ae65c08dacdcf86770b7b5bbb187') + + def test_parse_conda_list_str_with_hash_3(self) -> None: + cp: CondaPackage = parse_conda_list_str_to_conda_package( + conda_list_str='https://repo.anaconda.com/pkgs/main/noarch/idna-2.10-pyhd3eb1b0_0.tar.bz2' + '#153ff132f593ea80aae2eea61a629c92' + ) + + self.assertIsInstance(cp, dict) + self.assertEqual(cp['base_url'], 'https://repo.anaconda.com/pkgs/main') + self.assertEqual(cp['build_number'], 0) + self.assertEqual(cp['build_string'], 'pyhd3eb1b0_0') + self.assertEqual(cp['channel'], 'pkgs/main') + self.assertEqual(cp['dist_name'], 'idna-2.10-pyhd3eb1b0_0') + self.assertEqual(cp['name'], 'idna') + self.assertEqual(cp['platform'], 'noarch') + self.assertEqual(cp['version'], '2.10') + self.assertEqual(cp['md5_hash'], '153ff132f593ea80aae2eea61a629c92') + + def test_parse_conda_list_str_with_hash_4(self) -> None: + cp: CondaPackage = parse_conda_list_str_to_conda_package( + conda_list_str='https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2' + '#d7c89558ba9fa0495403155b64376d81' + ) + + self.assertIsInstance(cp, dict) + self.assertEqual(cp['base_url'], 'https://conda.anaconda.org/conda-forge') + self.assertIsNone(cp['build_number']) + self.assertEqual(cp['build_string'], 'conda_forge') + self.assertEqual(cp['channel'], 'conda-forge') + self.assertEqual(cp['dist_name'], '_libgcc_mutex-0.1-conda_forge') + self.assertEqual(cp['name'], '_libgcc_mutex') + self.assertEqual(cp['platform'], 'linux-64') + self.assertEqual(cp['version'], '0.1') + self.assertEqual(cp['md5_hash'], 'd7c89558ba9fa0495403155b64376d81')