Skip to content

Commit 6345593

Browse files
ecclesPaul Hewlett
authored andcommitted
Release a software package story
Problem: Example of release process using an SBOM. Solution: Added sbom entry to EVENTS_CREATE when releasing a software package using an SBOM. Signed-off-by: Paul Hewlett <phewlett76@gmail.com>
1 parent e62df38 commit 6345593

16 files changed

Lines changed: 879 additions & 16 deletions

File tree

archivist/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
SBOMS_METADATA = "metadata"
5353
SBOMS_PUBLISH = "publish"
5454
SBOMS_WITHDRAW = "withdraw"
55+
SBOM_RELEASE = "SBOM_RELEASE"
5556

5657
SUBJECTS_SUBPATH = "iam/v1"
5758
SUBJECTS_LABEL = "subjects"

archivist/events.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
2323
"""
2424

25+
from copy import deepcopy
2526
from logging import getLogger
2627
from typing import Dict, Optional
27-
from copy import deepcopy
2828

2929
# pylint:disable=unused-import # To prevent cyclical import errors forward referencing is used
3030
# pylint:disable=cyclic-import # but pylint doesn't understand this feature
@@ -36,6 +36,7 @@
3636
ASSETS_WILDCARD,
3737
CONFIRMATION_STATUS,
3838
EVENTS_LABEL,
39+
SBOM_RELEASE,
3940
)
4041
from . import confirmer
4142
from .dictmerge import _deepmerge
@@ -166,19 +167,38 @@ def create_from_data(self, asset_id: str, data: Dict, *, confirm=False) -> Event
166167
"""
167168
data = deepcopy(data)
168169

170+
event_attributes = data["event_attributes"]
169171
# is location present?
170172
location = data.pop("location", None)
171173
if location is not None:
172174
loc, _ = self._archivist.locations.create_if_not_exists(
173175
location,
174176
)
175-
data["event_attributes"]["arc_location_identity"] = loc["identity"]
177+
event_attributes["arc_location_identity"] = loc["identity"]
178+
179+
sbom = data.pop("sbom", None)
180+
if sbom is not None:
181+
sbom_result = self._archivist.sboms.create(sbom)
182+
for k, v in sbom_result.items():
183+
event_attributes[f"sbom_{k}"] = v
176184

177185
attachments = data.pop("attachments", None)
178186
if attachments is not None:
179-
data["event_attributes"]["arc_attachments"] = [
180-
self._archivist.attachments.create(a) for a in attachments
181-
]
187+
result = [self._archivist.attachments.create(a) for a in attachments]
188+
for i, a in enumerate(attachments):
189+
if a.get("type") == SBOM_RELEASE:
190+
sbom_result = self._archivist.sboms.parse(a)
191+
for k, v in sbom_result.items():
192+
event_attributes[f"sbom_{k}"] = v
193+
194+
event_attributes["sbom_identity"] = result[i][
195+
"arc_attachment_identity"
196+
]
197+
break
198+
199+
event_attributes["arc_attachments"] = result
200+
201+
data["event_attributes"] = event_attributes
182202

