Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework some project and bom methods #35

Merged
merged 5 commits into from
Aug 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 11 additions & 27 deletions capycli/bom/download_sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import requests
from cyclonedx.model import ExternalReference, ExternalReferenceType, HashAlgorithm, HashType
from cyclonedx.model.bom import Bom
from cyclonedx.model.component import Component

import capycli.common.json_support
import capycli.common.script_base
Expand Down Expand Up @@ -122,35 +121,20 @@ def download_sources(self, sbom: Bom, source_folder: str) -> None:
if new:
component.external_references.add(ext_ref)

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)
ext_ref = CycloneDxSupport.get_ext_ref(
component, ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT)
if ext_ref:
try:
name = CycloneDxSupport.have_relative_ext_ref_path(ext_ref, bompath)
CycloneDxSupport.update_or_set_property(
component,
CycloneDxSupport.CDX_PROP_FILENAME,
name)
except ValueError:
print_yellow(" SBOM file is not relative to source file " + ext_ref.url)

def run(self, args):
"""Main method
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 @@ -9,6 +9,7 @@
import json
import os
import tempfile
import pathlib
import uuid
from datetime import datetime
from enum import Enum
Expand Down Expand Up @@ -370,6 +371,14 @@ def update_or_set_ext_ref(comp: Component, type: ExternalReferenceType, comment:
else:
CycloneDxSupport.set_ext_ref(comp, type, comment, value)

@staticmethod
def have_relative_ext_ref_path(ext_ref: ExternalReference, rel_to: str):
bip = pathlib.PurePath(ext_ref.url)
file = bip.as_posix()
if os.path.isfile(file):
ext_ref.url = "file://" + bip.relative_to(rel_to).as_posix()
return bip.name

@staticmethod
def get_ext_ref_by_comment(comp: Component, comment: str) -> Any:
for ext_ref in comp.external_references:
Expand Down
28 changes: 28 additions & 0 deletions capycli/common/script_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
Base class for python scripts.
"""

from typing import List, Tuple

import json
import os
import sys
Expand Down Expand Up @@ -111,6 +113,32 @@ def get_error_message(self, swex: sw360.sw360_api.SW360Error) -> str:
str(jcontent["status"]) + "): " + jcontent["message"]
return text

@staticmethod
def get_release_attachments(release_details: dict, att_types: Tuple[str] = None) -> List[dict]:
"""Returns the attachments with the given types from a release. Use empty att_types
to get all attachments."""
if "_embedded" not in release_details:
return None

if "sw360:attachments" not in release_details["_embedded"]:
return None

found = []
attachments = release_details["_embedded"]["sw360:attachments"]
if not att_types:
return attachments

for attachment in attachments:
if attachment["attachmentType"] in att_types:
found.append(attachment)

return found

def release_web_url(self, release_id) -> str:
"""Returns the HTML URL for a given release_id."""
return (self.sw360_url + "group/guest/components/-/component/release/detailRelease/"
+ release_id)

def find_project(self, name: str, version: str, show_results: bool = False) -> str:
"""Find the project with the matching name and version on SW360"""
print_text(" Searching for project...")
Expand Down
23 changes: 2 additions & 21 deletions capycli/project/create_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import logging
import sys
from typing import List, Tuple

import sw360
from cyclonedx.model import ExternalReferenceType, HashAlgorithm
Expand All @@ -34,22 +33,6 @@ def get_external_id(self, name: str, release_details: dict):

return release_details["externalIds"].get(name, "")

def get_attachments(self, att_types: Tuple[str], release_details: dict) -> List[dict]:
"""Returns the attachments with the given types or empty list."""
if "_embedded" not in release_details:
return None

if "sw360:attachments" not in release_details["_embedded"]:
return None

found = []
attachments = release_details["_embedded"]["sw360:attachments"]
for attachment in attachments:
if attachment["attachmentType"] in att_types:
found.append(attachment)

return found

def get_clearing_state(self, proj, href) -> str:
"""Returns the clearing state of the given component/release"""
rel = proj["linkedReleases"]
Expand Down Expand Up @@ -103,7 +86,7 @@ def create_project_bom(self, project) -> list:

