Skip to content

Commit 0621d97

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

File tree

8 files changed

+578
-14
lines changed

8 files changed

+578
-14
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

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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 sys
12+
from typing import Tuple
13+
14+
import sw360.sw360_api
15+
from cyclonedx.model.bom import Bom
16+
17+
import capycli.common.json_support
18+
import capycli.common.script_base
19+
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomWriter
20+
from capycli.common.print import print_red, print_text, print_yellow
21+
from capycli.common.script_support import ScriptSupport
22+
from capycli.main.result_codes import ResultCode
23+
24+
LOG = capycli.get_logger(__name__)
25+
26+
27+
class BomDownloadAttachments(capycli.common.script_base.ScriptBase):
28+
"""
29+
Download SW360 attachments as specified in the SBOM.
30+
"""
31+
32+
def download_attachments(self, sbom: Bom, source_folder: str,
33+
attachment_types: Tuple[str] = ("COMPONENT_LICENSE_INFO_XML", "CLEARING_REPORT")) -> Bom:
34+
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+
found = False
43+
for at_type in attachment_types:
44+
if ext_ref.comment.startswith(CaPyCliBom.FILE_COMMENTS[at_type]):
45+
found = True
46+
if not found:
47+
continue
48+
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+
55+
release_id = CycloneDxSupport.get_property_value(component, CycloneDxSupport.CDX_PROP_SW360ID)
56+
if not release_id:
57+
print_red(" No sw360Id for release!")
58+
continue
59+
print(" ", ext_ref.url, release_id, attachment_id)
60+
filename = os.path.join(source_folder, ext_ref.url)
61+
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)
68+
69+
self.client.download_release_attachment(filename, release_id, attachment_id)
70+
ext_ref.url = filename
71+
try:
72+
CycloneDxSupport.have_relative_ext_ref_path(ext_ref, bompath)
73+
except ValueError:
74+
print_yellow(" SBOM file is not relative to source file " + ext_ref.url)
75+
76+
except sw360.sw360_api.SW360Error as swex:
77+
print_red(" Error getting", swex.url, swex.response)
78+
return sbom
79+
80+
def run(self, args):
81+
"""Main method
82+
83+
@params:
84+
args - command line arguments
85+
"""
86+
if args.debug:
87+
global LOG
88+
LOG = capycli.get_logger(__name__)
89+
else:
90+
# suppress (debug) log output from requests and urllib
91+
logging.getLogger("requests").setLevel(logging.WARNING)
92+
logging.getLogger("urllib3").setLevel(logging.WARNING)
93+
logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
94+
95+
print_text(
96+
"\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
97+
" - Download SW360 attachments as specified in the SBOM\n")
98+
99+
if args.help:
100+
print("usage: capycli bom downloadattachments -i bom.json [-source <folder>]")
101+
print("")
102+
print("optional arguments:")
103+
print(" -h, --help show this help message and exit")
104+
print(" -i INPUTFILE, input SBOM file to read from (JSON)")
105+
print(" -source SOURCE source folder or additional source file")
106+
print(" -o OUTPUTFILE output file to write to")
107+
print(" -v be verbose")
108+
return
109+
110+
if not args.inputfile:
111+
print_red("No input file specified!")
112+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
113+
114+
if not os.path.isfile(args.inputfile):
115+
print_red("Input file not found!")
116+
sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)
117+
118+
print_text("Loading SBOM file " + args.inputfile)
119+
try:
120+
bom = CaPyCliBom.read_sbom(args.inputfile)
121+
except Exception as ex:
122+
print_red("Error reading input SBOM file: " + repr(ex))
123+
sys.exit(ResultCode.RESULT_ERROR_READING_BOM)
124+
125+
if args.verbose:
126+
print_text(" " + str(len(bom.components)) + "components read from SBOM file")
127+
128+
source_folder = "./"
129+
if args.source:
130+
source_folder = args.source
131+
if (not source_folder) or (not os.path.isdir(source_folder)):
132+
print_red("Target source code folder does not exist!")
133+
sys.exit(ResultCode.RESULT_COMMAND_ERROR)
134+
135+
if args.sw360_token and args.oauth2:
136+
self.analyze_token(args.sw360_token)
137+
138+
print_text(" Checking access to SW360...")
139+
if not self.login(token=args.sw360_token, url=args.sw360_url, oauth2=args.oauth2):
140+
print_red("ERROR: login failed!")
141+
sys.exit(ResultCode.RESULT_AUTH_ERROR)
142+
143+
print_text("Downloading source files to folder " + source_folder + " ...")
144+
145+
self.download_attachments(bom, source_folder)
146+
147+
if args.outputfile:
148+
print_text("Updating path information")
149+
self.update_local_path(bom, args.outputfile)
150+
151+
print_text("Writing updated SBOM to " + args.outputfile)
152+
try:
153+
SbomWriter.write_to_json(bom, args.outputfile, True)
154+
except Exception as ex:
155+
print_red("Error writing updated SBOM file: " + repr(ex))
156+
sys.exit(ResultCode.RESULT_ERROR_WRITING_BOM)
157+
158+
if args.verbose:
159+
print_text(" " + str(len(bom.components)) + " components written to SBOM file")
160+
161+
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()

