Skip to content

Commit e2e1d69

Browse files
committed
feat(bom): new command downloadattachments
1 parent 411f8ed commit e2e1d69

File tree

6 files changed

+537
-3
lines changed

6 files changed

+537
-3
lines changed

ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
* `project createbom` add SW360 attachment info as external references to SBOM
1515
(currently supported: source, binary, CLI, report).
1616
* `project createbom` adds SW360 project name, version and description to SBOM.
17+
* new command `bom downloadattachments` to download CLI and report attachments
1718

1819
## 2.0.0 (2023-06-02)
1920

capycli/bom/download_attachments.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# -------------------------------------------------------------------------------
2+
# Copyright (c) 2020-2023 Siemens
3+
# All Rights Reserved.
4+
5+
#
6+
# SPDX-License-Identifier: MIT
7+
# -------------------------------------------------------------------------------
8+
9+
import logging
10+
import os
11+
import pathlib
12+
import sys
13+
14+
import sw360.sw360_api
15+
from cyclonedx.model import ExternalReferenceType
16+
from cyclonedx.model.bom import Bom
17+
from cyclonedx.model.component import Component
18+
19+
import capycli.common.json_support
20+
import capycli.common.script_base
21+
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomWriter
22+
from capycli.common.print import print_red, print_text, print_yellow
23+
from capycli.common.script_support import ScriptSupport
24+
from capycli.main.result_codes import ResultCode
25+
26+
LOG = capycli.get_logger(__name__)
27+
28+
29+
class BomDownloadAttachments(capycli.common.script_base.ScriptBase):
30+
"""
31+
Download SW360 attachments as specified in the SBOM.
32+
"""
33+
34+
def download_attachments(self, sbom: Bom, source_folder: str) -> Bom:
35+
for component in sbom.components:
36+
item_name = ScriptSupport.get_full_name_from_component(component)
37+
print_text(" " + item_name)
38+
39+
for ext_ref in component.external_references:
40+
if not ext_ref.comment:
41+
continue
42+
if (not ext_ref.comment.startswith(CaPyCliBom.CLI_FILE_COMMENT)
43+
and not ext_ref.comment.startswith(CaPyCliBom.CRT_FILE_COMMENT)):
44+
continue
45+
46+
attachment_id = ext_ref.comment.split(", sw360Id: ")
47+
if len(attachment_id) != 2:
48+
print_red(" No sw360Id for attachment!")
49+
continue
50+
attachment_id = attachment_id[1]
51+
52+
release_id = CycloneDxSupport.get_property_value(component, CycloneDxSupport.CDX_PROP_SW360ID)
53+
if not release_id:
54+
print_red(" No sw360Id for release!")
55+
continue
56+
print(" ", ext_ref.url, release_id, attachment_id)
57+
filename = os.path.join(source_folder, ext_ref.url)
58+
59+
try:
60+
at_info = self.client.get_attachment(attachment_id)
61+
at_info = {k: v for k, v in at_info.items()
62+
if k.startswith("check")
63+
or k.startswith("created")}
64+
print(at_info)
65+
66+
self.client.download_release_attachment(filename, release_id, attachment_id)
67+
ext_ref.url = filename
68+
except sw360.sw360_api.SW360Error as swex:
69+
print_red(" Error getting", swex.url, swex.response)
70+
return sbom
71+
72+
def have_relative_source_file_path(self, component: Component, bompath: str):
73+
ext_ref = CycloneDxSupport.get_ext_ref(
74+
component, ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT)
75+
if not ext_ref:
76+
return
77+
78+
bip = pathlib.PurePath(ext_ref.url)
79+
try:
80+
CycloneDxSupport.update_or_set_property(
81+
component,
82+
CycloneDxSupport.CDX_PROP_FILENAME,
83+
bip.name)
84+
file = bip.as_posix()
85+
if os.path.isfile(file):
86+
CycloneDxSupport.update_or_set_ext_ref(
87+
component,
88+
ExternalReferenceType.DISTRIBUTION,
89+
CaPyCliBom.SOURCE_FILE_COMMENT,
90+
"file://" + bip.relative_to(bompath).as_posix())
91+
except ValueError:
92+
print_yellow(
93+
" SBOM file is not relative to source file " + ext_ref.url)
94+
# .relative_to
95+
pass
96+
97+
def update_local_path(self, sbom: Bom, bomfile: str):
98+
bompath = pathlib.Path(bomfile).parent
99+
for component in sbom.components:
100+
self.have_relative_source_file_path(component, bompath)
101+
102+
def run(self, args):
103+
"""Main method
104+
105+
@params:
106+
args - command line arguments
107+
"""
108+
if args.debug:
109+
global LOG
110+
LOG = capycli.get_logger(__name__)
111+
else:
112+
# suppress (debug) log output from requests and urllib
113+
logging.getLogger("requests").setLevel(logging.WARNING)
114+
logging.getLogger("urllib3").setLevel(logging.WARNING)
115+
logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
116+
117+
print_text(
118+
"\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
119+
" - Download SW360 attachments as specified in the SBOM\n")
120+
121+
if args.help:
122+
print("usage: capycli bom downloadattachments -i bom.json [-source <folder>]")
123+
print("")
124+
print("optional arguments:")
125+
print(" -h, --help show this help message and exit")
126+
print(" -i INPUTFILE, input SBOM file to read from (JSON)")
127+
print(" -source SOURCE source folder or additional source file")
128+
print(" -o OUTPUTFILE output file to write to")
129+
print(" -v be verbose")
130+
return
131+
132+
if not args.inputfile:
133+
print_red("No input file specified!")
134+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
135+
136+
if not os.path.isfile(args.inputfile):
137+
print_red("Input file not found!")
138+
sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)
139+
140+
print_text("Loading SBOM file " + args.inputfile)
141+
try:
142+
bom = CaPyCliBom.read_sbom(args.inputfile)
143+
except Exception as ex:
144+
print_red("Error reading input SBOM file: " + repr(ex))
145+
sys.exit(ResultCode.RESULT_ERROR_READING_BOM)
146+
147+
if args.verbose:
148+
print_text(" " + str(len(bom.components)) + "components read from SBOM file")
149+
150+
source_folder = "./"
151+
if args.source:
152+
source_folder = args.source
153+
if (not source_folder) or (not os.path.isdir(source_folder)):
154+
print_red("Target source code folder does not exist!")
155+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
156+
157+
if args.sw360_token and args.oauth2:
158+
self.analyze_token(args.sw360_token)
159+
160+
print_text(" Checking access to SW360...")
161+
if not self.login(token=args.sw360_token, url=args.sw360_url, oauth2=args.oauth2):
162+
print_red("ERROR: login failed!")
163+
sys.exit(ResultCode.RESULT_AUTH_ERROR)
164+
165+
print_text("Downloading source files to folder " + source_folder + " ...")
166+
167+
self.download_attachments(bom, source_folder)
168+
169+
if args.outputfile:
170+
print_text("Updating path information")
171+
self.update_local_path(bom, args.outputfile)
172+
173+
print_text("Writing updated SBOM to " + args.outputfile)
174+
try:
175+
SbomWriter.write_to_json(bom, args.outputfile, True)
176+
except Exception as ex:
177+
print_red("Error writing updated SBOM file: " + repr(ex))
178+
sys.exit(ResultCode.RESULT_ERROR_WRITING_BOM)
179+
180+
if args.verbose:
181+
print_text(" " + str(len(bom.components)) + " components written to SBOM file")
182+
183+
print("\n")