for at_type, comment in (("SOURCE", CaPyCliBom.SOURCE_FILE_COMMENT),
("BINARY", CaPyCliBom.BINARY_FILE_COMMENT)):
attachments = self.get_attachments((at_type, at_type + "_SELF"), release_details)
attachments = self.get_release_attachments(release_details, (at_type, at_type + "_SELF"))
for attachment in attachments:
CycloneDxSupport.set_ext_ref(rel_item, ExternalReferenceType.DISTRIBUTION,
comment, attachment["filename"],
Expand All @@ -123,9 +106,7 @@ def create_project_bom(self, project) -> list:
CycloneDxSupport.set_property(
rel_item,
CycloneDxSupport.CDX_PROP_SW360_URL,
self.sw360_url
+ "group/guest/components/-/component/release/detailRelease/"
+ sw360_id)
self.release_web_url(sw360_id))

bom.append(rel_item)

Expand Down
6 changes: 2 additions & 4 deletions capycli/project/show_ecc.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,8 @@ def get_project_status(self, project_id: str):
rel_item["Id"] = self.client.get_id_from_href(href)
rel_item["S360Id"] = rel_item["Id"]
rel_item["Href"] = href
rel_item["Url"] = (
self.sw360_url
+ "group/guest/components/-/component/release/detailRelease/"
+ self.client.get_id_from_href(href))
rel_item["Url"] = self.release_web_url(
self.client.get_id_from_href(href))

try:
release_details = self.client.get_release_by_url(href)
Expand Down
31 changes: 4 additions & 27 deletions tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,16 +84,13 @@ def dump_textfile(text: str, filename: str) -> None:
outfile.write(text)

@staticmethod
def capture_stderr(func: Any, args: Any = None) -> str:
def capture_stderr(func: Any, *args, **kwargs) -> str:
"""Capture stderr for the given function and return result as string"""
# setup the environment
old_stderr = sys.stderr
sys.stderr = TextIOWrapper(BytesIO(), sys.stderr.encoding)

if args:
func(args)
else:
func()
func(*args, **kwargs)

# get output
sys.stderr.seek(0) # jump to the start
Expand All @@ -106,33 +103,13 @@ def capture_stderr(func: Any, args: Any = None) -> str:
return out

@staticmethod
def capture_stdout(func: Any, args: Any = None) -> str:
def capture_stdout(func: Any, *args, **kwargs) -> str:
"""Capture stdout for the given function and return result as string"""
# setup the environment
old_stdout = sys.stdout
sys.stdout = TextIOWrapper(BytesIO(), sys.stdout.encoding)

func(args)

# get output
sys.stdout.seek(0) # jump to the start
out = sys.stdout.read() # read output

# restore stdout
sys.stdout.close()
sys.stdout = old_stdout

return out

@staticmethod
def capture_stdout_no_args(func: Any) -> str:
"""Capture stdout for the given function and return result as string"""
# setup the environment
old_stdout = sys.stdout
sys.stdout = TextIOWrapper(BytesIO(), sys.stdout.encoding)

func()

func(*args, **kwargs)
# get output
sys.stdout.seek(0) # jump to the start
out = sys.stdout.read() # read output
Expand Down
69 changes: 60 additions & 9 deletions tests/test_bom_downloadsources.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@

import responses

from cyclonedx.model import ExternalReferenceType

from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport
from capycli.bom.download_sources import BomDownloadSources
from capycli.main.result_codes import ResultCode
from tests.test_base import AppArguments, TestBase


class TestShowBom(TestBase):
class TestBomDownloadsources(TestBase):
INPUTFILE = "sbom_for_download.json"
INPUTERROR = "plaintext.txt"
OUTPUTFILE = "output.json"
Expand Down Expand Up @@ -74,7 +77,7 @@ def test_error_loading_file(self) -> None:
args.command = []
args.command.append("bom")
args.command.append("downloadsources")
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestShowBom.INPUTERROR)
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.INPUTERROR)

