Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make possible to parse metadata via parsers #300

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 3 additions & 174 deletions cyclonedx/model/bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,191 +17,19 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.
import warnings
from datetime import datetime, timezone
from typing import Iterable, Optional, Set
from uuid import UUID, uuid4

from sortedcontainers import SortedSet

from ..exception.model import UnknownComponentDependencyException
from ..parser import BaseParser
from . import ExternalReference, LicenseChoice, OrganizationalContact, OrganizationalEntity, Property, ThisTool, Tool
from . import ExternalReference
from .bom_meta import BomMetaData
from .component import Component
from .service import Service


class BomMetaData:
"""
This is our internal representation of the metadata complex type within the CycloneDX standard.

.. note::
See the CycloneDX Schema for Bom metadata: https://cyclonedx.org/docs/1.4/#type_metadata
"""

def __init__(self, *, tools: Optional[Iterable[Tool]] = None,
authors: Optional[Iterable[OrganizationalContact]] = None, component: Optional[Component] = None,
manufacture: Optional[OrganizationalEntity] = None,
supplier: Optional[OrganizationalEntity] = None,
licenses: Optional[Iterable[LicenseChoice]] = None,
properties: Optional[Iterable[Property]] = None) -> None:
self.timestamp = datetime.now(tz=timezone.utc)
self.tools = tools or [] # type: ignore
self.authors = authors or [] # type: ignore
self.component = component
self.manufacture = manufacture
self.supplier = supplier
self.licenses = licenses or [] # type: ignore
self.properties = properties or [] # type: ignore

if not tools:
self.tools.add(ThisTool)

@property
def timestamp(self) -> datetime:
"""
The date and time (in UTC) when this BomMetaData was created.

Returns:
`datetime` instance in UTC timezone
"""
return self._timestamp

@timestamp.setter
def timestamp(self, timestamp: datetime) -> None:
self._timestamp = timestamp

@property
def tools(self) -> "SortedSet[Tool]":
"""
Tools used to create this BOM.

Returns:
`Set` of `Tool` objects.
"""
return self._tools

@tools.setter
def tools(self, tools: Iterable[Tool]) -> None:
self._tools = SortedSet(tools)

@property
def authors(self) -> "SortedSet[OrganizationalContact]":
"""
The person(s) who created the BOM.

Authors are common in BOMs created through manual processes.

BOMs created through automated means may not have authors.

Returns:
Set of `OrganizationalContact`
"""
return self._authors

@authors.setter
def authors(self, authors: Iterable[OrganizationalContact]) -> None:
self._authors = SortedSet(authors)

@property
def component(self) -> Optional[Component]:
"""
The (optional) component that the BOM describes.

Returns:
`cyclonedx.model.component.Component` instance for this Bom Metadata.
"""
return self._component

@component.setter
def component(self, component: Component) -> None:
"""
The (optional) component that the BOM describes.

Args:
component
`cyclonedx.model.component.Component` instance to add to this Bom Metadata.

Returns:
None
"""
self._component = component

@property
def manufacture(self) -> Optional[OrganizationalEntity]:
"""
The organization that manufactured the component that the BOM describes.

Returns:
`OrganizationalEntity` if set else `None`
"""
return self._manufacture

@manufacture.setter
def manufacture(self, manufacture: Optional[OrganizationalEntity]) -> None:
self._manufacture = manufacture

@property
def supplier(self) -> Optional[OrganizationalEntity]:
"""
The organization that supplied the component that the BOM describes.

The supplier may often be the manufacturer, but may also be a distributor or repackager.

Returns:
`OrganizationalEntity` if set else `None`
"""
return self._supplier

@supplier.setter
def supplier(self, supplier: Optional[OrganizationalEntity]) -> None:
self._supplier = supplier

@property
def licenses(self) -> "SortedSet[LicenseChoice]":
"""
A optional list of statements about how this BOM is licensed.

Returns:
Set of `LicenseChoice`
"""
return self._licenses

@licenses.setter
def licenses(self, licenses: Iterable[LicenseChoice]) -> None:
self._licenses = SortedSet(licenses)

@property
def properties(self) -> "SortedSet[Property]":
"""
Provides the ability to document properties in a key/value store. This provides flexibility to include data not
officially supported in the standard without having to use additional namespaces or create extensions.

Property names of interest to the general public are encouraged to be registered in the CycloneDX Property
Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. Formal registration is OPTIONAL.

Return:
Set of `Property`
"""
return self._properties

@properties.setter
def properties(self, properties: Iterable[Property]) -> None:
self._properties = SortedSet(properties)

def __eq__(self, other: object) -> bool:
if isinstance(other, BomMetaData):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
self.timestamp, self.tools, self.component
))

def __repr__(self) -> str:
return f'<BomMetaData timestamp={self.timestamp.utcnow()}>'


class Bom:
"""
This is our internal representation of a bill-of-materials (BOM).
Expand All @@ -226,6 +54,7 @@ def from_parser(parser: BaseParser) -> 'Bom':
"""
bom = Bom()
bom.components.update(parser.get_components())
bom.metadata.update(parser.get_metadata())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


since this seams to be the only relevant change here, please add proper unit tests

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure will add, but as for me, it looks like it's should be covered by existing tests.

return bom

