Skip to content

Commit

Permalink
feat(bom): new command downloadattachments
Browse files Browse the repository at this point in the history
  • Loading branch information
gernot-h committed Aug 7, 2023
1 parent 411f8ed commit 0621d97
Show file tree
Hide file tree
Showing 8 changed files with 578 additions and 14 deletions.
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* `project createbom` add SW360 attachment info as external references to SBOM
(currently supported: source, binary, CLI, report).
* `project createbom` adds SW360 project name, version and description to SBOM.
* new command `bom downloadattachments` to download CLI and report attachments

## 2.0.0 (2023-06-02)

Expand Down
161 changes: 161 additions & 0 deletions capycli/bom/download_attachments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
# -------------------------------------------------------------------------------
# Copyright (c) 2020-2023 Siemens
# All Rights Reserved.
# Author: gernot.hillier@siemens.com, thomas.graf@siemens.com
#
# SPDX-License-Identifier: MIT
# -------------------------------------------------------------------------------

import logging
import os
import sys
from typing import Tuple

import sw360.sw360_api
from cyclonedx.model.bom import Bom

import capycli.common.json_support
import capycli.common.script_base
from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomWriter
from capycli.common.print import print_red, print_text, print_yellow
from capycli.common.script_support import ScriptSupport
from capycli.main.result_codes import ResultCode

LOG = capycli.get_logger(__name__)


class BomDownloadAttachments(capycli.common.script_base.ScriptBase):
"""
Download SW360 attachments as specified in the SBOM.
"""

def download_attachments(self, sbom: Bom, source_folder: str,
attachment_types: Tuple[str] = ("COMPONENT_LICENSE_INFO_XML", "CLEARING_REPORT")) -> Bom:

for component in sbom.components:
item_name = ScriptSupport.get_full_name_from_component(component)
print_text(" " + item_name)

for ext_ref in component.external_references:
if not ext_ref.comment:
continue
found = False
for at_type in attachment_types:
if ext_ref.comment.startswith(CaPyCliBom.FILE_COMMENTS[at_type]):
found = True
if not found:
continue

attachment_id = ext_ref.comment.split(", sw360Id: ")
if len(attachment_id) != 2:
print_red(" No sw360Id for attachment!")
continue
attachment_id = attachment_id[1]

release_id = CycloneDxSupport.get_property_value(component, CycloneDxSupport.CDX_PROP_SW360ID)
if not release_id:
print_red(" No sw360Id for release!")
continue
print(" ", ext_ref.url, release_id, attachment_id)
filename = os.path.join(source_folder, ext_ref.url)

try:
at_info = self.client.get_attachment(attachment_id)
at_info = {k: v for k, v in at_info.items()
if k.startswith("check")
or k.startswith("created")}
print(at_info)

self.client.download_release_attachment(filename, release_id, attachment_id)
ext_ref.url = filename
try:
CycloneDxSupport.have_relative_ext_ref_path(ext_ref, bompath)
except ValueError:
print_yellow(" SBOM file is not relative to source file " + ext_ref.url)

except sw360.sw360_api.SW360Error as swex:
print_red(" Error getting", swex.url, swex.response)
return sbom

def run(self, args):
"""Main method
@params:
args - command line arguments
"""
if args.debug:
global LOG
LOG = capycli.get_logger(__name__)
else:
# suppress (debug) log output from requests and urllib
logging.getLogger("requests").setLevel(logging.WARNING)
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)

print_text(
"\n" + capycli.APP_NAME + ", " + capycli.get_app_version() +
" - Download SW360 attachments as specified in the SBOM\n")

if args.help:
print("usage: capycli bom downloadattachments -i bom.json [-source <folder>]")
print("")
print("optional arguments:")
print(" -h, --help show this help message and exit")
print(" -i INPUTFILE, input SBOM file to read from (JSON)")
print(" -source SOURCE source folder or additional source file")
print(" -o OUTPUTFILE output file to write to")
print(" -v be verbose")
return

if not args.inputfile:
print_red("No input file specified!")
sys.exit(ResultCode.RESULT_COMMAND_ERROR)

if not os.path.isfile(args.inputfile):
print_red("Input file not found!")
sys.exit(ResultCode.RESULT_FILE_NOT_FOUND)

print_text("Loading SBOM file " + args.inputfile)
try:
bom = CaPyCliBom.read_sbom(args.inputfile)
except Exception as ex:
print_red("Error reading input SBOM file: " + repr(ex))
sys.exit(ResultCode.RESULT_ERROR_READING_BOM)

if args.verbose:
print_text(" " + str(len(bom.components)) + "components read from SBOM file")

source_folder = "./"
if args.source:
source_folder = args.source
if (not source_folder) or (not os.path.isdir(source_folder)):
print_red("Target source code folder does not exist!")
sys.exit(ResultCode.RESULT_COMMAND_ERROR)

if args.sw360_token and args.oauth2:
self.analyze_token(args.sw360_token)