sut.run(args)
self.assertTrue(False, "Failed to report invalid file")
Expand All @@ -90,7 +93,7 @@ def test_source_folder_does_not_exist(self) -> None:
args.command = []
args.command.append("bom")
args.command.append("downloadsources")
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestShowBom.INPUTFILE)
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.INPUTFILE)
args.source = "XXX"

sut.run(args)
Expand All @@ -107,8 +110,8 @@ def test_simple_bom(self) -> None:
args.command = []
args.command.append("bom")
args.command.append("downloadsources")
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestShowBom.INPUTFILE)
args.outputfile = TestShowBom.OUTPUTFILE
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.INPUTFILE)
args.outputfile = TestBomDownloadsources.OUTPUTFILE

with tempfile.TemporaryDirectory() as tmpdirname:
args.source = tmpdirname
Expand All @@ -126,20 +129,68 @@ def test_simple_bom(self) -> None:

try:
out = self.capture_stdout(sut.run, args)
out_bom = CaPyCliBom.read_sbom(args.outputfile)
# capycli.common.json_support.write_json_to_file(out, "STDOUT.TXT")
self.assertTrue("Loading SBOM file" in out)
self.assertTrue("sbom_for_download.json" in out) # path may vary
self.assertIn("SBOM file is not relative to", out)
self.assertTrue("Downloading source files to folder" in out)
self.assertTrue("Downloading file certifi-2022.12.7.tar.gz" in out)

resultfile = os.path.join(tmpdirname, "certifi-2022.12.7.tar.gz")
self.assertTrue(os.path.isfile(resultfile))

ext_ref = CycloneDxSupport.get_ext_ref(
out_bom.components[0], ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT)
self.assertEqual(ext_ref.url, resultfile)

self.delete_file(args.outputfile)
return
except: # noqa
except Exception as e: # noqa
# catch all exception to let Python cleanup the temp folder
pass
print(e)

self.assertTrue(False, "Error: we must never arrive here")

@responses.activate
def test_simple_bom_relative_path(self) -> None:
sut = BomDownloadSources()

# create argparse command line argument object
args = AppArguments()
args.command = []
args.command.append("bom")
args.command.append("downloadsources")
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.INPUTFILE)

with tempfile.TemporaryDirectory() as tmpdirname:
args.source = tmpdirname
args.outputfile = os.path.join(tmpdirname, TestBomDownloadsources.OUTPUTFILE)

# fake file content
responses.add(
responses.GET,
url="https://files.pythonhosted.org/packages/37/f7/2b1b/certifi-2022.12.7.tar.gz",
body="""
SOME DUMMY DATA
""",
status=200,
content_type="application/json",
)

try:
sut.run(args)
out_bom = CaPyCliBom.read_sbom(args.outputfile)

ext_ref = CycloneDxSupport.get_ext_ref(
out_bom.components[0], ExternalReferenceType.DISTRIBUTION, CaPyCliBom.SOURCE_FILE_COMMENT)
self.assertEqual(ext_ref.url, "file://certifi-2022.12.7.tar.gz")

self.delete_file(args.outputfile)
return
except Exception as e: # noqa
# catch all exception to let Python cleanup the temp folder
print(e)

self.assertTrue(False, "Error: we must never arrive here")

Expand All @@ -152,8 +203,8 @@ def test_simple_bom_error_download(self) -> None:
args.command = []
args.command.append("bom")
args.command.append("downloadsources")
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestShowBom.INPUTFILE)
args.outputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestShowBom.OUTPUTFILE)
args.inputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.INPUTFILE)
args.outputfile = os.path.join(os.path.dirname(__file__), "fixtures", TestBomDownloadsources.OUTPUTFILE)

with tempfile.TemporaryDirectory() as tmpdirname:
args.source = tmpdirname
Expand Down
2 changes: 1 addition & 1 deletion tests/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def test_error_critical(self) -> None:
self.assertFalse(self.DEBUG_MSG in out)

def test_info_debug(self) -> None:
out = self.capture_stdout_no_args(self.create_output)
out = self.capture_stdout(self.create_output)
# self.dump_textfile(out, "dump.txt")
self.assertFalse(self.CRITICAL_MSG in out)
self.assertFalse(self.ERROR_MSG in out)
Expand Down