capycli/common/capycli_bom_support.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,6 +646,15 @@ class CaPyCliBom():
646646
# human-readable description of licensing situation and obligations
647647
CRT_FILE_COMMENT = "clearing report (local copy)"
648648

649+
FILE_COMMENTS = {
650+
"SOURCE": SOURCE_FILE_COMMENT,
651+
"SOURCE_SELF": SOURCE_FILE_COMMENT,
652+
"BINARY": BINARY_FILE_COMMENT,
653+
"BINARY_SELF": BINARY_FILE_COMMENT,
654+
"COMPONENT_LICENSE_INFO_XML": CLI_FILE_COMMENT,
655+
"CLEARING_REPORT": CRT_FILE_COMMENT
656+
}
657+
649658
@classmethod
650659
def read_sbom(cls, inputfile: str) -> Bom:
651660
LOG.debug(f"Reading from file {inputfile}")

capycli/project/create_bom.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,6 @@
2626
class CreateBom(capycli.common.script_base.ScriptBase):
2727
"""Create a SBOM for a project on SW360."""
2828

29-
comments = {
30-
"SOURCE": CaPyCliBom.SOURCE_FILE_COMMENT,
31-
"SOURCE_SELF": CaPyCliBom.SOURCE_FILE_COMMENT,
32-
"BINARY": CaPyCliBom.BINARY_FILE_COMMENT,
33-
"BINARY_SELF": CaPyCliBom.BINARY_FILE_COMMENT,
34-
"COMPONENT_LICENSE_INFO_XML": CaPyCliBom.CLI_FILE_COMMENT,
35-
"CLEARING_REPORT": CaPyCliBom.CRT_FILE_COMMENT
36-
}
37-
3829
def get_external_id(self, name: str, release_details: dict):
3930
"""Returns the external id with the given name or None."""
4031
if "externalIds" not in release_details:
@@ -97,9 +88,9 @@ def create_project_bom(self, project) -> list:
9788
attachments = self.get_release_attachments(release_details)
9889
for attachment in attachments:
9990
at_type = attachment["attachmentType"]
100-
if at_type not in self.comments:
91+
if at_type not in CaPyCliBom.FILE_COMMENTS:
10192
continue
102-
comment = self.comments[at_type]
93+
comment = CaPyCliBom.FILE_COMMENTS[at_type]
10394
if at_type in ("SOURCE", "SOURCE_SELF", "BINARY", "BINARY_SELF"):
10495
ext_ref_type = ExternalReferenceType.DISTRIBUTION
10596
else:

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)