print_text(" Checking access to SW360...")
if not self.login(token=args.sw360_token, url=args.sw360_url, oauth2=args.oauth2):
print_red("ERROR: login failed!")
sys.exit(ResultCode.RESULT_AUTH_ERROR)

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

self.download_attachments(bom, source_folder)

if args.outputfile:
print_text("Updating path information")
self.update_local_path(bom, args.outputfile)

print_text("Writing updated SBOM to " + args.outputfile)
try:
SbomWriter.write_to_json(bom, args.outputfile, True)
except Exception as ex:
print_red("Error writing updated SBOM file: " + repr(ex))
sys.exit(ResultCode.RESULT_ERROR_WRITING_BOM)

if args.verbose:
print_text(" " + str(len(bom.components)) + " components written to SBOM file")

print("\n")
7 changes: 7 additions & 0 deletions capycli/bom/handle_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import capycli.bom.create_components
import capycli.bom.diff_bom
import capycli.bom.download_sources
import capycli.bom.download_attachments
import capycli.bom.filter_bom
import capycli.bom.findsources
import capycli.bom.map_bom
Expand Down Expand Up @@ -100,6 +101,12 @@ def run_bom_command(args) -> None:
app.run(args)
return

if subcommand == "downloadattachments":
"""Download attachments from SW360 as specified in the SBOM."""
app = capycli.bom.download_attachments.BomDownloadAttachments()
app.run(args)
return

if subcommand == "granularity":
"""Check the granularity of the releases in the SBOM."""
app = capycli.bom.check_granularity.CheckGranularity()
Expand Down
9 changes: 9 additions & 0 deletions capycli/common/capycli_bom_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,15 @@ class CaPyCliBom():
# human-readable description of licensing situation and obligations
CRT_FILE_COMMENT = "clearing report (local copy)"

FILE_COMMENTS = {
"SOURCE": SOURCE_FILE_COMMENT,
"SOURCE_SELF": SOURCE_FILE_COMMENT,
"BINARY": BINARY_FILE_COMMENT,
"BINARY_SELF": BINARY_FILE_COMMENT,
"COMPONENT_LICENSE_INFO_XML": CLI_FILE_COMMENT,
"CLEARING_REPORT": CRT_FILE_COMMENT
}

@classmethod
def read_sbom(cls, inputfile: str) -> Bom:
LOG.debug(f"Reading from file {inputfile}")
Expand Down
13 changes: 2 additions & 11 deletions capycli/project/create_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,6 @@
class CreateBom(capycli.common.script_base.ScriptBase):
"""Create a SBOM for a project on SW360."""

comments = {
"SOURCE": CaPyCliBom.SOURCE_FILE_COMMENT,
"SOURCE_SELF": CaPyCliBom.SOURCE_FILE_COMMENT,
"BINARY": CaPyCliBom.BINARY_FILE_COMMENT,
"BINARY_SELF": CaPyCliBom.BINARY_FILE_COMMENT,
"COMPONENT_LICENSE_INFO_XML": CaPyCliBom.CLI_FILE_COMMENT,
"CLEARING_REPORT": CaPyCliBom.CRT_FILE_COMMENT
}

def get_external_id(self, name: str, release_details: dict):
"""Returns the external id with the given name or None."""
if "externalIds" not in release_details:
Expand Down Expand Up @@ -97,9 +88,9 @@ def create_project_bom(self, project) -> list:
attachments = self.get_release_attachments(release_details)
for attachment in attachments:
at_type = attachment["attachmentType"]
if at_type not in self.comments:
if at_type not in CaPyCliBom.FILE_COMMENTS:
continue
comment = self.comments[at_type]
comment = CaPyCliBom.FILE_COMMENTS[at_type]
if at_type in ("SOURCE", "SOURCE_SELF", "BINARY", "BINARY_SELF"):
ext_ref_type = ExternalReferenceType.DISTRIBUTION
else:
Expand Down
28 changes: 27 additions & 1 deletion tests/fixtures/sbom_for_download.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,12 +88,38 @@
{
"url": "https://github.com/certifi/python-certifi",
"type": "website"
},
{
"url": "CLIXML_certifi-2022.12.7.xml",
"comment": "component license information (local copy), sw360Id: 794446",
"type": "other",
"hashes": [
{
"alg": "SHA-1",
"content": "542e87fa0acb8d9c4659145a3e1bfcd66c979f33"
}
]
},
{
"url": "certifi-2022.12.7_clearing_report.docx",
"comment": "clearing report (local copy), sw360Id: 63b368",
"type": "other",
"hashes": [
{
"alg": "SHA-1",
"content": "3cd24769fa3da4af74d0118433619a130da091b0"
}
]
}
],
"properties": [
{
"name": "siemens:primaryLanguage",
"value": "Python"
},
{
"name": "siemens:sw360Id",
"value": "ae8c7ed"
}
]
}
Expand All @@ -108,4 +134,4 @@
"dependsOn": []
}
]
}
}

0 comments on commit 0621d97

Please sign in to comment.