Skip to content

Commit ddfa863

Browse files
committed
feat(project CreateBom): write control file with attachment details
This can be used to check meta data of attachments before feeding the list into "bom downloadAttachments".
1 parent f251258 commit ddfa863

4 files changed

Lines changed: 192 additions & 26 deletions

File tree

capycli/main/options.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,13 @@ def register_options(self):
210210
help="create an mapping overview JSON file",
211211
)
212212

213+
self.parser.add_argument(
214+
"-ct",
215+
"--controlfile",
216+
dest="controlfile",
217+
help="control file for \"bom DownloadAttachments\" and \"project CreateReadme\"",
218+
)
219+
213220
self.parser.add_argument(
214221
"-mr",
215222
"--mapresult",

capycli/project/create_bom.py

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import logging
1010
import sys
11+
import json
12+
from typing import Tuple, Dict
1113

1214
import sw360
1315
from cyclonedx.model import ExternalReferenceType, HashAlgorithm
@@ -18,6 +20,7 @@
1820
from capycli import get_logger
1921
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomCreator
2022
from capycli.common.purl_utils import PurlUtils
23+
from capycli.common.script_support import ScriptSupport
2124
from capycli.common.print import print_red, print_text, print_yellow
2225
from capycli.main.result_codes import ResultCode
2326

@@ -43,8 +46,9 @@ def get_clearing_state(self, proj, href) -> str:
4346

4447
return None
4548

46-
def create_project_bom(self, project) -> list:
49+
def create_project_bom(self, project, create_controlfile) -> Tuple[list, list]:
4750
bom = []
51+
details = []
4852

4953
releases = project["_embedded"].get("sw360:releases", [])
5054
releases.sort(key=lambda s: s["name"].lower())
@@ -91,7 +95,6 @@ def create_project_bom(self, project) -> list:
9195
if "repository" in release_details and "url" in release_details["repository"]:
9296
CycloneDxSupport.set_ext_ref(rel_item, ExternalReferenceType.VCS, comment=None,
9397
value=release_details["repository"]["url"])
94-
9598
attachments = self.get_release_attachments(release_details)
9699
for attachment in attachments:
97100
at_type = attachment["attachmentType"]
@@ -102,8 +105,23 @@ def create_project_bom(self, project) -> list:
102105
ext_ref_type = ExternalReferenceType.DISTRIBUTION
103106
else:
104107
ext_ref_type = ExternalReferenceType.OTHER
105-
comment += (", sw360Id: "
106-
+ self.client.get_id_from_href(attachment["_links"]["self"]["href"]))
108+
if create_controlfile:
109+
at_data = self.client.get_attachment_by_url(attachment["_links"]["self"]["href"])
110+
111+
at_details = {
112+
"ComponentName": " ".join((release["name"], release["version"])),
113+
"Sw360Id": sw360_id,
114+
"Sw360AttachmentId": self.client.get_id_from_href(attachment["_links"]["self"]["href"])}
115+
for key in ("createdBy", "createdTeam", "createdOn", "createdComment", "checkStatus",
116+
"checkedBy", "checkedTeam", "checkedOn", "checkedComment"):
117+
if key in at_data and at_data[key]:
118+
at_details[key[0].upper() + key[1:]] = at_data[key]
119+
120+
if at_type == "COMPONENT_LICENSE_INFO_XML":
121+
at_details["CliFile"] = attachment["filename"]
122+
elif at_type == "CLEARING_REPORT":
123+
at_details["ReportFile"] = attachment["filename"]
124+
details.append(at_details)
107125
CycloneDxSupport.set_ext_ref(rel_item, ext_ref_type,
108126
comment, attachment["filename"],
109127
HashAlgorithm.SHA_1, attachment.get("sha1"))
@@ -127,9 +145,9 @@ def create_project_bom(self, project) -> list:
127145

128146
# sub-projects are not handled at the moment
129147

130-
return bom
148+
return bom, details
131149

132-
def create_project_cdx_bom(self, project_id) -> Bom:
150+
def create_project_cdx_bom(self, project_id, create_controlfile) -> Tuple[Bom, Dict]:
133151
try:
134152
project = self.client.get_project(project_id)
135153
except sw360.sw360_api.SW360Error as swex:
@@ -138,14 +156,19 @@ def create_project_cdx_bom(self, project_id) -> Bom:
138156

139157
print_text(" Project name: " + project["name"] + ", " + project["version"])
140158

141-
cdx_components = self.create_project_bom(project)
159+
cdx_components, control_components = self.create_project_bom(project, create_controlfile)
142160

143161
creator = SbomCreator()
144162
sbom = creator.create(cdx_components, addlicense=True, addprofile=True, addtools=True,
145163
name=project.get("name"), version=project.get("version"),
146164
description=project.get("description"), addprojectdependencies=True)
147165

148-
return sbom
166+
controlfile = {
167+
"ProjectName": ScriptSupport.get_full_name_from_dict(project, "name", "version"),
168+
"Components": control_components
169+
}
170+
171+
return sbom, controlfile
149172

150173
def show_command_help(self):
151174
print("\nusage: CaPyCli project createbom [options]")
@@ -158,6 +181,7 @@ def show_command_help(self):
158181
-name name of the project, component or release
159182
-version version of the project, component or release
160183
-o OUTPUTFILE output file to write to
184+
-ct CONTROLFILE write control file for "bom DownloadAttachments" and "project CreateReadme"
161185
""")
162186

163187
print()
@@ -204,7 +228,11 @@ def run(self, args):
204228
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
205229

206230
if pid:
207-
bom = self.create_project_cdx_bom(pid)
231+
bom, controlfile = self.create_project_cdx_bom(pid, args.controlfile)
208232
CaPyCliBom.write_sbom(bom, args.outputfile)
233+
234+
if args.controlfile:
235+
with open(args.controlfile, "w") as outfile:
236+
json.dump(controlfile, outfile, indent=2)
209237
else:
210238
print_yellow(" No matching project found")

tests/fixtures/sbom_for_download.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
},
9292
{
9393
"url": "CLIXML_certifi-2022.12.7.xml",
94-
"comment": "component license information (local copy), sw360Id: 794446",
94+
"comment": "component license information (local copy)",
9595
"type": "other",
9696
"hashes": [
9797
{
@@ -102,7 +102,7 @@
102102
},
103103
{
104104
"url": "certifi-2022.12.7_clearing_report.docx",
105-
"comment": "clearing report (local copy), sw360Id: 63b368",
105+
"comment": "clearing report (local copy)",
106106
"type": "other",
107107
"hashes": [
108108
{

tests/test_create_bom.py

Lines changed: 146 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def test_project_not_found(self) -> None:
102102
args.verbose = True
103103
args.id = "34ef5c5452014c52aa9ce4bc180624d8"
104104
args.outputfile = self.OUTPUTFILE
105+
args.controlfile = None
105106

106107
self.add_login_response()
107108

@@ -149,25 +150,19 @@ def test_create_bom_multiple_purls(self, capsys):
149150
adding_headers={"Authorization": "Token " + self.MYTOKEN},
150151
)
151152

152-
cdx_components = sut.create_project_bom(self.get_project_for_test())
153+
cdx_components, _ = sut.create_project_bom(self.get_project_for_test(),
154+
create_controlfile=False)
153155
captured = capsys.readouterr()
154156

155157
assert "Multiple purls added" in captured.out
156158
assert cdx_components[0].purl == "pkg:deb/debian/cli-support@1.3-1 pkg:pypi/cli-support@1.3"
157159

158-
@responses.activate
159-
def test_project_by_id(self):
160-
sut = CreateBom()
161-
162-
self.add_login_response()
163-
sut.login(token=TestBasePytest.MYTOKEN, url=TestBasePytest.MYURL)
164-
160+
def add_project_releases_responses(self):
165161
# the project
166-
project = self.get_project_for_test()
167162
responses.add(
168163
responses.GET,
169164
url=self.MYURL + "resource/api/projects/p001",
170-
json=project,
165+
json=self.get_project_for_test(),
171166
status=200,
172167
content_type="application/json",
173168
adding_headers={"Authorization": "Token " + self.MYTOKEN},
@@ -194,7 +189,7 @@ def test_project_by_id(self):
194189
"attachmentType": "SOURCE_SELF",
195190
"_links": {
196191
"self": {
197-
"href": "https://my.server.com/resource/api/attachments/r002a002"
192+
"href": "https://my.server.com/resource/api/attachments/r002a003"
198193
}
199194
}
200195
})
@@ -204,7 +199,7 @@ def test_project_by_id(self):
204199
"attachmentType": "CLEARING_REPORT",
205200
"_links": {
206201
"self": {
207-
"href": "https://my.server.com/resource/api/attachments/r002a003"
202+
"href": "https://my.server.com/resource/api/attachments/r002a004"
208203
}
209204
}
210205
})
@@ -217,8 +212,19 @@ def test_project_by_id(self):
217212
content_type="application/json",
218213
adding_headers={"Authorization": "Token " + self.MYTOKEN},
219214
)
215+
return release
220216

221-
cdx_bom = sut.create_project_cdx_bom("p001")
217+
@responses.activate
218+
def test_project_by_id(self):
219+
sut = CreateBom()
220+
221+
self.add_login_response()
222+
sut.login(token=TestBasePytest.MYTOKEN, url=TestBasePytest.MYURL)
223+
224+
release = self.add_project_releases_responses()
225+
project = self.get_project_for_test()
226+
227+
cdx_bom, _ = sut.create_project_cdx_bom("p001", create_controlfile=False)
222228
cx_comp = cdx_bom.components[0]
223229
assert cx_comp.purl == release["externalIds"]["package-url"]
224230

@@ -239,15 +245,15 @@ def test_project_by_id(self):
239245
assert len(ext_refs) == 1
240246
assert ext_refs[0].url == release["_embedded"]["sw360:attachments"][1]["filename"]
241247
assert ext_refs[0].type == ExternalReferenceType.OTHER
242-
assert ext_refs[0].comment, CaPyCliBom.CLI_FILE_COMMENT + " == sw360Id: r002a002"
248+
assert ext_refs[0].comment == CaPyCliBom.CLI_FILE_COMMENT
243249
assert ext_refs[0].hashes[0].alg == "SHA-1"
244250
assert ext_refs[0].hashes[0].content == release["_embedded"]["sw360:attachments"][1]["sha1"]
245251

246252
ext_refs = [e for e in cx_comp.external_references
247253
if e.comment and e.comment.startswith(CaPyCliBom.CRT_FILE_COMMENT)]
248254
assert len(ext_refs) == 1
249255
assert ext_refs[0].url == release["_embedded"]["sw360:attachments"][3]["filename"]
250-
assert ext_refs[0].comment, CaPyCliBom.CRT_FILE_COMMENT + " == sw360Id: r002a003"
256+
assert ext_refs[0].comment == CaPyCliBom.CRT_FILE_COMMENT
251257
assert ext_refs[0].type == ExternalReferenceType.OTHER
252258
assert ext_refs[0].hashes[0].alg == "SHA-1"
253259
assert ext_refs[0].hashes[0].content == release["_embedded"]["sw360:attachments"][3]["sha1"]
@@ -260,6 +266,71 @@ def test_project_by_id(self):
260266
assert cdx_bom.metadata.component.version == project["version"]
261267
assert cdx_bom.metadata.component.description == project["description"]
262268

269+
@responses.activate
270+
def test_project_by_id_controlfile(self):
271+
sut = CreateBom()
272+
self.add_login_response()
273+
sut.login(token=TestBasePytest.MYTOKEN, url=TestBasePytest.MYURL)
274+
275+
self.add_project_releases_responses()
276+
277+
# attachment info
278+
responses.add(
279+
method=responses.GET,
280+
url=self.MYURL + "resource/api/attachments/r001a001",
281+
body="""
282+
{
283+
"filename": "CLIXML_wheel-0.38.4.xml",
284+
"sha1": "ccd9f1ed2f59c46ff3f0139c05bfd76f83fd9851",
285+
"attachmentType": "COMPONENT_LICENSE_INFO_XML"
286+
}""",
287+
status=200,
288+
content_type="application/json",
289+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
290+
)
291+
responses.add(
292+
method=responses.GET,
293+
url=self.MYURL + "resource/api/attachments/r002a002",
294+
body="""
295+
{
296+
"filename": "CLIXML_clipython-1.3.0.xml",
297+
"sha1": "dd4c38387c6811dba67d837af7742d84e61e20de",
298+
"attachmentType": "COMPONENT_LICENSE_INFO_XML",
299+
"checkedBy": "user2@siemens.com",
300+
"checkStatus": "ACCEPTED",
301+
"createdBy": "user1@siemens.com"
302+
}""",
303+
status=200,
304+
content_type="application/json",
305+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
306+
)
307+
responses.add(
308+
method=responses.GET,
309+
url=self.MYURL + "resource/api/attachments/r002a004",
310+
body="""
311+
{
312+
"filename": "clipython-1.3.0.docx",
313+
"sha1": "f0d8f2ddd017bdeaecbaec72ff76a6c0a045ec66",
314+
"attachmentType": "CLEARING_REPORT"
315+
316+
}""",
317+
status=200,
318+
content_type="application/json",
319+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
320+
)
321+
322+
_, controlfile = sut.create_project_cdx_bom("p001", create_controlfile=True)
323+
assert controlfile['ProjectName'] == 'CaPyCLI, 1.9.0'
324+
assert controlfile['Components'][0]['ComponentName'] == 'cli-support 1.3'
325+
assert controlfile['Components'][0]['Sw360Id'] == 'r002'
326+
assert controlfile['Components'][0]['Sw360AttachmentId'] == 'r002a002'
327+
assert controlfile['Components'][0]['CliFile'] == 'CLIXML_clipython-1.3.0.xml'
328+
assert controlfile['Components'][0]['CheckedBy'] == 'user2@siemens.com'
329+
assert controlfile['Components'][0]['CheckStatus'] == 'ACCEPTED'
330+
assert controlfile['Components'][0]['CreatedBy'] == 'user1@siemens.com'
331+
332+
assert controlfile['Components'][1]['ReportFile'] == 'clipython-1.3.0.docx'
333+
263334
@responses.activate
264335
def test_project_show_by_name(self):
265336
sut = CreateBom()
@@ -275,6 +346,7 @@ def test_project_show_by_name(self):
275346
args.name = "CaPyCLI"
276347
args.version = "1.9.0"
277348
args.outputfile = self.OUTPUTFILE
349+
args.controlfile = None
278350

279351
self.add_login_response()
280352

@@ -358,6 +430,65 @@ def test_project_show_by_name(self):
358430

359431
self.delete_file(self.OUTPUTFILE)
360432

433+
@responses.activate
434+
def test_create_project_bom_release_error(self):
435+
sut = CreateBom()
436+
437+
self.add_login_response()
438+
sut.login(token=TestBasePytest.MYTOKEN, url=TestBasePytest.MYURL)
439+
440+
responses.add(
441+
responses.GET,
442+
url=self.MYURL + "resource/api/releases/r001",
443+
status=404,
444+
content_type="application/json",
445+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
446+
)
447+
responses.add(
448+
responses.GET,
449+
url=self.MYURL + "resource/api/releases/r002",
450+
json=self.get_release_cli_for_test(),
451+
status=200,
452+
content_type="application/json",
453+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
454+
)
455+
with pytest.raises(SystemExit):
456+
bom, _ = sut.create_project_bom(self.get_project_for_test(), create_controlfile=False)
457+
458+
@responses.activate
459+
def test_create_project_bom_controlfile_attachment_error(self):
460+
sut = CreateBom()
461+
462+
self.add_login_response()
463+
sut.login(token=TestBasePytest.MYTOKEN, url=TestBasePytest.MYURL)
464+
465+
responses.add(
466+
responses.GET,
467+
url=self.MYURL + "resource/api/releases/r001",
468+
json=self.get_release_wheel_for_test(),
469+
status=200,
470+
content_type="application/json",
471+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
472+
)
473+
responses.add(
474+
responses.GET,
475+
url=self.MYURL + "resource/api/releases/r002",
476+
json=self.get_release_cli_for_test(),
477+
status=200,
478+
content_type="application/json",
479+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
480+
)
481+
responses.add(
482+
method=responses.GET,
483+
url=self.MYURL + "resource/api/attachments/r002a002",
484+
status=404,
485+
content_type="application/json",
486+
adding_headers={"Authorization": "Token " + self.MYTOKEN},
487+
)
488+
489+
with pytest.raises(SystemExit):
490+
bom, _ = sut.create_project_bom(self.get_project_for_test(), create_controlfile=True)
491+
361492

362493
if __name__ == "__main__":
363494
APP = TestCreateBom()

0 commit comments

Comments
 (0)