From 7606fe587b806141bdbafceaf1ebd155721e95a6 Mon Sep 17 00:00:00 2001 From: anthonyharrison Date: Thu, 22 Feb 2024 16:35:35 +0000 Subject: [PATCH] feat: Enhanced ecosystem support for checking latest versions of packages --- README.md | 22 +++++++++++-- requirements.txt | 1 + sbomaudit/audit.py | 77 +++++++++++--------------------------------- sbomaudit/cli.py | 2 +- sbomaudit/version.py | 4 +-- 5 files changed, 42 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index ea8459c..5efabd8 100644 --- a/README.md +++ b/README.md @@ -153,12 +153,12 @@ The following checks are performed on each package item: - Check that a version is specified. -- Check that the package version is the latest released version of the package. The latest version checks are only performed if the `--offline` option is not specified and is only performed for Python modules available on the [Python Package Index (PyPi)](https://pypi.org/). +- Check that the package version is the latest released version of the package. The latest version checks are only performed if the `--offline` option is not specified. -- Check that a mature version of the package is being used as determined by the value specified in the `--age` option. The release date checks are only performed if the `--offline` option is not specified and is only performed for Python modules available on the [Python Package Index (PyPi)](https://pypi.org/). +- Check that a mature version of the package is being used as determined by the value specified in the `--age` option. The release date checks are only performed if the `--offline` option is not specified. - Check the age of a package being used, which is not the latest released version, is greater than the value specified in the `--maxage` option. -The check is only performed if the `--offline` option is not specified and is only performed for Python modules available on the [Python Package Index (PyPi)](https://pypi.org/). +The check is only performed if the `--offline` option is not specified. - Check that a license is specified and that the license identified is a valid [SPDX License identifier](https://spdx.org/licenses/). Note that NOASSERTION is not considered a valid license. @@ -172,6 +172,22 @@ The check is only performed if the `--offline` option is not specified and is on - Check that a [CPE specification](https://nvd.nist.gov/products/cpe) is provided for the package. +### Latest package version checks + +The checks for the latest package version are performed for packages within the following language ecosystems: + +- dart +- go +- java +- javascript +- .net +- perl +- python +- r +- ruby +- rust +- swift + ### Relationships The following checks are performed: diff --git a/requirements.txt b/requirements.txt index 998b58f..1962ee6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ lib4sbom >= 0.5.0 +lib4package >= 0.2.0 rich requests packageurl-python diff --git a/sbomaudit/audit.py b/sbomaudit/audit.py index 2322bec..debcc72 100644 --- a/sbomaudit/audit.py +++ b/sbomaudit/audit.py @@ -7,6 +7,7 @@ import dateutil.parser import pytz import requests +from lib4package.metadata import Metadata from lib4sbom.data.document import SBOMDocument from lib4sbom.license import LicenseScanner from packageurl import PackageURL @@ -23,6 +24,7 @@ def __init__(self, options={}, output=""): self.purl_check = options.get("purlcheck", False) self.license_check = options.get("license_check", True) self.age = int(options.get("age", "0")) + self.debug = options.get("debug", False) DAYS_IN_YEAR = 365 self.maxage = int(options.get("maxage", "2")) * DAYS_IN_YEAR self.license_scanner = LicenseScanner() @@ -85,24 +87,6 @@ def _check_value(self, text, values, data_item): def _check(self, text, value, failure_text="MISSING"): self._show_result(text, value, failure_text=failure_text) - def find_latest_version_maven(self, name): - """Returns the latest version of the package available at Maven Central.""" - - url: str = f"https://search.maven.org/solrsearch/select?q=a:{name}&wt=json" - maven_version = None - try: - package_json = requests.get(url).json() - maven_version = package_json["response"]["docs"][0]["latestVersion"] - except Exception as error: - print( - f""" - -------------------------- Can't check Maven Central for the latest version ------- - Exception details: {error} - Please make sure you have a working internet connection or try again later. - """ - ) - return maven_version - def find_latest_version(self, name, version=None): """Returns the version and release date of the package available at PyPI.""" @@ -119,40 +103,16 @@ def find_latest_version(self, name, version=None): "upload_time_iso_8601" ] except Exception as error: - print( - f""" - -------------------------- Can't check PyPi for the latest version ------- - Exception details: {error} - Please make sure you have a working internet connection or try again later. - """ - ) + if self.debug: + print(f"Unable to retrieve Python data for {name} - {version}. {error}") return pypi_version, pypi_date - def get_latest_version(self, name, backend="PyPI"): - url: str = f"https://release-monitoring.org/api/v2/projects/?name={name}" - latest_version = None - try: - package_json = requests.get(url).json() - if ( - package_json["total_items"] == 1 - and package_json["items"][0]["backend"].lower() == backend.lower() - ): - latest_version = package_json["items"][0]["stable_versions"][0] - else: - for item in package_json["items"]: - if item["backend"].lower() == backend.lower(): - latest_version = item["stable_versions"][0] - break - - except Exception as error: - print( - f""" - -------------------------- Can't check for the latest version ------- - Exception details: {error} - Please make sure you have a working internet connection or try again later. - """ - ) - return latest_version + def get_package_info(self, package_name, package_type): + self.package_metadata = Metadata(package_type, debug=self.debug) + self.package_metadata.get_package(package_name) + latest_version = self.package_metadata.get_latest_version() + latest_date = self.package_metadata.get_latest_release_time() + return latest_version, latest_date def process_file(self, filename, allow): # Only process if file exists @@ -401,18 +361,20 @@ def audit_sbom(self, sbom_parser): _, latest_date = self.find_latest_version( name, version=version ) - elif purl["type"] == "maven": - # Maven package detected - latest_version = ( - self.find_latest_version_maven(name) - ) else: - latest_version = self.get_latest_version( - name, backend=purl["type"] + ( + latest_version, + latest_date, + ) = self.get_package_info( + name, purl["type"] ) purl_name = purl["name"] except ValueError: purl_used = False + if self.debug: + print( + f"Version check for {name} within {purl['type']} ecosystem. {latest_version} {latest_date}" + ) elif external_ref[1] in ["cpe22Type", "cpe23Type"]: cpe_used = True @@ -473,7 +435,6 @@ def audit_sbom(self, sbom_parser): failure_text=report, ) if latest_date is not None: - release_date = dateutil.parser.parse(latest_date) release_age = ( pytz.utc.localize(datetime.datetime.utcnow()) diff --git a/sbomaudit/cli.py b/sbomaudit/cli.py index eb24553..073dfd9 100644 --- a/sbomaudit/cli.py +++ b/sbomaudit/cli.py @@ -16,7 +16,6 @@ def main(argv=None): - argv = argv or sys.argv app_name = "sbomaudit" parser = argparse.ArgumentParser( @@ -164,6 +163,7 @@ def main(argv=None): "license_check": not args["disable_license_check"], "age": args["age"], "maxage": args["maxage"], + "debug": args["debug"], } sbom_parser = SBOMParser() diff --git a/sbomaudit/version.py b/sbomaudit/version.py index be76f3d..bda6a38 100644 --- a/sbomaudit/version.py +++ b/sbomaudit/version.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023 Anthony Harrison +# Copyright (C) 2024 Anthony Harrison # SPDX-License-Identifier: Apache-2.0 -VERSION: str = "0.3.1" +VERSION: str = "0.4.0"