capycli/bom/handle_bom.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import capycli.bom.create_components
1616
import capycli.bom.diff_bom
1717
import capycli.bom.download_sources
18+
import capycli.bom.download_attachments
1819
import capycli.bom.filter_bom
1920
import capycli.bom.findsources
2021
import capycli.bom.map_bom
@@ -100,6 +101,12 @@ def run_bom_command(args) -> None:
100101
app.run(args)
101102
return
102103

104+
if subcommand == "downloadattachments":
105+
"""Download attachments from SW360 as specified in the SBOM."""
106+
app = capycli.bom.download_attachments.BomDownloadAttachments()
107+
app.run(args)
108+
return
109+
103110
if subcommand == "granularity":
104111
"""Check the granularity of the releases in the SBOM."""
105112
app = capycli.bom.check_granularity.CheckGranularity()

tests/fixtures/sbom_for_download.json

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,38 @@
8888
{
8989
"url": "https://github.com/certifi/python-certifi",
9090
"type": "website"
91+
},
92+
{
93+
"url": "CLIXML_certifi-2022.12.7.xml",
94+
"comment": "component license information (local copy), sw360Id: 794446",
95+
"type": "other",
96+
"hashes": [
97+
{
98+
"alg": "SHA-1",
99+
"content": "542e87fa0acb8d9c4659145a3e1bfcd66c979f33"
100+
}
101+
]
102+
},
103+
{
104+
"url": "certifi-2022.12.7_clearing_report.docx",
105+
"comment": "clearing report (local copy), sw360Id: 63b368",
106+
"type": "other",
107+
"hashes": [
108+
{
109+
"alg": "SHA-1",
110+
"content": "3cd24769fa3da4af74d0118433619a130da091b0"
111+
}
112+
]
91113
}
92114
],
93115
"properties": [
94116
{
95117
"name": "siemens:primaryLanguage",
96118
"value": "Python"
119+
},
120+
{
121+
"name": "siemens:sw360Id",
122+
"value": "ae8c7ed"
97123
}
98124
]
99125
}
@@ -108,4 +134,4 @@
108134
"dependsOn": []
109135
}
110136
]
111-
}
137+
}

0 commit comments

Comments
 (0)