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 5, 2023
1 parent 411f8ed commit e2e1d69
Show file tree
Hide file tree
Showing 6 changed files with 537 additions and 3 deletions.
1 change: 1 addition & 0 deletions ChangeLog.md
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
183 changes: 183 additions & 0 deletions capycli/bom/download_attachments.py
@@ -0,0 +1,183 @@
# -------------------------------------------------------------------------------
# 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 pathlib
import sys

import sw360.sw360_api
from cyclonedx.model import ExternalReferenceType
from cyclonedx.model.bom import Bom
from cyclonedx.model.component import Component

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) -> 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
if (not ext_ref.comment.startswith(CaPyCliBom.CLI_FILE_COMMENT)
and not ext_ref.comment.startswith(CaPyCliBom.CRT_FILE_COMMENT)):
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
except sw360.sw360_api.SW360Error as swex:
print_red(" Error getting", swex.url, swex.response)
return sbom

def have_relative_source_file_path(self, component: Component, bompath: str):
ext_ref = CycloneDxSupport.get_ext_ref(
component, ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT)
if not ext_ref:
return

bip = pathlib.PurePath(ext_ref.url)
try:
CycloneDxSupport.update_or_set_property(
component,
CycloneDxSupport.CDX_PROP_FILENAME,
bip.name)
file = bip.as_posix()
if os.path.isfile(file):
CycloneDxSupport.update_or_set_ext_ref(
component,
ExternalReferenceType.DISTRIBUTION,
CaPyCliBom.SOURCE_FILE_COMMENT,
"file://" + bip.relative_to(bompath).as_posix())
except ValueError:
print_yellow(
" SBOM file is not relative to source file " + ext_ref.url)
# .relative_to
pass

def update_local_path(self, sbom: Bom, bomfile: str):
bompath = pathlib.Path(bomfile).parent
for component in sbom.components:
self.have_relative_source_file_path(component, bompath)

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
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
28 changes: 27 additions & 1 deletion tests/fixtures/sbom_for_download.json
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 e2e1d69

Please sign in to comment.