183203
event = Event(
184204
**self._archivist.post(

archivist/sboms.py

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,11 @@
2626

2727
from typing import BinaryIO, Dict, Optional
2828
from copy import deepcopy
29+
from io import BytesIO
2930
from logging import getLogger
3031

3132
from requests.models import Response
33+
from xmltodict import parse as xmltodict_parse
3234

3335
# pylint:disable=unused-import # To prevent cyclical import errors forward referencing is used
3436
# pylint:disable=cyclic-import # but pylint doesn't understand this feature
@@ -45,6 +47,7 @@
4547
from . import publisher, uploader, withdrawer
4648
from .dictmerge import _deepmerge
4749
from .sbommetadata import SBOM
50+
from .utils import get_url
4851

4952
LOGGER = getLogger(__name__)
5053

@@ -66,12 +69,147 @@ def __init__(self, archivist: "type_helper.Archivist"):
6669
def __str__(self) -> str:
6770
return f"SBOMSClient({self._archivist.url})"
6871

72+
@staticmethod
73+
def parse(data: Dict) -> Dict: # pragma: no cover
74+
"""
75+
parse the sbom and extract pertinent informtion
76+
77+
Args:
78+
data (dict): dictionary
79+
80+
A YAML representation of the data argument would be:
81+
82+
.. code-block:: yaml
83+
84+
filename: functests/test_resources/sboms/gen1.xml
85+
86+
OR
87+
88+
.. code-block:: yaml
89+
90+
url: https://some.hostname/cdx.xml
91+
92+
Either 'filename' or 'url' is required.
93+
94+
Returns:
95+
96+
A dict suitable for adding to an asset or event creation
97+
98+
"""
99+
result = None
100+
filename = data.get("filename")
101+
if filename is not None:
102+
with open(filename, "rb") as fd:
103+
sbom = xmltodict_parse(fd, xml_attribs=True, disable_entities=False)
104+
105+
else:
106+
url = data["url"]
107+
fd = BytesIO()
108+
get_url(url, fd)
109+
sbom = xmltodict_parse(fd, xml_attribs=True, disable_entities=False)
110+
111+
b = sbom["bom"]
112+
m = b["metadata"]
113+
c = m["component"]
114+
hash_value = c["hashes"]["hash"]["#text"]
115+
result = {
116+
"author": c["author"],
117+
"component": c["name"],
118+
"hash": hash_value,
119+
"supplier": c["supplier"]["name"],
120+
"uuid": b["@serialNumber"],
121+
"version": c["version"],
122+
}
123+
124+
return result
125+
126+
def create(self, data: Dict) -> Dict: # pragma: no cover
127+
"""
128+
Create an sbom and return struct suitable for use in an asset
129+
or event creation.
130+
131+
Args:
132+
data (dict): dictionary
133+
134+
A YAML representation of the data argument would be:
135+
136+
.. code-block:: yaml
137+
138+
filename: functests/test_resources/sboms/gen1.xml
139+
content_type: text/xml
140+
confirm: True
141+
params:
142+
privacy: PRIVATE
143+
144+
OR
145+
146+
.. code-block:: yaml
147+
148+
url: https://some.hostname/cdx.xml
149+
content_type: text/xml
150+
confirm: True
151+
params:
152+
privacy: PRIVATE
153+
154+
Either 'filename' or 'url' is required.
155+
'content_type' is required.
156+
157+
Returns:
158+
159+
A dict suitable for adding to an asset or event creation
160+
161+
A YAML representation of the result would be:
162+
163+
.. code-block:: yaml
164+
165+
arc_display_name: Acme Generation1 SBOM
166+
arc_attachment_identity: sboms/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
167+
.....
168+
169+
"""
170+
result = None
171+
filename = data.get("filename")
172+
if filename is not None:
173+
with open(filename, "rb") as fd:
174+
sbom = self.upload(
175+
fd,
176+
confirm=data.get("confirm", False),
177+
mtype=data.get("content_type"),
178+
params=data.get("params"),
179+
)
180+
181+
else:
182+
url = data["url"]
183+
fd = BytesIO()
184+
get_url(url, fd)
185+
sbom = self.upload(
186+
fd,
187+
confirm=data.get("confirm", False),
188+
mtype=data.get("content_type"),
189+
params=data.get("params"),
190+
)
191+
192+
# response to sbom upload contains all the info we need.
193+
s = sbom.dict()
194+
_, hash_value = s["hashes"][0].split(":")
195+
result = {
196+
"author": ",".join(s["authors"]),
197+
"component": s["component"],
198+
"identity": s["identity"],
199+
"hash": hash_value,
200+
"supplier": s["supplier"],
201+
"uuid": s["unique_id"],
202+
"version": s["version"],
203+
}
204+
205+
return result
206+
69207
def upload(
70208
self,
71209
fd: BinaryIO,
72210
*,
73211
confirm: bool = False,
74-
mtype: str = "text/xml",
212+
mtype: Optional[str] = None,
75213
params: Optional[Dict] = None,
76214
) -> SBOM:
77215
"""Create SBOM
@@ -89,6 +227,8 @@ def upload(
89227
90228
"""
91229

230+
mtype = mtype or "text/xml"
231+
92232
LOGGER.debug("Upload SBOM %s", params)
93233

94234
sbom = SBOM(

docs/runner/events_create.rst

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ added to the event before posting.
1212
The optional 'location' setting creates the location if it does not exist and adds it to
1313
the event.
1414

15+
The optional 'sbom' setting uploads the sbom to archivist and the response added to the
16+
event before posting. (see second example below)
17+
18+
An example when opening a door in Paris:
19+
1520
.. code-block:: yaml
1621
1722
---
@@ -51,3 +56,54 @@ the event.
5156
- filename: functests/test_resources/doors/events/door_open.png
5257
content_type: image/png
5358
confirm: true
59+
60+
61+
An example when releasing a software package as an sbom:
62+
63+
.. code-block:: yaml
64+
65+
---
66+
steps:
67+
- step:
68+
action: EVENTS_CREATE
69+
description: Release YYYYMMDD.1 of Test SBOM for YAML story
70+
asset_name: ACME Corporation Detector SAAS
71+
print_response: true
72+
operation: Record
73+
behaviour: RecordEvidence
74+
confirm: true
75+
event_attributes:
76+
arc_description: ACME Corporation Detector SAAS Released YYYYMMDD.1
77+
arc_display_type: sbom release
78+
sbom:
79+
filename: functests/test_resources/sbom/gen1.xml
80+
content_type: text/xml
81+
display_name: ACME Generation1 SBOM
82+
confirm: True
83+
params:
84+
privacy: PRIVATE
85+
86+
87+
An example when releasing a software package as an sbom attachment:
88+
89+
.. code-block:: yaml
90+
91+
---
92+
steps:
93+
- step:
94+
action: EVENTS_CREATE
95+
description: Release YYYYMMDD.1 of Test SBOM for YAML story
96+
asset_name: ACME Corporation Detector SAAS
97+
print_response: true
98+
operation: Record
99+
behaviour: RecordEvidence
100+
confirm: true
101+
event_attributes:
102+
arc_description: ACME Corporation Detector SAAS Released YYYYMMDD.1
103+
arc_display_type: sbom release
104+
attachments:
105+
- filename: functests/test_resources/sbom/gen1.xml
106+
content_type: text/xml
107+
display_name: ACME Generation1 SBOM
108+
type: SBOM_RELEASE
109+

0 commit comments

Comments
 (0)