Repository: CycloneDX/cyclonedx-python

Issue:
[CONDA] Include component hashes when generating SBoM from Conda Explicit MD5 List
* What I did
```
# Create Conda environment with openssl included
conda create -y --name openssl openssl=1.1.1o

# Create an explicit MD5 list
conda list -n openssl --explicit --md5 > openssl-1.1.1_explict_md5_spec.txt

# Create a SBoM from the environment
cyclonedx-py -c -i ./openssl-1.1.1_explict_md5_spec.txt --format json --schema-version 1.3 -o ./openssl-1.1.1_conda_explicit_md5_cyclonedx_1.3_sbom.json
```

* Resulting explicit MD5 list
```
$ cat openssl-3.0.3_explict_md5_spec.txt 
# This file may be used to create an environment using:
# $ conda create --name <env> --file <this file>
# platform: linux-64
@EXPLICIT
https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81
https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2021.10.8-ha878542_0.tar.bz2#575611b8a84f45960e87722eeb51fa26
https://conda.anaconda.org/conda-forge/linux-64/libgomp-12.1.0-h8d9b700_16.tar.bz2#f013cf7749536ce43d82afbffdf499ab
https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2#73aaf86a425cc6e73fcf236a5a46396d
https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-12.1.0-h8d9b700_16.tar.bz2#4f05bc9844f7c101e6e147dab3c88d5c
https://conda.anaconda.org/conda-forge/linux-64/openssl-3.0.3-h166bdaf_0.tar.bz2#f4c4e71d7cf611b513170eaa0852bf1d
```


* Resulting SBoM's first component in components, all other components include similar information
```
"components": [
    {
      "type": "library",
      "bom-ref": "999ea8df-ef27-47db-a8fd-a305112a051d",
      "name": "_libgcc_mutex",
      "version": "0.1",
      "purl": "pkg:pypi/-libgcc-mutex@0.1",
      "externalReferences": [
        {
          "url": "https://repo.anaconda.com/pkgs/main",
          "comment": "Distribution name _libgcc_mutex-0.1-main",
          "type": "distribution"
        }
      ]
    },
```

* Requesting the package MD5 be added to the component.hashes entry in components
    * ref https://cyclonedx.org/docs/1.3/json/#metadata_component_hashes_items_content
    * You can get the package MD5 from the explicit MD5 list, it is after the hashtag


Discussion:

Patch:
diff --git a/cyclonedx_py/parser/conda.py b/cyclonedx_py/parser/conda.py
index 59fc527..1c9197b 100644
--- a/cyclonedx_py/parser/conda.py
+++ b/cyclonedx_py/parser/conda.py
@@ -21,7 +21,7 @@ import json
 from abc import ABCMeta, abstractmethod
 from typing import List
 
-from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri
+from cyclonedx.model import ExternalReference, ExternalReferenceType, HashAlgorithm, HashType, XsUri
 from cyclonedx.model.component import Component
 from cyclonedx.parser import BaseParser
 
@@ -71,6 +71,11 @@ class _BaseCondaParser(BaseParser, metaclass=ABCMeta):
                 url=XsUri(conda_package['base_url']),
                 comment=f"Distribution name {conda_package['dist_name']}"
             ))
+            if conda_package['md5_hash'] is not None:
+                c.hashes.add(HashType(
+                    algorithm=HashAlgorithm.MD5,
+                    hash_value=str(conda_package['md5_hash'])
+                ))
 
             self._components.append(c)
 


Test patch:
diff --git a/tests/test_parser_conda.py b/tests/test_parser_conda.py
index cf6c6a5..ece9aec 100644
--- a/tests/test_parser_conda.py
+++ b/tests/test_parser_conda.py
@@ -21,6 +21,8 @@ import os
 import re
 from unittest import TestCase
 
+from cyclonedx.model import HashAlgorithm, HashType
+
 from cyclonedx_py.parser.conda import CondaListExplicitParser, CondaListJsonParser
 
 
@@ -42,6 +44,7 @@ class TestCondaParser(TestCase):
         self.assertEqual('2.10', c_idna.version)
         self.assertEqual(1, len(c_idna.external_references), f'{c_idna.external_references}')
         self.assertEqual(0, len(c_idna.external_references.pop().hashes))
+        self.assertEqual(0, len(c_idna.hashes), f'{c_idna.hashes}')
 
     def test_conda_list_explicit_md5(self) -> None:
         conda_list_ouptut_file = os.path.join(os.path.dirname(__file__),
@@ -59,6 +62,10 @@ class TestCondaParser(TestCase):
         self.assertEqual('2.10', c_idna.version)
         self.assertEqual(1, len(c_idna.external_references), f'{c_idna.external_references}')
         self.assertEqual(0, len(c_idna.external_references.pop().hashes))
+        self.assertEqual(1, len(c_idna.hashes), f'{c_idna.hashes}')
+        hash: HashType = c_idna.hashes.pop()
+        self.assertEqual(HashAlgorithm.MD5, hash.alg)
+        self.assertEqual('153ff132f593ea80aae2eea61a629c92', hash.content)
 
     def test_conda_list_build_number_text(self) -> None:
         conda_list_output_file = os.path.join(os.path.dirname(__file__), 'fixtures/conda-list-build-number-text.txt')
@@ -73,14 +80,17 @@ class TestCondaParser(TestCase):
         self.assertIsNotNone(c_libgcc_mutex)
         self.assertEqual('_libgcc_mutex', c_libgcc_mutex.name)
         self.assertEqual('0.1', c_libgcc_mutex.version)
+        self.assertEqual(0, len(c_libgcc_mutex.hashes), f'{c_libgcc_mutex.hashes}')
         c_pycparser = next(filter(lambda c: c.name == 'pycparser', components), None)
         self.assertIsNotNone(c_pycparser)
         self.assertEqual('pycparser', c_pycparser.name)
         self.assertEqual('2.21', c_pycparser.version)
+        self.assertEqual(0, len(c_pycparser.hashes), f'{c_pycparser.hashes}')
         c_openmp_mutex = next(filter(lambda c: c.name == '_openmp_mutex', components), None)
         self.assertIsNotNone(c_openmp_mutex)
         self.assertEqual('_openmp_mutex', c_openmp_mutex.name)
         self.assertEqual('4.5', c_openmp_mutex.version)
+        self.assertEqual(0, len(c_openmp_mutex.hashes), f'{c_openmp_mutex.hashes}')
 
     def test_conda_list_malformed(self) -> None:
         conda_list_output_file = os.path.join(os.path.dirname(__file__), 'fixtures/conda-list-broken.txt')