def __init__(self, *, components: Optional[Iterable[Component]] = None,
Expand Down
187 changes: 187 additions & 0 deletions cyclonedx/model/bom_meta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
from datetime import datetime, timezone
from typing import Iterable, Optional

from sortedcontainers import SortedSet

from cyclonedx.model import LicenseChoice, OrganizationalContact, OrganizationalEntity, Property, ThisTool, Tool
from cyclonedx.model.component import Component


class BomMetaData:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


this looks like it was refactored to an own file/module. what is the purpose?
why was it needed to be moved? whats the benefit of yet another module? are there any other changes?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi this is due to dependency recursion while type check:
cyclonedx/model/bom.py

from ..parser import BaseParser

cyclonedx/parser/init.py

from ..model.bom_meta import BomMetaData

"""
This is our internal representation of the metadata complex type within the CycloneDX standard.

.. note::
See the CycloneDX Schema for Bom metadata: https://cyclonedx.org/docs/1.4/#type_metadata
"""

def __init__(self, *, tools: Optional[Iterable[Tool]] = None,
authors: Optional[Iterable[OrganizationalContact]] = None, component: Optional[Component] = None,
manufacture: Optional[OrganizationalEntity] = None,
supplier: Optional[OrganizationalEntity] = None,
licenses: Optional[Iterable[LicenseChoice]] = None,
properties: Optional[Iterable[Property]] = None) -> None:
self.timestamp = datetime.now(tz=timezone.utc)
self.tools = tools or [] # type: ignore
self.authors = authors or [] # type: ignore
self.component = component
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incompatible attribute type: Attribute component declared in class BomMetaData has type Component but is used as type Optional[Component].


ℹ️ Learn about @sonatype-lift commands

You can reply with the following commands. For example, reply with @sonatype-lift ignoreall to leave out all findings.

Command Usage
@sonatype-lift ignore Leave out the above finding from this PR
@sonatype-lift ignoreall Leave out all the existing findings from this PR
@sonatype-lift exclude <file|issue|path|tool> Exclude specified file|issue|path|tool from Lift findings by updating your config.toml file

Note: When talking to LiftBot, you need to refresh the page to see its response.
Click here to add LiftBot to another repo.


Was this a good recommendation?
[ 🙁 Not relevant ] - [ 😕 Won't fix ] - [ 😑 Not critical, will fix ] - [ 🙂 Critical, will fix ] - [ 😊 Critical, fixing now ]

self.manufacture = manufacture
self.supplier = supplier
self.licenses = licenses or [] # type: ignore
self.properties = properties or [] # type: ignore

if not tools:
self.tools.add(ThisTool)

@property
def timestamp(self) -> datetime:
"""
The date and time (in UTC) when this BomMetaData was created.

Returns:
`datetime` instance in UTC timezone
"""
return self._timestamp

@timestamp.setter
def timestamp(self, timestamp: datetime) -> None:
self._timestamp = timestamp

@property
def tools(self) -> "SortedSet[Tool]":
"""
Tools used to create this BOM.

Returns:
`Set` of `Tool` objects.
"""
return self._tools

@tools.setter
def tools(self, tools: Iterable[Tool]) -> None:
self._tools = SortedSet(tools)

@property
def authors(self) -> "SortedSet[OrganizationalContact]":
"""
The person(s) who created the BOM.

Authors are common in BOMs created through manual processes.

BOMs created through automated means may not have authors.

Returns:
Set of `OrganizationalContact`
"""
return self._authors

@authors.setter
def authors(self, authors: Iterable[OrganizationalContact]) -> None:
self._authors = SortedSet(authors)

@property
def component(self) -> Optional[Component]:
"""
The (optional) component that the BOM describes.

Returns:
`cyclonedx.model.component.Component` instance for this Bom Metadata.
"""
return self._component

@component.setter
def component(self, component: Component) -> None:
"""
The (optional) component that the BOM describes.

Args:
component
`cyclonedx.model.component.Component` instance to add to this Bom Metadata.

Returns:
None
"""
self._component = component

@property
def manufacture(self) -> Optional[OrganizationalEntity]:
"""
The organization that manufactured the component that the BOM describes.

Returns:
`OrganizationalEntity` if set else `None`
"""
return self._manufacture

@manufacture.setter
def manufacture(self, manufacture: Optional[OrganizationalEntity]) -> None:
self._manufacture = manufacture

@property
def supplier(self) -> Optional[OrganizationalEntity]:
"""
The organization that supplied the component that the BOM describes.

The supplier may often be the manufacturer, but may also be a distributor or repackager.

Returns:
`OrganizationalEntity` if set else `None`
"""
return self._supplier

@supplier.setter
def supplier(self, supplier: Optional[OrganizationalEntity]) -> None:
self._supplier = supplier

@property
def licenses(self) -> "SortedSet[LicenseChoice]":
"""
A optional list of statements about how this BOM is licensed.

Returns:
Set of `LicenseChoice`
"""
return self._licenses

@licenses.setter
def licenses(self, licenses: Iterable[LicenseChoice]) -> None:
self._licenses = SortedSet(licenses)

@property
def properties(self) -> "SortedSet[Property]":
"""
Provides the ability to document properties in a key/value store. This provides flexibility to include data not
officially supported in the standard without having to use additional namespaces or create extensions.

Property names of interest to the general public are encouraged to be registered in the CycloneDX Property
Taxonomy - https://github.com/CycloneDX/cyclonedx-property-taxonomy. Formal registration is OPTIONAL.

Return:
Set of `Property`
"""
return self._properties

@properties.setter
def properties(self, properties: Iterable[Property]) -> None:
self._properties = SortedSet(properties)

def __eq__(self, other: object) -> bool:
if isinstance(other, BomMetaData):
return hash(other) == hash(self)
return False

def __hash__(self) -> int:
return hash((
self.timestamp, self.tools, self.component
))

def __repr__(self) -> str:
return f'<BomMetaData timestamp={self.timestamp.utcnow()}>'

def update(self, new_metadata: Optional["BomMetaData"]) -> None:
if not new_metadata or self == new_metadata:
return

# TODO Add support of whole metadata and define update policy
if new_metadata.component:
self.component = new_metadata.component
Loading