diff --git a/capycli/bom/findsources.py b/capycli/bom/findsources.py index 3a1f89c..7e1141c 100644 --- a/capycli/bom/findsources.py +++ b/capycli/bom/findsources.py @@ -11,16 +11,23 @@ import re import sys from typing import Any - +import time import requests +import semver + +# from packageurl import PackageURL +from typing import Any from cyclonedx.model import ExternalReferenceType from cyclonedx.model.bom import Bom +from sw360 import SW360Error import capycli.common.script_base from capycli import get_logger from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport, SbomWriter from capycli.common.print import print_green, print_red, print_text, print_yellow from capycli.main.result_codes import ResultCode +from bs4 import BeautifulSoup +from colorama import Fore, Style LOG = get_logger(__name__) @@ -38,7 +45,7 @@ def __init__(self): def is_sourcefile_accessible(self, sourcefile_url: str) -> bool: """Check if the URL is accessible.""" try: - response = requests.head(sourcefile_url) + response = requests.head(sourcefile_url, allow_redirects=True) if not response.ok: return False @@ -60,48 +67,180 @@ def is_sourcefile_accessible(self, sourcefile_url: str) -> bool: return False + @staticmethod + def github_request(url: str, username="", token=""): + try: + headers = {} + if token: + headers["Authorization"] = "token " + token + if username: + headers["Username"] = username + response = requests.get(url, headers=headers) + if not response.ok: + if response.status_code == 429 or \ + 'rate limit exceeded' in response.reason or \ + "API rate limit exceeded" in response.json().get("message"): + print( + Fore.LIGHTYELLOW_EX + + " Github API rate limit exceeded - wait 60s and retry ... " + + Style.RESET_ALL) + time.sleep(60) + return FindSources.github_request(url, username, token) + + return response.json() + + except Exception as ex: + print( + Fore.LIGHTYELLOW_EX + + " Error acccessing GitHub: " + repr(ex) + + Style.RESET_ALL) + return None + + @staticmethod + def get_repositories(name: str, language: str, username="", token=""): + """Query for GitHub repositories""" + query = name + " language:" + language.lower() + search_url = "https://api.github.com/search/repositories?q=" + query + return FindSources.github_request(search_url, username, token) + @staticmethod def get_repo_name(github_url: str) -> str: """Extract the GitHub repo name from the specified URL.""" git = "github.com/" url = github_url.replace(".git", "").replace("#readme", "")[github_url.find(git) + len(git):] split = url.split("/") - repo_name = split[0] + "/" + split[1] + + if len(split) > 0: + if len(split) > 1: + repo_name = split[0] + "/" + split[1] + else: + repo_name = split[0] + else: + print( + Fore.LIGHTYELLOW_EX + + " Error getting repo name from: " + github_url + + Style.RESET_ALL) + return None + return repo_name @staticmethod def get_github_info(repository_url: str, username="", token="") -> Any: """Query tag infos from GitHub.""" - try: - headers = {} - if username and token: - headers = { - "Username": username, - "Authorization": "token " + token - } - - tag_url = "https://api.github.com/repos/" + repository_url + "/tags?per_page=100" - tags = requests.get(tag_url, headers=headers).json() + length_per_page = 100 + page = 1 + tags = [] + tag_url = "https://api.github.com/repos/" + repository_url + "/tags" + query = "?per_page=%s&page=%s" % (length_per_page, page) + tmp = FindSources.github_request(tag_url + query, username, token) + if not isinstance(tmp, list): return tags - except Exception as ex: - print_yellow(" Error acccessing GitHub: " + repr(ex)) - return None + tags.extend(tmp) + while len(tmp) == length_per_page: + page += 1 + query = "?per_page=%s&page=%s" % (length_per_page, page) + tmp = FindSources.github_request(tag_url + query, username, token) + tags.extend(tmp) + return tags def to_semver_string(self, version) -> str: """Bring all version information to a format we can compare.""" result = self.version_regex.search(version) if result is None: return "0.0.0" - ver = result.group(0).replace("_", ".") + ver = result.group(0).replace("_", ".").replace("+", "") if not ver[0].isdigit(): return "0.0.0" # Remove leading zeros e.g. 01.10.01 -> 1.10.1 - ver = ".".join(str(int(i)) for i in ver.split(".")) + ver = ".".join(str(int(i)) for i in ver.split(".") if i.isnumeric()) if len(ver[ver.find("."):]) <= 3: return str(ver + ".0") return ver + def find_github_url(self, component, use_language=True) -> str: + """ Find github url for component""" + if not component: + return "" + component_name = component.name + language = "" + for val in component.properties: + if val.name == "siemens:primaryLanguage": + language = val.value + if not use_language: + language = "" + repositories = self.get_repositories( + component_name, language, + self.github_name, self.github_token) + if not repositories or repositories.get("total_count", 0) == 0: + return "" + name_match = [r for r in repositories.get("items") if r.get("name", "") == component_name] + if not len(name_match): + name_match = [r for r in repositories.get("items") if component_name in r.get("name", "")] + if len(name_match): + for match in name_match: + tag_info = self.github_request(match["tags_url"], self.github_name, self.github_token) + source_url = self.get_matching_tag(tag_info, component.version, match["html_url"]) + if len(name_match) == 1: + return source_url + elif source_url: + return source_url + + return "" + + def get_pkg_go_repo_url(self, name: str) -> str: + repo_request_url = 'https://pkg.go.dev/' + name + link_repo = repo_request_url + try: + pkg_go_page = requests.get(repo_request_url) + soup = BeautifulSoup(pkg_go_page.text, 'html.parser') + link_repo = soup.find('div', class_='UnitMeta-repo').find('a').get("href") + except Exception as ex: + print( + Fore.LIGHTYELLOW_EX + + " Error trying to get repository url: " + repr(ex) + + Style.RESET_ALL) + return link_repo + + def find_golang_url(self, component) -> str: + """ Find github url for component""" + if not component: + return "" + component_name = component.name + component_version = component.version.removesuffix("+incompatible") + + repository_name = self.get_pkg_go_repo_url(component_name) + if not len(repository_name): + return "" + + source_url = "" + if repository_name.__contains__("github.com"): + version_split = component_version.split("-") + if len(version_split) == 3: + commit_ref = version_split[2] + print( + Fore.LIGHTYELLOW_EX + + " Warning: version " + component_version + + " does not exist, trying to use the commit ref :" + commit_ref + + Style.RESET_ALL) + source_url = repository_name + "/archive/" + commit_ref + ".zip" + else: + component_name_without_repo_prefix = component_name.removeprefix("github.com/") + component_name_without_repo_prefix = component_name_without_repo_prefix.removeprefix("gopkg.in/") + component_name_without_version = re.sub(r"/v[0-9]+$", '', component_name_without_repo_prefix) + component_name_without_version = re.sub(r"\.v[0-9]+$", '', component_name_without_version) + component_name_without_github_split = component_name_without_version.split("/") + version_prefix = None + if len(component_name_without_github_split) > 2: + version_prefix = "/".join(component_name_without_github_split[2:]) + + tag_info = self.get_github_info(repository_name.removeprefix("https://github.com/"), self.github_name, + self.github_token) + source_url = self.get_matching_tag(tag_info, component_version, repository_name, version_prefix) + + # component["RepositoryUrl"] = repository_name + return source_url + def get_github_source_url(self, github_url: str, version: str) -> str: """Find a source file URL from repository URL and version information.""" github_url = github_url.lower() @@ -119,9 +258,15 @@ def get_github_source_url(self, github_url: str, version: str) -> str: print_text(" repo_name:", repo_name) tag_info = self.get_github_info(repo_name, self.github_name, self.github_token) + return self.get_matching_tag(tag_info, version, github_url) + + def get_matching_tag(self, tag_info, version, github_url, version_prefix=None): if not tag_info or (len(tag_info) == 0): - print_red(" No reply from GitHub URL!") - return "" + print( + Fore.LIGHTRED_EX + + " No tags info reply from GitHub! " + github_url + + Style.RESET_ALL) + return None # check for 'rate limit exceeded' message if "message" in tag_info: @@ -131,7 +276,7 @@ def get_github_source_url(self, github_url: str, version: str) -> str: if tag_info["message"].startswith("Bad credentials"): print_red("Invalid GitHub credential provided - aborting!") sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SERVICE) - + # search for a tag matching our given version information matching_tag = None @@ -141,9 +286,24 @@ def get_github_source_url(self, github_url: str, version: str) -> str: # this should be dictionary, if it is a string then # something went wrong! continue - - version_match = self.to_semver_string(tag.get("name", None)) == normalized_version - if version_match: + try: + if version_prefix and tag.get("name").rpartition("/")[0] != version_prefix: + continue + + version_diff = semver.compare( + self.to_semver_string(tag.get("name", None)), + self.to_semver_string(version)) + except Exception as e: + print( + Fore.LIGHTYELLOW_EX + + " Warning: semver.compare() threw " + e.__class__.__name__ + + " Exception :" + github_url + " " + version + + ", released version: " + tag.get("name", None) + + Style.RESET_ALL) + version_diff = 0 if tag.get("name", None) == version else 2 + # If versions are equal, version_diff shall be 0. + # 1 and -1 have different meanings that doesn't be checked below + if version_diff == 0: matching_tag = tag break @@ -160,6 +320,103 @@ def get_github_source_url(self, github_url: str, version: str) -> str: return source_url + def get_source_url_from_release(self, release_id: str) -> str: + """ get source code url from release """ + for x in range(5): + try: + # print(self.client) + release_details = self.client.get_release(release_id) + source_url = release_details.get("sourceCodeDownloadurl", "") + if self.verbose: + print("getting source url from get from sw360 for release_id " + release_id) + if source_url != "": + return source_url + break + except SW360Error as ex: + if x < 4 and ex.response.status_code == requests.codes["bad_gateway"]: + time.sleep(5) + else: + raise ex + + return None + + def get_release_component_id(self, release_id: str) -> str: + """ get the component id of a release """ + + release_details = self.client.get_release(release_id) + return str(release_details["_links"]["sw360:component"]["href"]).split('/')[-1] + + def find_source_url_from_component(self, component_id: str) -> str: + """ find source code url from component releases """ + + component_details = self.client.get_component(component_id) + source_url = None + github = "github.com" + if component_details.get("_embedded") and \ + component_details["_embedded"].get("sw360:releases"): + for release in component_details["_embedded"].get("sw360:releases"): + release_id = str(release["_links"]["self"]["href"]).split('/')[-1] + source_url = self.get_source_url_from_release(release_id) + if source_url and github in source_url: + break + + if source_url and self.verbose: + print(f'{source_url} found over component_id {component_id}') + + if not source_url and "github.com" in component_details.get("homepage", ""): + source_url = component_details.get("homepage") + if source_url and self.verbose: + print(f'{source_url} found on github over component homepage') + + return source_url + + def find_source_url_on_release(self, component) -> str: + """find the url from sourceCodeDownloadurl from the Id or Sw360Id""" + url = None + release_id = "" + for val in component.properties: + if val.name == "siemens:sw360Id": + release_id = val.value + if release_id: + # get the existing source_url for any kind of release. + url = self.get_source_url_from_release(release_id) + return url + + def find_source_url_recursive_by_sw360(self, component) -> str: + """find the url via an other release of the parent component""" + url = None + found_by_component = False + version = component.version + release_id = "" + component_id = "" + for val in component.properties: + if val.name == "capycli:componentId": + component_id = val.value + if val.name == "siemens:sw360Id": + release_id = val.value + if release_id: + # get the existing source_url for any kind of release, not only related to Github. + url = self.get_source_url_from_release(release_id) + if not url or url == "": + component_id = self.get_release_component_id(release_id) + # when searching by component, only a github url will be considered. + url = self.find_source_url_from_component(component_id) + found_by_component = True + elif component_id: + url = self.find_source_url_from_component(component_id) + found_by_component = True + + # 2nd try for component only when on github: find again the proper url for the current version. + if url and found_by_component and "github.com" in url: + url = self.get_github_source_url(url, version) + + return url + + @staticmethod + def find_source_url_by_language(component) -> str: + capycli.dependencies.javascript.GetJavascriptDependencies().try_find_component_metadata(component, "") + return CycloneDxSupport.get_ext_ref_source_url(component) + def find_sources(self, bom: Bom): """Go through the list of SBOM items and try to determine the source code.""" @@ -176,21 +433,50 @@ def find_sources(self, bom: Bom): continue source_url = None + language = "" + for val in component.properties: + if val.name == "siemens:primaryLanguage": + language = val.value + # first check if not already set on the release. + if self.use_sw360: + if self.verbose: + print(" No Source code URL available", + "try to find from sw360 component or releases") + source_url = self.find_source_url_on_release(component) + + # then consider the package managers + if not source_url and language.lower() == "javascript": + if self.verbose: + print(" No Source code URL available - try to find with language:") + source_url = self.find_source_url_by_language(component) + if not source_url and language.lower() == "golang": + if self.verbose: + print(" No Source code URL available - try to find on pkg.go.dev:") + source_url = self.find_golang_url(component) + + # finally look on github repository_url = CycloneDxSupport.get_ext_ref_repository(component) - website = CycloneDxSupport.get_ext_ref_website(component) - source_code_url = CycloneDxSupport.get_ext_ref_source_code_url(component) - if repository_url: + if repository_url and not source_url: if self.verbose: print_text(" Repository URL available:", repository_url) source_url = self.get_github_source_url( repository_url, component.version) + binary_url = CycloneDxSupport.get_ext_ref_binary_url(component) + if binary_url and not source_url: + if self.verbose: + print_text(" Repository URL available:", repository_url) + source_url = self.get_github_source_url( + binary_url, + component.version) + website = CycloneDxSupport.get_ext_ref_website(component) if website and not source_url: if self.verbose: print_text(" Project site URL available:", website) source_url = self.get_github_source_url( website, component.version) + source_code_url = CycloneDxSupport.get_ext_ref_source_code_url(component) if source_code_url and not source_url: if self.verbose: print_text(" Repository URL available:", source_code_url) @@ -198,6 +484,23 @@ def find_sources(self, bom: Bom): source_code_url, component.version) + # look via the component + if not source_url and self.use_sw360: + if self.verbose: + print(" No Source code URL available", + "try to find via the parent sw360 component") + source_url = self.find_source_url_recursive_by_sw360(component) + + # deeper search on github + if not source_url and not component.bom_ref.value.startswith("pkg:deb/debian/"): + if self.verbose: + print(" No Source code URL available - try to find on github:") + source_url = self.find_github_url(component) + if not source_url and not language == "": + if self.verbose: + print(" No Source code URL available - try to find on github without language:") + source_url = self.find_github_url(component, use_language=False) + if source_url: if self.is_sourcefile_accessible(source_url): found_count += 1 @@ -238,7 +541,7 @@ def run(self, args): print(" -i INPUTFILE SBOM file to read from (JSON)") print(" -o OUTPUTFILE output file to write to") print(" -name NAME (optional) GitHub name for login") - print(" -t TOKEN (optional) GitHub token for login") + print(" -gt TOKEN (optional) GitHub token for login") print(" -v be verbose") return @@ -253,6 +556,16 @@ def run(self, args): self.verbose = args.verbose self.github_name = args.name self.github_token = args.sw360_token + + if self.login( + token=args.sw360_token, url=args.sw360_url, + oauth2=args.oauth2, exit_no_login=False): + print("Using SW360 releases and components to detect GitHub url") + self.github_token = args.github_token + self.use_sw360 = True + else: + self.use_sw360 = False + if self.verbose: if self.github_name and self.github_token: print_text("Using provided GitHub credentials") diff --git a/capycli/common/script_base.py b/capycli/common/script_base.py index b98433c..90d1a65 100644 --- a/capycli/common/script_base.py +++ b/capycli/common/script_base.py @@ -34,7 +34,7 @@ def __init__(self): self.project = None self.sw360_url = os.environ.get("SW360ServerUrl", None) - def login(self, token: str = "", url: str = "", oauth2: bool = False) -> bool: + def login(self, token: str = "", url: str = "", oauth2: bool = False, exit_no_login: bool = True) -> bool: """Login to SW360""" self.sw360_url = os.environ.get("SW360ServerUrl", None) sw360_api_token = os.environ.get("SW360ProductionToken", None) @@ -46,15 +46,21 @@ def login(self, token: str = "", url: str = "", oauth2: bool = False) -> bool: self.sw360_url = url if not self.sw360_url: - print_red(" No SW360 server URL specified!") - sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360) + if exit_no_login: + print_red(" No SW360 server URL specified!") + sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360) + else: + return False if self.sw360_url[-1] != "/": self.sw360_url += "/" if not sw360_api_token: - print_red(" No SW360 API token specified!") - sys.exit(ResultCode.RESULT_AUTH_ERROR) + if exit_no_login: + print_red(" No SW360 API token specified!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + else: + return False self.client = sw360.sw360_api.SW360(self.sw360_url, sw360_api_token, oauth2) diff --git a/capycli/main/options.py b/capycli/main/options.py index 9cc248a..d368701 100644 --- a/capycli/main/options.py +++ b/capycli/main/options.py @@ -160,6 +160,13 @@ def register_options(self): help="use this token for access to SW360", ) + self.parser.add_argument( + "-gt", + "--github_token", + dest="github_token", + help="use this token for access to github", + ) + self.parser.add_argument( "-oa", "--oauth2", diff --git a/poetry.lock b/poetry.lock index 7826ef2..cd5da37 100644 --- a/poetry.lock +++ b/poetry.lock @@ -29,6 +29,21 @@ files = [ [package.extras] tzdata = ["tzdata"] +[[package]] +name = "beautifulsoup4" +version = "4.12.2" +description = "Screen-scraping library" +category = "main" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, + {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, +] + +[package.dependencies] +soupsieve = ">1.2" + [[package]] name = "certifi" version = "2023.11.17" @@ -901,6 +916,18 @@ urllib3 = ">=1.25.10" [package.extras] tests = ["coverage (>=3.7.1,<6.0.0)", "flake8", "mypy", "pytest (>=4.6)", "pytest (>=4.6,<5.0)", "pytest-cov", "pytest-localserver", "types-mock", "types-requests", "types-six"] +[[package]] +name = "semver" +version = "2.13.0" +description = "Python helper for Semantic Versioning (http://semver.org/)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"}, + {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"}, +] + [[package]] name = "setuptools" version = "69.0.2" @@ -942,6 +969,18 @@ files = [ {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, ] +[[package]] +name = "soupsieve" +version = "2.4.1" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "soupsieve-2.4.1-py3-none-any.whl", hash = "sha256:1c1bfee6819544a3447586c889157365a27e10d88cde3ad3da0cf0ddf646feb8"}, + {file = "soupsieve-2.4.1.tar.gz", hash = "sha256:89d12b2d5dfcd2c9e8c22326da9d9aa9cb3dfab0a83a024f05704076ee8d35ea"}, +] + [[package]] name = "sw360" version = "1.3.2rc1" diff --git a/pyproject.toml b/pyproject.toml index ccb3ccc..8bbac04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ capycli = "capycli.main.cli:main" python = "^3.8" # drop support for 3.6 and 3.7 because of wheel and cli-support colorama = "^0.4.3" requests = "^2.31.0" # fix CVE-2023-32681 +semver = "^2.9.1" packageurl-python = ">0.8, <1.0" pyjwt = "^1.7.1" openpyxl = "^3.0.3" @@ -51,6 +52,7 @@ tomli = "^2.0.1" dateparser = "^1.1.8" urllib3 = "*" importlib-resources = "^5.12.0" +beautifulsoup4 = "^4.11.1" [tool.poetry.dev-dependencies] flake8 = ">=3.7.8" diff --git a/tests/test_find_sources.py b/tests/test_find_sources.py index b3c3c45..9b83303 100644 --- a/tests/test_find_sources.py +++ b/tests/test_find_sources.py @@ -7,6 +7,7 @@ # ------------------------------------------------------------------------------- import os +import responses import capycli.common.json_support import capycli.common.script_base @@ -14,6 +15,7 @@ from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport from capycli.main.result_codes import ResultCode from tests.test_base import AppArguments, TestBase +from unittest.mock import MagicMock, patch class TestFindSources(TestBase): @@ -100,7 +102,7 @@ def test_find_sources(self) -> None: self.assertTrue("Using anonymous GitHub access" in out) self.assertTrue("8 components read from SBOM" in out) self.assertTrue("1 source files were already available" in out) - self.assertTrue("5 source file URLs were found" in out) + self.assertTrue("6 source file URLs were found" in out) sbom = CaPyCliBom.read_sbom(args.outputfile) self.assertIsNotNone(sbom) @@ -191,3 +193,69 @@ def test_normalize_version(self): actual = sut.to_semver_string(version) self.assertEqual(actual, expected) self.assertTrue(actual == expected, 'version %s is %s' % (actual, expected)) + + @responses.activate + def test_get_release_component_id(self): + # Mock the sw360 client + mock_client = MagicMock() + mock_client.get_release.return_value = {"_links": {"sw360:component": {"href": self.MYURL + 'components/123'}}} + + # Call the method and assert the result + find_sources = FindSources() + find_sources.client = mock_client + component_id = find_sources.get_release_component_id("some_release_id") + self.assertEqual(component_id, "123") + + @responses.activate + def test_find_source_url_from_component(self): + # Mock the client + mock_client = MagicMock() + mock_client.get_component.return_value = {"_embedded": {"sw360:releases": [{"_links": {"self": {"href": self.MYURL + 'releases/456'}}}]}} + mock_client.get_release.return_value = {"_links": {"sw360:component": {"href": self.MYURL + 'components/123'}}, "sourceCodeDownloadurl": "http://github.com/some/repo/0.0.0"} + + # Call the method and assert the result + find_sources = FindSources() + find_sources.client = mock_client # Inject the mocked client + source_url = find_sources.find_source_url_from_component(component_id="some_component_id") + self.assertEqual(source_url, "http://github.com/some/repo/0.0.0") + + @patch('requests.get') + @patch('bs4.BeautifulSoup') + def test_get_pkg_go_repo_url_success(self, mock_beautifulsoup, mock_requests_get): + # Mocking successful response + mock_requests_get.return_value.text = '
Repo Link
' + mock_beautifulsoup.return_value.find.return_value = MagicMock(get=lambda x: 'https://github.com/example/repo/1.0.0') + find_sources = FindSources() + repo_url = find_sources.get_pkg_go_repo_url('example/package') + self.assertEqual(repo_url, 'https://github.com/example/repo/1.0.0') + + @patch('requests.get', side_effect=Exception('Some error')) + def test_get_pkg_go_repo_url_error(self, mock_requests_get): + # Mocking an exception during the request + find_sources = FindSources() + repo_url = find_sources.get_pkg_go_repo_url('some/package') + self.assertEqual(repo_url, 'https://pkg.go.dev/some/package') + + @patch('capycli.bom.findsources.FindSources.get_github_info') + @patch('capycli.bom.findsources.FindSources.get_matching_tag') + def test_find_golang_url_github(self, mock_get_github_info, mock_get_matching_tag): + # Mocking a GitHub scenario + mock_get_github_info.return_value = 'https://pkg.go.dev/github.com/opencontainers/runc' + mock_get_matching_tag.return_value = 'https://github.com/opencontainers/runc/archive/refs/tags/v1.0.1.zip' + find_sources = FindSources() + component = MagicMock() + component.name = 'github.com/opencontainers/runc' + component.version = 'v1.0.1' + source_url = find_sources.find_golang_url(component) + + self.assertEqual(source_url, 'https://pkg.go.dev/github.com/opencontainers/runc') + + def test_find_golang_url_non_github(self): + # Mocking a non-GitHub scenario + find_sources = FindSources() + component = MagicMock() + component.name = 'example/package' + component.version = 'v1.0.0' + source_url = find_sources.find_golang_url(component) + + self.assertEqual(source_url, '') \ No newline at end of file diff --git a/tests/test_get_dependencies_python.py b/tests/test_get_dependencies_python.py index 283cc01..0433add 100644 --- a/tests/test_get_dependencies_python.py +++ b/tests/test_get_dependencies_python.py @@ -405,7 +405,7 @@ def test_process_poetry_lock_v2(self): self.assertTrue("Checking meta-data:" in out) self.assertTrue("cli-support" in out) self.assertTrue(self.OUTPUTFILE2 in out) - self.assertTrue("34 components items written to file." in out) + self.assertTrue("37 components items written to file." in out) # ensure that dev dependencies are NOT listed self.assertTrue("flake8" not in out)