Skip to content

Commit

Permalink
Basic structure without any output generation available (very basic C…
Browse files Browse the repository at this point in the history
…omponent definition).
  • Loading branch information
madpah committed Aug 27, 2021
1 parent 1def201 commit 6ac5dc2
Show file tree
Hide file tree
Showing 16 changed files with 281 additions and 0 deletions.
36 changes: 36 additions & 0 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import List
from .cyclonedx import Component
from ..parser import BaseParser


class Bom:
"""
This is our internal representation of the BOM.
We can pass a BOM instance to a Generator to produce CycloneDX in the required format and according
to the requested schema version.
"""

_components: List[Component] = []

@staticmethod
def from_parser(parser: BaseParser):
bom = Bom()
bom.add_components(parser.get_components())
return bom

def __init__(self):
self._components.clear()

def add_component(self, component: Component):
self._components.add(component)

def add_components(self, components: List[Component]):
self._components = self._components + components

def component_count(self) -> int:
return len(self._components)

def has_component(self, component: Component) -> bool:
print("Checking if {} is contained within {}".format(component, self._components))
return component in self._components
46 changes: 46 additions & 0 deletions cyclonedx/model/cyclonedx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from enum import Enum

PURL_TYPE_PREFIX = 'pypi'


class ComponentType(Enum):
"""
Enum object that defines the permissable 'types' for a Component according to the CycloneDX
schemas.
"""
APPLICATION = 'application'
CONTAINER = 'container'
DEVICE = 'device'
FILE = 'file'
FIRMWARE = 'firmware'
FRAMEWORK = 'framework'
LIBRARY = 'library'
OPERATING_SYSTEM = 'operating-system'


class Component:
"""
An object that mirrors the Component type in the CycloneDX schema.
"""
_type: ComponentType
_name: str
_version: str
_qualifiers: str

def __init__(self, name: str, version: str, qualifiers: str = None, type: ComponentType = ComponentType.LIBRARY):
self._name = name
self._version = version
self._type = type
self._qualifiers = qualifiers

def get_purl(self) -> str:
base_purl = 'pkg:{}/{}@{}'.format(PURL_TYPE_PREFIX, self._name, self._version)
if self._qualifiers:
base_purl = '{}?{}'.format(base_purl, self._qualifiers)
return base_purl

def __eq__(self, other):
return other.get_purl() == self.get_purl()

def __repr__(self):
return '<Component {}={}>'.format(self._name, self._version)
Empty file added cyclonedx/output/__init__.py
Empty file.
9 changes: 9 additions & 0 deletions cyclonedx/output/xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from cyclonedx.model.bom import Bom


class Xml:

_bom: Bom

def __init__(self, bom: Bom):
self._bom = bom
14 changes: 14 additions & 0 deletions cyclonedx/parser/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from abc import ABC
from typing import List

from ..model.cyclonedx import Component


class BaseParser(ABC):
_components: List[Component] = []

def component_count(self) -> int:
return len(self._components)

def get_components(self) -> List[Component]:
return self._components
16 changes: 16 additions & 0 deletions cyclonedx/parser/environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from . import BaseParser

from ..model.cyclonedx import Component


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):
import pkg_resources
for i in iter(pkg_resources.working_set):
self._components.append(Component(name=i.project_name, version=i.version, type='pypi'))
32 changes: 32 additions & 0 deletions cyclonedx/parser/requirements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pkg_resources

from . import BaseParser

from ..model.cyclonedx import Component


class RequirementsParser(BaseParser):

def __init__(self, requirements_content: str):
requirements = pkg_resources.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 interpretting this
as if it were written "PickyThing==1.6"
"""
(op, version) = requirement.specs[0]
self._components.append(Component(
name=requirement.project_name, version=version
))


class RequirementsFileParser(RequirementsParser):

def __init__(self, requirements_file: str):
with open(requirements_file) as r:
super(RequirementsFileParser, self).__init__(requirements_content=r.read())
r.close()
2 changes: 2 additions & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
packageurl-python==0.9.4
requirements_parser==0.2.0
setuptools>=50.3.2
tox==3.24.3
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
packageurl-python>=0.9.4
requirements_parser>=0.2.0
setuptools>=50.3.2
Empty file added tests/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions tests/fixtures/requirements-example-1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
packageurl-python>=0.9.4
requirements_parser>=0.2.0
setuptools>=50.3.2
1 change: 1 addition & 0 deletions tests/fixtures/requirements-simple.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
setuptools==50.3.2
21 changes: 21 additions & 0 deletions tests/test_bom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from unittest import TestCase

import os

from cyclonedx.model.bom import Bom
from cyclonedx.model.cyclonedx import Component
from cyclonedx.parser.requirements import RequirementsFileParser


class TestBom(TestCase):

def test_bom_simple(self):
parser = RequirementsFileParser(
requirements_file=os.path.join(os.path.dirname(__file__), 'fixtures/requirements-simple.txt')
)
bom = Bom.from_parser(parser=parser)

self.assertEqual(bom.component_count(), 1)
self.assertTrue(bom.has_component(
Component(name='setuptools', version='50.3.2')
))
60 changes: 60 additions & 0 deletions tests/test_component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from unittest import TestCase

from cyclonedx.model.cyclonedx import Component
from packageurl import PackageURL


class TestComponent(TestCase):
_component: Component

@classmethod
def setUpClass(cls) -> None:
cls._component = Component(name='setuptools', version='50.3.2').get_purl()
cls._component_with_qualifiers = Component(name='setuptools', version='50.3.2',
qualifiers='extension=tar.gz').get_purl()

def test_purl_correct(self):
self.assertEqual(
str(PackageURL(
type='pypi', name='setuptools', version='50.3.2'
)),
TestComponent._component
)

def test_purl_incorrect_version(self):
purl = PackageURL(
type='pypi', name='setuptools', version='50.3.1'
)
self.assertNotEqual(
str(purl),
TestComponent._component
)
self.assertEqual(purl.type, 'pypi')
self.assertEqual(purl.name, 'setuptools')
self.assertEqual(purl.version, '50.3.1')

def test_purl_incorrect_name(self):
purl = PackageURL(
type='pypi', name='setuptoolz', version='50.3.2'
)
self.assertNotEqual(
str(purl),
TestComponent._component
)
self.assertEqual(purl.type, 'pypi')
self.assertEqual(purl.name, 'setuptoolz')
self.assertEqual(purl.version, '50.3.2')

def test_purl_with_qualifiers(self):
purl = PackageURL(
type='pypi', name='setuptools', version='50.3.2', qualifiers='extension=tar.gz'
)
self.assertEqual(
str(purl),
TestComponent._component_with_qualifiers
)
self.assertNotEqual(
str(purl),
TestComponent._component
)
self.assertEqual(purl.qualifiers, {'extension': 'tar.gz'})
16 changes: 16 additions & 0 deletions tests/test_parser_environment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from unittest import TestCase

from cyclonedx.parser.environment import EnvironmentParser


class TestRequirementsParser(TestCase):

def test_simple(self):
"""
@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)
23 changes: 23 additions & 0 deletions tests/test_parser_requirements.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import os
from unittest import TestCase

from cyclonedx.parser.requirements import RequirementsParser


class TestRequirementsParser(TestCase):

def test_simple(self):
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())

def test_example_1(self):
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())

0 comments on commit 6ac5dc2

Please sign in to comment.