Skip to content

Commit d4e70ef

Browse files
committed
feat(bom downloadAttachments): read attachment id from control file
1 parent 07cac99 commit d4e70ef

File tree

3 files changed

+81
-138
lines changed

3 files changed

+81
-138
lines changed

capycli/bom/download_attachments.py

+28-15
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomWriter
2020
from capycli.common.print import print_red, print_text, print_yellow
2121
from capycli.common.script_support import ScriptSupport
22+
from capycli.common.json_support import load_json_file
2223
from capycli.main.result_codes import ResultCode
2324

2425
LOG = capycli.get_logger(__name__)
@@ -29,7 +30,7 @@ class BomDownloadAttachments(capycli.common.script_base.ScriptBase):
2930
Download SW360 attachments as specified in the SBOM.
3031
"""
3132

32-
def download_attachments(self, sbom: Bom, source_folder: str, bompath: str = None,
33+
def download_attachments(self, sbom: Bom, control_components: list, source_folder: str, bompath: str = None,
3334
attachment_types: Tuple[str] = ("COMPONENT_LICENSE_INFO_XML", "CLEARING_REPORT")) -> Bom:
3435

3536
for component in sbom.components:
@@ -46,26 +47,23 @@ def download_attachments(self, sbom: Bom, source_folder: str, bompath: str = Non
4647
if not found:
4748
continue
4849

49-
attachment_id = ext_ref.comment.split(", sw360Id: ")
50-
if len(attachment_id) != 2:
51-
print_red(" No sw360Id for attachment!")
52-
continue
53-
attachment_id = attachment_id[1]
54-
5550
release_id = CycloneDxSupport.get_property_value(component, CycloneDxSupport.CDX_PROP_SW360ID)
5651
if not release_id:
5752
print_red(" No sw360Id for release!")
5853
continue
59-
print(" ", ext_ref.url, release_id, attachment_id)
6054
filename = os.path.join(source_folder, ext_ref.url)
6155

62-
try:
63-
at_info = self.client.get_attachment(attachment_id)
64-
at_info = {k: v for k, v in at_info.items()
65-
if k.startswith("check")
66-
or k.startswith("created")}
67-
print(at_info)
56+
details = [e for e in control_components
57+
if e["Sw360Id"] == release_id and (
58+
e.get("CliFile", "") == ext_ref.url
59+
or e.get("ReportFile", "") == ext_ref.url)]
60+
if len(details) != 1:
61+
print_red(" ERROR: Found", len(details), "entries for attachment",
62+
ext_ref.url, "of", item_name, "in control file!")
63+
continue
64+
attachment_id = details[0]["Sw360AttachmentId"]
6865

66+
try:
6967
self.client.download_release_attachment(filename, release_id, attachment_id)
7068
ext_ref.url = filename
7169
try:
@@ -103,6 +101,7 @@ def run(self, args):
103101
print("optional arguments:")
104102
print(" -h, --help show this help message and exit")
105103
print(" -i INPUTFILE, input SBOM file to read from (JSON)")
104+
print(" -ct CONTROLFILE, control file to read from as created by project CreateBom")
106105
print(" -source SOURCE source folder or additional source file")
107106
print(" -o OUTPUTFILE output file to write to")
108107
print(" -v be verbose")
@@ -112,6 +111,10 @@ def run(self, args):
112111
print_red("No input file specified!")
113112
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
114113

114+
if not args.controlfile:
115+
print_red("No control file specified!")
116+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
117+
115118
if not os.path.isfile(args.inputfile):
116119
print_red("Input file not found!")
117120
sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)
@@ -126,6 +129,16 @@ def run(self, args):
126129
if args.verbose:
127130
print_text(" " + str(len(bom.components)) + "components read from SBOM file")
128131

132+
print_text("Loading control file " + args.controlfile)
133+
try:
134+
control = load_json_file(args.controlfile)
135+
except Exception as ex:
136+
print_red("JSON error reading control file: " + repr(ex))
137+
sys.exit(ResultCode.RESULT_ERROR_READING_BOM)
138+
if "Components" not in control:
139+
print_red("missing Components in control file")
140+
sys.exit(ResultCode.RESULT_ERROR_READING_BOM)
141+
129142
source_folder = "./"
130143
if args.source:
131144
source_folder = args.source
@@ -143,7 +156,7 @@ def run(self, args):
143156

144157
print_text("Downloading source files to folder " + source_folder + " ...")
145158

146-
self.download_attachments(bom, source_folder, os.path.dirname(args.outputfile))
159+
self.download_attachments(bom, control["Components"], source_folder, os.path.dirname(args.outputfile))
147160

148161
if args.outputfile:
149162
print_text("Updating path information")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"ProjectName": "CaPyCLI, 2.0.0-dev1",
3+
"Components": [
4+
{
5+
"ComponentName": "certifi 2022.12.7",
6+
"Sw360Id": "ae8c7ed",
7+
"Sw360AttachmentId": "794446",
8+
"CreatedBy": "[email protected]",
9+
"CreatedTeam": "AA",
10+
"CreatedOn": "2020-10-23",
11+
"CheckStatus": "ACCEPTED",
12+
"CheckedBy": "[email protected]",
13+
"CheckedTeam": "BB",
14+
"CheckedOn": "2020-10-30",
15+
"CliFile": "CLIXML_certifi-2022.12.7.xml"
16+
},
17+
{
18+
"ComponentName": "certifi 2022.12.7",
19+
"Sw360Id": "ae8c7ed",
20+
"Sw360AttachmentId": "63b368",
21+
"ReportFile": "certifi-2022.12.7_clearing_report.docx"
22+
}
23+
]
24+
}

tests/test_bom_downloadattachments.py

+29-123
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@
1111

1212
import responses
1313

14-
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport
14+
from capycli.common.capycli_bom_support import CaPyCliBom
15+
from capycli.common.json_support import load_json_file
1516
from capycli.bom.download_attachments import BomDownloadAttachments
1617
from capycli.main.result_codes import ResultCode
17-
from cyclonedx.model import ExternalReferenceType, HashAlgorithm
1818
from tests.test_base import AppArguments, TestBase
1919

2020

2121
class TestBomDownloadAttachments(TestBase):
2222
INPUTFILE = "sbom_for_download.json"
23+
CONTROLFILE = "sbom_for_download-control.json"
2324
INPUTERROR = "plaintext.txt"
2425
OUTPUTFILE = "output.json"
2526

@@ -69,6 +70,8 @@ def test_file_not_found(self) -> None:
6970
args.command.append("bom")
7071
args.command.append("downloadattachments")
7172
args.inputfile = "DOESNOTEXIST"
73+
args.controlfile = os.path.join(os.path.dirname(__file__),
74+
"fixtures", TestBomDownloadAttachments.CONTROLFILE)
7275

7376
sut.run(args)
7477
self.assertTrue(False, "Failed to report missing file")
@@ -85,6 +88,8 @@ def test_error_loading_file(self) -> None:
8588
args.command.append("bom")
8689
args.command.append("downloadattachments")
8790
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadAttachments.INPUTERROR)
91+
args.controlfile = os.path.join(os.path.dirname(__file__),
92+
"fixtures", TestBomDownloadAttachments.CONTROLFILE)
8893

8994
sut.run(args)
9095
self.assertTrue(False, "Failed to report invalid file")
@@ -103,6 +108,8 @@ def test_source_folder_does_not_exist(self) -> None:
103108
args.command.append("downloadattachments")
104109

105110
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadAttachments.INPUTFILE)
111+
args.controlfile = os.path.join(os.path.dirname(__file__),
112+
"fixtures", TestBomDownloadAttachments.CONTROLFILE)
106113
args.source = "XXX"
107114

108115
sut.run(args)
@@ -113,32 +120,10 @@ def test_source_folder_does_not_exist(self) -> None:
113120
@responses.activate
114121
def test_simple_bom(self) -> None:
115122
bom = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadAttachments.INPUTFILE)
116-
bom = CaPyCliBom.read_sbom(bom)
123+
controlfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadAttachments.CONTROLFILE)
117124

118-
# attachment info - CLI
119-
responses.add(
120-
method=responses.GET,
121-
url=self.MYURL + "resource/api/attachments/794446",
122-
body="""
123-
{
124-
"filename": "CLIXML_certifi-2022.12.7.xml",
125-
"sha1": "3cd24769fa3da4af74d0118433619a130da091b0",
126-
"attachmentType": "COMPONENT_LICENSE_INFO_XML",
127-
"createdBy": "[email protected]",
128-
"createdTeam": "AA",
129-
"createdComment": "comment1",
130-
"createdOn": "2020-10-08",
131-
"checkStatus": "NOTCHECKED",
132-
"_links": {
133-
"self": {
134-
"href": "https://my.server.com/resource/api/attachments/794446"
135-
}
136-
}
137-
}""",
138-
status=200,
139-
content_type="application/json",
140-
adding_headers={"Authorization": "Token " + self.MYTOKEN},
141-
)
125+
bom = CaPyCliBom.read_sbom(bom)
126+
controlfile = load_json_file(controlfile)
142127

143128
# get attachment - CLI
144129
cli_file = self.get_cli_file_mit()
@@ -150,35 +135,6 @@ def test_simple_bom(self) -> None:
150135
content_type="application/text",
151136
adding_headers={"Authorization": "Token " + self.MYTOKEN},
152137
)
153-
154-
# attachment info - report
155-
responses.add(
156-
method=responses.GET,
157-
url=self.MYURL + "resource/api/attachments/63b368",
158-
body="""
159-
{
160-
"filename": "certifi-2022.12.7_clearing_report.docx",
161-
"sha1": "3cd24769fa3da4af74d0118433619a130da091b0",
162-
"attachmentType": "CLEARING_REPORT",
163-
"createdBy": "[email protected]",
164-
"createdTeam": "BB",
165-
"createdComment": "comment3",
166-
"createdOn": "2020-10-08",
167-
"checkedBy": "[email protected]",
168-
"checkedOn" : "2021-01-18",
169-
"checkedComment": "comment4",
170-
"checkStatus": "ACCEPTED",
171-
"_links": {
172-
"self": {
173-
"href": "https://my.server.com/resource/api/attachments/63b368"
174-
}
175-
}
176-
}""",
177-
status=200,
178-
content_type="application/json",
179-
adding_headers={"Authorization": "Token " + self.MYTOKEN},
180-
)
181-
182138
# get attachment - report
183139
responses.add(
184140
method=responses.GET,
@@ -191,7 +147,7 @@ def test_simple_bom(self) -> None:
191147

192148
with tempfile.TemporaryDirectory() as tmpdirname:
193149
try:
194-
bom = self.app.download_attachments(bom, tmpdirname)
150+
bom = self.app.download_attachments(bom, controlfile["Components"], tmpdirname)
195151
resultfile = os.path.join(tmpdirname, "CLIXML_certifi-2022.12.7.xml")
196152
self.assertEqual(bom.components[0].external_references[5].url, resultfile)
197153
self.assertTrue(os.path.isfile(resultfile), "CLI file missing")
@@ -211,25 +167,8 @@ def test_simple_bom_relpath(self) -> None:
211167
bom = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadAttachments.INPUTFILE)
212168
bom = CaPyCliBom.read_sbom(bom)
213169

214-
# attachment info - CLI
215-
responses.add(
216-
method=responses.GET,
217-
url=self.MYURL + "resource/api/attachments/794446",
218-
body="""
219-
{
220-
"filename": "CLIXML_certifi-2022.12.7.xml",
221-
"sha1": "3cd24769fa3da4af74d0118433619a130da091b0",
222-
"attachmentType": "COMPONENT_LICENSE_INFO_XML",
223-
"_links": {
224-
"self": {
225-
"href": "https://my.server.com/resource/api/attachments/794446"
226-
}
227-
}
228-
}""",
229-
status=200,
230-
content_type="application/json",
231-
adding_headers={"Authorization": "Token " + self.MYTOKEN},
232-
)
170+
controlfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadAttachments.CONTROLFILE)
171+
controlfile = load_json_file(controlfile)
233172

234173
# get attachment - CLI
235174
cli_file = self.get_cli_file_mit()
@@ -244,7 +183,8 @@ def test_simple_bom_relpath(self) -> None:
244183

245184
with tempfile.TemporaryDirectory() as tmpdirname:
246185
try:
247-
bom = self.app.download_attachments(bom, tmpdirname, tmpdirname, ("COMPONENT_LICENSE_INFO_XML",))
186+
bom = self.app.download_attachments(bom, controlfile["Components"],
187+
tmpdirname, tmpdirname, ("COMPONENT_LICENSE_INFO_XML",))
248188
resultfile = os.path.join(tmpdirname, "CLIXML_certifi-2022.12.7.xml")
249189
self.assertEqual(bom.components[0].external_references[5].url,
250190
"file://CLIXML_certifi-2022.12.7.xml")
@@ -263,59 +203,29 @@ def test_simple_bom_download_errors(self) -> None:
263203
bom = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadAttachments.INPUTFILE)
264204
bom = CaPyCliBom.read_sbom(bom)
265205

266-
# attachment info - CLI, ok
267-
responses.add(
268-
method=responses.GET,
269-
url=self.MYURL + "resource/api/attachments/794446",
270-
body="""
271-
{
272-
"filename": "CLIXML_certifi-2022.12.7.xml",
273-
"sha1": "3cd24769fa3da4af74d0118433619a130da091b0",
274-
"attachmentType": "COMPONENT_LICENSE_INFO_XML",
275-
"_links": {
276-
"self": {
277-
"href": "https://my.server.com/resource/api/attachments/794446"
278-
}
279-
}
280-
}""",
281-
status=200,
282-
content_type="application/json",
283-
adding_headers={"Authorization": "Token " + self.MYTOKEN},
284-
)
206+
controlfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadAttachments.CONTROLFILE)
207+
controlfile = load_json_file(controlfile)
285208

286209
# get attachment - CLI, error
287210
responses.add(
288211
method=responses.GET,
289212
url=self.MYURL + "resource/api/releases/ae8c7ed/attachments/794446",
290-
body="cli_file",
291213
status=500,
292214
content_type="application/text",
293215
adding_headers={"Authorization": "Token " + self.MYTOKEN},
294216
)
295-
296-
# attachment info - report, error
217+
# get attachment - CLI, error
297218
responses.add(
298219
method=responses.GET,
299-
url=self.MYURL + "resource/api/attachments/63b368",
300-
body="""
301-
{
302-
"filename": "certifi-2022.12.7_clearing_report.docx",
303-
"sha1": "3cd24769fa3da4af74d0118433619a130da091b0",
304-
"attachmentType": "CLEARING_REPORT",
305-
"_links": {
306-
"self": {
307-
"href": "https://my.server.com/resource/api/attachments/63b368"
308-
}
309-
}
310-
}""",
311-
status=404,
312-
content_type="application/json",
220+
url=self.MYURL + "resource/api/releases/ae8c7ed/attachments/63b368",
221+
status=403,
222+
content_type="application/text",
313223
adding_headers={"Authorization": "Token " + self.MYTOKEN},
314224
)
315225

316226
with tempfile.TemporaryDirectory() as tmpdirname:
317227
try:
318-
bom = self.app.download_attachments(bom, tmpdirname)
228+
bom = self.app.download_attachments(bom, controlfile["Components"], tmpdirname)
319229
resultfile = os.path.join(tmpdirname, "CLIXML_certifi-2022.12.7.xml")
320230
self.assertFalse(os.path.isfile(resultfile), "CLI created despite HTTP 500")
321231

@@ -335,8 +245,8 @@ def test_simple_bom_no_release_id(self) -> None:
335245
bom.components[0].properties = []
336246
with tempfile.TemporaryDirectory() as tmpdirname:
337247
try:
338-
err = self.capture_stdout(self.app.download_attachments, bom, tmpdirname)
339-
assert "No sw360Id for release" in err
248+
err = self.capture_stdout(self.app.download_attachments, bom, [], tmpdirname)
249+
self.assertIn("No sw360Id for release", err)
340250

341251
return
342252
except Exception as e: # noqa
@@ -346,18 +256,14 @@ def test_simple_bom_no_release_id(self) -> None:
346256
self.assertTrue(False, "Error: we must never arrive here")
347257

348258
@responses.activate
349-
def test_simple_bom_no_attachment_id(self) -> None:
259+
def test_simple_bom_no_ctrl_file_entry(self) -> None:
350260
bom = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadAttachments.INPUTFILE)
351261
bom = CaPyCliBom.read_sbom(bom)
352-
bom.components[0].external_references = []
353-
CycloneDxSupport.set_ext_ref(bom.components[0], ExternalReferenceType.OTHER,
354-
CaPyCliBom.CLI_FILE_COMMENT, "CLIXML_foo.xml",
355-
HashAlgorithm.SHA_1, "123")
356262

357263
with tempfile.TemporaryDirectory() as tmpdirname:
358264
try:
359-
err = self.capture_stdout(self.app.download_attachments, bom, tmpdirname)
360-
assert "No sw360Id for attachment" in err
265+
err = self.capture_stdout(self.app.download_attachments, bom, [], tmpdirname)
266+
assert "Found 0 entries for attachment CLIXML_certifi-2022.12.7.xml" in err
361267

362268
return
363269
except Exception as e: # noqa

0 commit comments

Comments
 (0)