diff --git a/ChangeLog.md b/ChangeLog.md index b52d816..a908259 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -11,6 +11,8 @@ Fixes CaPyCLI so that it doesn't crash. But the root cause cannot get fixed: SW360 releases may have multiple purls, CycloneDX components can have only one single purl. +* New options `-client_id` and `-client_secret` to support dynamic token generation + with the new SW360 backend >= 20. * update dependencies, especially "idna" to fix CVE-2026-45409 and "sw360" lib to fix CVE-2026-41066, CVE-2026-44431 and CVE-2026-44432. When accessing a trusted SW360 server using REST API, they all shouldn't be critical, however. diff --git a/capycli/bom/check_bom.py b/capycli/bom/check_bom.py index 6af5b20..53b98e1 100644 --- a/capycli/bom/check_bom.py +++ b/capycli/bom/check_bom.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2019-2024 Siemens +# Copyright (c) 2019-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -15,7 +15,7 @@ from colorama import Fore, Style from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component -from sw360 import SW360Error +from sw360 import SW360Error, SW360Keycloak import capycli.common.script_base from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport @@ -165,13 +165,14 @@ def run(self, args: Any) -> None: print("usage: CaPyCli bom check [-h] [-t SW360_TOKEN] [-oa] [-url SW360_URL] [-v] -i bomfile") print("") print("optional arguments:") - print(" -h, --help show this help message and exit") - print(" -t SW360_TOKEN, SW360_TOKEN") - print(" use this token for access to SW360") - print(" -oa, --oauth2 this is an oauth2 token") - print(" -url SW360_URL use this URL for access to SW360") - print(" -i INPUTFILE SBOM file to read from") - print(" -v be verbose") + print(" -h, --help show this help message and exit") + print(" -t SW360_TOKEN, SW360_TOKEN use this token for access to SW360") + print(" -oa, --oauth2 this is an oauth2 token") + print(" -url SW360_URL use this URL for access to SW360") + print(" -i INPUTFILE SBOM file to read from") + print(" -v be verbose") + print(" -client_id CLIENT_ID the SW360 client_id to be used for token generation") + print(" -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation") return if not args.inputfile: @@ -195,6 +196,17 @@ def run(self, args: Any) -> None: if self._bom_has_items_without_id(bom): print("There are SBOM items without Sw360 id - searching per name may take a little bit longer...") + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + if args.sw360_token and args.oauth2: self.analyze_token(args.sw360_token) diff --git a/capycli/bom/check_bom_item_status.py b/capycli/bom/check_bom_item_status.py index beade56..f262c95 100644 --- a/capycli/bom/check_bom_item_status.py +++ b/capycli/bom/check_bom_item_status.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2019-2024 Siemens +# Copyright (c) 2019-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -15,7 +15,7 @@ from colorama import Fore, Style from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component -from sw360 import SW360Error +from sw360 import SW360Error, SW360Keycloak import capycli.common.script_base from capycli.common.capycli_bom_support import CaPyCliBom, CycloneDxSupport @@ -183,9 +183,11 @@ def run(self, args: Any) -> None: print("usage: capycli bom CheckItemStatus [-h] [-all] -i bomfile") print("") print("optional arguments:") - print("-h, --help show this help message and exit") - print("-i INPUTFILE input file to read from") - print("-all show status of all versions of the component") + print("-h, --help show this help message and exit") + print("-i INPUTFILE input file to read from") + print("-all show status of all versions of the component") + print("-client_id CLIENT_ID the SW360 client_id to be used for token generation") + print("-client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation") return if not args.inputfile: @@ -209,6 +211,17 @@ def run(self, args: Any) -> None: if self._bom_has_items_without_id(bom): print("There are SBOM items without Sw360 id - searching per name may take a little bit longer...") + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + if args.sw360_token and args.oauth2: self.analyze_token(args.sw360_token) diff --git a/capycli/bom/create_components.py b/capycli/bom/create_components.py index d06b418..eab2b66 100644 --- a/capycli/bom/create_components.py +++ b/capycli/bom/create_components.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2019-2025 Siemens +# Copyright (c) 2019-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -20,7 +20,7 @@ from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component from cyclonedx.model.license import DisjunctiveLicense, LicenseExpression -from sw360 import SW360Error +from sw360 import SW360Error, SW360Keycloak import capycli.common.json_support import capycli.common.script_base @@ -43,17 +43,19 @@ class BomCreateComponents(capycli.common.script_base.ScriptBase): "usage: CaPyCLI bom {} -i bom.json -o bom_created.json [-source ]", "", "optional arguments:", - " -h, --help show this help message and exit", - " -i INPUTFILE, input file to read from (JSON)", - " -o OUTPUTFILE, output file to write to", - " -t SW360_TOKEN, use this token for access to SW360", - " -oa, --oauth2 this is an oauth2 token", - " -url SW360_URL use this URL for access to SW360", - " -o OUTPUT write updated BOM to a JSON file", - " -source SOURCE source folder or additional source file", - " --download enable automatic download of missing sources", - " --dbx relaxed Debian version handling: when checking for existing releases,", - " ignore prefixes like \"2:\" (epoch) and suffixes like \".debian\"", + " -h, --help show this help message and exit", + " -i INPUTFILE, input file to read from (JSON)", + " -o OUTPUTFILE, output file to write to", + " -t SW360_TOKEN, use this token for access to SW360", + " -oa, --oauth2 this is an oauth2 token", + " -url SW360_URL use this URL for access to SW360", + " -o OUTPUT write updated BOM to a JSON file", + " -source SOURCE source folder or additional source file", + " --download enable automatic download of missing sources", + " --dbx relaxed Debian version handling: when checking for existing releases,", + " ignore prefixes like \"2:\" (epoch) and suffixes like \".debian\"", + " -client_id CLIENT_ID the SW360 client_id to be used for token generation", + " -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation" ] def __init__(self, onlyCreateReleases: bool = False) -> None: @@ -775,6 +777,20 @@ def run(self, args: Any) -> None: print_text("Using relaxed debian version checks") self.relaxed_debian_parsing = True + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=True) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + + if args.sw360_token and args.oauth2: + self.analyze_token(args.sw360_token) + 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) diff --git a/capycli/bom/findsources.py b/capycli/bom/findsources.py index d403089..caf709e 100644 --- a/capycli/bom/findsources.py +++ b/capycli/bom/findsources.py @@ -25,6 +25,7 @@ from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component from sw360 import SW360Error +from sw360.sw360keycloak import SW360Keycloak import capycli.common.script_base from capycli import get_logger @@ -743,15 +744,17 @@ def run(self, args: Any) -> None: print("usage: CaPyCli bom findsources [-h] [-v] [-o OUTPUTFILE] -i bomfile") print("") print("optional arguments:") - print(" -h, --help show this help message and exit") - print(" -i INPUTFILE SBOM file to read from (JSON)") - print(" -o OUTPUTFILE output file to write to") - print(" -t SW360_TOKEN (optional) use this token for access to SW360") - print(" -oa, --oauth2 (optional) this is an oauth2 token") - print(" -url SW360_URL (optional) use this URL for access to SW360") - print(" -name NAME (optional) GitHub name for login") - print(" -gt TOKEN (optional) GitHub token for login") - print(" -v be verbose") + print(" -h, --help show this help message and exit") + print(" -i INPUTFILE SBOM file to read from (JSON)") + print(" -o OUTPUTFILE output file to write to") + print(" -t SW360_TOKEN (opt.) use this token for access to SW360") + print(" -oa, --oauth2 (opt.) this is an oauth2 token") + print(" -url SW360_URL (opt.) use this URL for access to SW360") + print(" -name NAME (opt.) GitHub name for login") + print(" -gt TOKEN (opt.) GitHub token for login") + print(" -v be verbose") + print(" -client_id CLIENT_ID (opt.) the SW360 client_id to be used for token generation") + print(" -client_secret CLIENT_SECRET (opt.) the SW360 client_secret to be used for token generation") return if not args.inputfile: @@ -769,6 +772,17 @@ def run(self, args: Any) -> None: self.sw360_url = args.sw360_url if self.sw360_url: + if args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + self.login( token=args.sw360_token, url=self.sw360_url, oauth2=args.oauth2) print("Using SW360 releases and components to detect GitHub url") diff --git a/capycli/bom/map_bom.py b/capycli/bom/map_bom.py index 5f7ae67..313436a 100644 --- a/capycli/bom/map_bom.py +++ b/capycli/bom/map_bom.py @@ -21,7 +21,7 @@ from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component from packageurl import PackageURL -from sw360 import SW360 +from sw360 import SW360, SW360Keycloak import capycli.common.file_support import capycli.common.script_base @@ -868,6 +868,10 @@ def show_help(self) -> None: print(" version 3.1 will match SW360 version 3.1-3.debian") print(" -all deprecated, please use --matchmode all-versions") print(" --dbx deprecated, please use --matchmode ignore-debian") + print(" -client_id CLIENT_ID ") + print(" the SW360 client_id to be used for token generation") + print(" -client_secret CLIENT_SECRET") + print(" the SW360 client_secret to be used for token generation") def run(self, args: Any) -> None: """Main method()""" @@ -931,6 +935,17 @@ def run(self, args: Any) -> None: if self.verbosity > 1: print_text(" ", self.get_comp_count_text(sbom), "read from SBOM") + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + if args.sw360_token and args.oauth2: self.analyze_token(args.sw360_token) diff --git a/capycli/common/script_base.py b/capycli/common/script_base.py index 0dc35b7..e535f08 100644 --- a/capycli/common/script_base.py +++ b/capycli/common/script_base.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2019-2024 Siemens +# Copyright (c) 2019-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -78,6 +78,13 @@ def analyze_token(self, token: str) -> None: try: # alg = RS256 decoded = jwt.decode(token, algorithms=["HS256"], options={"verify_signature": False}) # type: ignore + if "scope" in decoded: + scope = decoded["scope"] + if scope.lower().find("write") >= 0: + print_text(" Token has write permissions") + else: + print_text(" Token has read permissions") + if "exp" in decoded: exp_seconds = int(decoded["exp"]) exp = datetime.fromtimestamp(exp_seconds) @@ -85,12 +92,23 @@ def analyze_token(self, token: str) -> None: # print(decoded) # { - # 'aud': ['sw360-REST-API'], - # 'user_name': 'thomas.graf@siemens.com', - # 'scope': ['READ', 'WRITE'], - # 'exp': 1581754268, - # 'authorities': ['READ', 'WRITE'], - # 'jti': 'bddc8951-bfae-475d-b2fd-04059b86598e', + # 'exp': 1776699510, + # 'iat': 1702769910, + # 'jti': 'trrtcc:6f1d3934-b319-1183-a059-8b7606f0a647', + # 'iss': 'https://stage.sw360.siemens.com/kc/realms/sw360', + # 'aud': 'account', + # 'sub': 'cf3fb608-4dba-42e0-bb89-7e13f995b931', + # 'typ': 'Bearer', + # 'azp': '7f75885d309970833f4187295d9babb8', + # 'acr': '1', + # 'realm_access': {'roles': ['default-roles-sw360', 'offline_access', 'uma_authorization']}, + # 'resource_access': {'account': {'roles': ['manage-account', 'manage-account-links', 'view-profile']}}, + # 'scope': ''READ profile email'', + # 'clientHost': '139.21.146.160' + # 'email_verified': False, + # 'preferred_username': 'service-account-7f75885d309970833f4187295d9babb8', + # 'clientAddress': '139.21.146.160', + # 'email': 'thomas.graf@siemens.com', # 'client_id': 'xxx' # } except Exception as ex: diff --git a/capycli/main/options.py b/capycli/main/options.py index 38eda84..c27cffb 100644 --- a/capycli/main/options.py +++ b/capycli/main/options.py @@ -444,6 +444,18 @@ def register_options(self) -> None: help="copy the project with the given id and the update it", ) + # used by all commands that need to access SW360 + self.parser.add_argument( + "-client_id", + dest="client_id", + help="the SW360 client_id to be used for token generation") + + # used by all commands that need to access SW360 + self.parser.add_argument( + "-client_secret", + dest="client_secret", + help="the SW360 client_secret to be used for token generation") + def read_config(self, filename: str = "", config_string: str = "") -> Dict[str, Any]: """ Read configuration from string or config file. diff --git a/capycli/project/check_prerequisites.py b/capycli/project/check_prerequisites.py index a6c37e9..8ac68cc 100644 --- a/capycli/project/check_prerequisites.py +++ b/capycli/project/check_prerequisites.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2019-2025 Siemens +# Copyright (c) 2019-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -13,6 +13,7 @@ from colorama import Fore from cyclonedx.model.bom import Bom +from sw360 import SW360Keycloak import capycli.common.script_base from capycli import get_logger @@ -301,17 +302,33 @@ def run(self, args: Any) -> None: print("Checks whether all prerequisites for a successful software clearing are fulfilled") print("") print("Options:") - print(" -h, --help show this help message and exit") - print(" -n NAME, --name NAME name of the project") - print(" -v VERSION, version of the project") - print(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") - print(" -i INPUTFILE SBOM input file to read from (JSON)") - print(" -t SW360_TOKEN, use this token for access to SW360") - print(" -oa, --oauth2 this is an oauth2 token") - print(" -url SW360_URL use this URL for access to SW360") - print(" --forceerror force an error exit code in case of prerequisite errors") + print(" -h, --help show this help message and exit") + print(" -n NAME, --name NAME name of the project") + print(" -v VERSION, version of the project") + print(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") + print(" -i INPUTFILE SBOM input file to read from (JSON)") + print(" -t SW360_TOKEN, use this token for access to SW360") + print(" -oa, --oauth2 this is an oauth2 token") + print(" -url SW360_URL use this URL for access to SW360") + print(" --forceerror force an error exit code in case of prerequisite errors") + print(" -client_id CLIENT_ID the SW360 client_id to be used for token generation") + print(" -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation") return + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + + if args.sw360_token and args.oauth2: + self.analyze_token(args.sw360_token) + 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) diff --git a/capycli/project/create_bom.py b/capycli/project/create_bom.py index 07fea3e..dd7bb0d 100644 --- a/capycli/project/create_bom.py +++ b/capycli/project/create_bom.py @@ -14,7 +14,7 @@ from cyclonedx.model.bom import Bom from cyclonedx.model.component import Component from packageurl import PackageURL -from sw360 import SW360Error +from sw360 import SW360Error, SW360Keycloak import capycli.common.script_base from capycli import get_logger @@ -162,13 +162,15 @@ def show_command_help(self) -> None: print("\nusage: CaPyCli project createbom [options]") print("Options:") print(""" - -id ID SW360 id of the project - -t SW360_TOKEN use this token for access to SW360 - -oa, this is an oauth2 token - -url SW360_URL use this URL for access to SW360 - -name name of the project, component or release - -version version of the project, component or release - -o OUTPUTFILE output file to write to + -id ID SW360 id of the project + -t SW360_TOKEN use this token for access to SW360 + -oa, this is an oauth2 token + -url SW360_URL use this URL for access to SW360 + -name name of the project, component or release + -version version of the project, component or release + -o OUTPUTFILE output file to write to + -client_id CLIENT_ID the SW360 client_id to be used for token generation + -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation """) print() @@ -192,6 +194,20 @@ def run(self, args: Any) -> None: self.show_command_help() return + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + + if args.sw360_token and args.oauth2: + self.analyze_token(args.sw360_token) + 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) diff --git a/capycli/project/create_project.py b/capycli/project/create_project.py index 61d45ce..bb5b7ce 100644 --- a/capycli/project/create_project.py +++ b/capycli/project/create_project.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2019-2025 Siemens +# Copyright (c) 2019-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -13,7 +13,7 @@ import requests from cyclonedx.model.bom import Bom -from sw360 import SW360Error +from sw360 import SW360Error, SW360Keycloak import capycli.common.script_base from capycli import get_logger @@ -322,17 +322,19 @@ def run(self, args: Any) -> None: print("usage: CaPyCli project create -i bom.json -o bom_created.json [-source ]") print("") print("optional arguments:") - print(" -i INPUTFILE, bom file to read from (JSON)") - print(" -t SW360_TOKEN, use this token for access to SW360") - print(" -oa, --oauth2 this is an oauth2 token") - print(" -url SW360_URL use this URL for access to SW360") - print(" -name NAME name of the project") - print(" -version VERSION, version of the project") - print(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") - print(" -old-version previous version") - print(" -source projectinfo.json additional information about the project to be created") - print(" -pms project mainline state for releases in a newly created project") - print(" --copy_from PROJECT_ID copy the project with the given id and the update it") + print(" -i INPUTFILE, bom file to read from (JSON)") + print(" -t SW360_TOKEN, use this token for access to SW360") + print(" -oa, --oauth2 this is an oauth2 token") + print(" -url SW360_URL use this URL for access to SW360") + print(" -name NAME name of the project") + print(" -version VERSION, version of the project") + print(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") + print(" -old-version previous version") + print(" -source projectinfo.json additional information about the project to be created") + print(" -pms project mainline state for releases in a newly created project") + print(" --copy_from PROJECT_ID copy the project with the given id and the update it") + print(" -client_id CLIENT_ID the SW360 client_id to be used for token generation") + print(" -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation") return if not args.inputfile: @@ -371,6 +373,20 @@ def run(self, args: Any) -> None: print_text("Project version will be updated with version: " + args.old_version) is_update_version = True + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=True) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + + if args.sw360_token and args.oauth2: + self.analyze_token(args.sw360_token) + 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) diff --git a/capycli/project/find_project.py b/capycli/project/find_project.py index 23aab8c..8033bfe 100644 --- a/capycli/project/find_project.py +++ b/capycli/project/find_project.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2019-23 Siemens +# Copyright (c) 2019-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -12,7 +12,7 @@ from typing import Any, Dict, Optional import requests -import sw360 +from sw360 import SW360Error, SW360Keycloak import capycli.common.script_base from capycli.common.print import print_red, print_text, print_yellow @@ -82,7 +82,7 @@ def check_project_id(self, project_id: str) -> None: print_text( "Project found, name = " + project["name"] + ", version = " + project["version"]) - except sw360.SW360Error as swex: + except SW360Error as swex: if swex.response is None: print_red("Unknown error: " + swex.message) elif swex.response.status_code == requests.codes['not_found']: @@ -114,12 +114,28 @@ def run(self, args: Any) -> None: print("usage: CaPyCli project find [-h] -t TOKEN -name NAME -version VERSION [-id PROJECT_ID]") print("") print("optional arguments:") - print(" -h, --help show this help message and exit") - print(" -name NAME name of the project") - print(" -version VERSION version of the project") - print(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") + print(" -h, --help show this help message and exit") + print(" -name NAME name of the project") + print(" -version VERSION version of the project") + print(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") + print(" -client_id CLIENT_ID the SW360 client_id to be used for token generation") + print(" -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation") return + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + + if args.sw360_token and args.oauth2: + self.analyze_token(args.sw360_token) + 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) diff --git a/capycli/project/get_license_info.py b/capycli/project/get_license_info.py index 2b1ccfa..890ad77 100644 --- a/capycli/project/get_license_info.py +++ b/capycli/project/get_license_info.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2019-2024 Siemens +# Copyright (c) 2019-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -12,7 +12,7 @@ import sys from typing import Any, Dict, List -from sw360 import SW360Error +from sw360 import SW360Error, SW360Keycloak import capycli.common.script_base from capycli.common.json_support import load_json_file @@ -197,6 +197,8 @@ def show_command_help(self) -> None: -nconf, --no-overwrite-config do not overwrite an existing configuration file -all add all available CLI files of a component --forceerror force an error exit code in case of missing information + -client_id CLIENT_ID the SW360 client_id to be used for token generation + -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation """) print() @@ -239,6 +241,20 @@ def run(self, args: Any) -> None: print_red("Input file not found!") sys.exit(ResultCode.RESULT_FILE_NOT_FOUND) + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + + if args.sw360_token and args.oauth2: + self.analyze_token(args.sw360_token) + 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) diff --git a/capycli/project/project_component_check.py b/capycli/project/project_component_check.py index fceb3e6..f1a3122 100644 --- a/capycli/project/project_component_check.py +++ b/capycli/project/project_component_check.py @@ -10,7 +10,7 @@ import sys from typing import Any -import sw360 +from sw360 import SW360Error, SW360Keycloak import capycli.common.script_base from capycli.bom.component_check import ComponentCheck @@ -71,7 +71,7 @@ def check_bom_items(self, project_id: str) -> int: try: self.project = self.client.get_project(project_id) - except sw360.SW360Error as swex: + except SW360Error as swex: print_red(" ERROR: unable to access project: " + repr(swex)) sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360) @@ -120,17 +120,19 @@ def run(self, args: Any) -> None: "[-v] [-id PROJECT_ID] [-rcl URL] [-lcl FILE]") print_text("") print_text("optional arguments:") - print_text(" -h, --help show this help message and exit") - print_text(" -name NAME name of the project") - print_text(" -version VERSION version of the project") - print_text(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") - print_text(" -t SW360_TOKEN use this token for access to SW360") - print_text(" -oa, this is an oauth2 token") - print_text(" -url SW360_URL use this URL for access to SW360") - print_text(" -v be verbose") - print_text(" -rcl read the component check list file from the URL specified") - print_text(" -lcl read the component check list file from local") - print_text(" --forceerror force an error exit code in case of validation errors or warnings") + print_text(" -h, --help show this help message and exit") + print_text(" -name NAME name of the project") + print_text(" -version VERSION version of the project") + print_text(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") + print_text(" -t SW360_TOKEN use this token for access to SW360") + print_text(" -oa, this is an oauth2 token") + print_text(" -url SW360_URL use this URL for access to SW360") + print_text(" -v be verbose") + print_text(" -rcl read the component check list file from the URL specified") + print_text(" -lcl read the component check list file from local") + print_text(" --forceerror force an error exit code in case of validation errors or warnings") + print_text(" -client_id CLIENT_ID the SW360 client_id to be used for token generation") + print_text(" -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation") return self.verbose = args.verbose @@ -148,6 +150,20 @@ def run(self, args: Any) -> None: self.component_check.files_to_ignore = self.component_check.component_check_list.get("files_to_ignore", []) print_text(f" {len(self.component_check.files_to_ignore)} components will be ignored.") + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + + if args.sw360_token and args.oauth2: + self.analyze_token(args.sw360_token) + 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) diff --git a/capycli/project/show_ecc.py b/capycli/project/show_ecc.py index a45d996..5d2e60f 100644 --- a/capycli/project/show_ecc.py +++ b/capycli/project/show_ecc.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2022-2024 Siemens +# Copyright (c) 2022-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -10,7 +10,7 @@ import sys from typing import Any, Dict -import sw360 +from sw360 import SW360Error, SW360Keycloak import capycli.common.script_base from capycli.common.json_support import write_json_to_file @@ -82,7 +82,7 @@ def get_project_status(self, project_id: str) -> Dict[str, Any]: try: self.project = self.client.get_project(project_id) - except sw360.SW360Error as swex: + except SW360Error as swex: print_red(" ERROR: unable to access project: " + repr(swex)) sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360) @@ -133,7 +133,7 @@ def get_project_status(self, project_id: str) -> Dict[str, Any]: rel_item["EccStatus"] = eccinfo.get("eccStatus", "UNKNOWN") rel_item["AL"] = eccinfo.get("al", "UNKNOWN") rel_item["ECCN"] = eccinfo.get("eccn", "UNKNOWN") - except sw360.SW360Error as swex: + except SW360Error as swex: print_red(" ERROR: unable to access project:" + repr(swex)) sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360) @@ -171,13 +171,29 @@ def run(self, args: Any) -> None: print("usage: CaPyCli project ecc [-h] -name NAME -version VERSION [-id PROJECT_ID] [-o OUTPUTFILE]") print("") print("optional arguments:") - print(" -h, --help show this help message and exit") - print(" -n NAME, --name NAME name of the project") - print(" -v VERSION, version of the project") - print(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") - print(" -o OUTPUTFILE output file to write project details to") + print(" -h, --help show this help message and exit") + print(" -n NAME, --name NAME name of the project") + print(" -v VERSION, version of the project") + print(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") + print(" -o OUTPUTFILE output file to write project details to") + print(" -client_id CLIENT_ID the SW360 client_id to be used for token generation") + print(" -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation") return + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + + if args.sw360_token and args.oauth2: + self.analyze_token(args.sw360_token) + 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) diff --git a/capycli/project/show_licenses.py b/capycli/project/show_licenses.py index 6d146f3..06e1697 100644 --- a/capycli/project/show_licenses.py +++ b/capycli/project/show_licenses.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2019-24 Siemens +# Copyright (c) 2019-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -15,6 +15,7 @@ from cli_support import CliFile from colorama import Fore, Style +from sw360 import SW360Keycloak import capycli.common.script_base from capycli.common.print import print_red, print_text, print_yellow @@ -176,12 +177,14 @@ def show_command_help(self) -> None: print("\nusage: CaPyCli project licenses [options]") print("Options:") print(""" - -id ID SW360 id of the project - -t SW360_TOKEN use this token for access to SW360 - -oa, this is an oauth2 token - -url SW360_URL use this URL for access to SW360 - -name name of the project, component or release - -version version of the project, component or release + -id ID SW360 id of the project + -t SW360_TOKEN use this token for access to SW360 + -oa, this is an oauth2 token + -url SW360_URL use this URL for access to SW360 + -name name of the project, component or release + -version version of the project, component or release + -client_id CLIENT_ID the SW360 client_id to be used for token generation + -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation """) print() @@ -205,6 +208,20 @@ def run(self, args: Any) -> None: self.show_command_help() return + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + + if args.sw360_token and args.oauth2: + self.analyze_token(args.sw360_token) + 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) diff --git a/capycli/project/show_project.py b/capycli/project/show_project.py index 5ad0d1d..03e75aa 100644 --- a/capycli/project/show_project.py +++ b/capycli/project/show_project.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2019-24 Siemens +# Copyright (c) 2019-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -10,8 +10,8 @@ import sys from typing import Any, Dict, Optional -import sw360 from colorama import Fore +from sw360 import SW360Error, SW360Keycloak import capycli.common.json_support import capycli.common.script_base @@ -87,7 +87,7 @@ def get_project_status(self, project_id: str) -> Dict[str, Any]: try: self.project = self.client.get_project(project_id) - except sw360.SW360Error as swex: + except SW360Error as swex: print_red(" ERROR: unable to access project: " + repr(swex)) sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360) @@ -149,7 +149,7 @@ def get_project_status(self, project_id: str) -> Dict[str, Any]: for key in att: if key.get("attachmentType", "") == "SOURCE": rel_item["SourceAvailable"] = "True" - except sw360.SW360Error as swex: + except SW360Error as swex: print_red(" ERROR: unable to access project:" + repr(swex)) sys.exit(ResultCode.RESULT_ERROR_ACCESSING_SW360) @@ -188,16 +188,32 @@ def run(self, args: Any) -> None: "[-id PROJECT_ID] [-o OUTPUTFILE]") print("") print("optional arguments:") - print(" -h, --help show this help message and exit") - print(" -name NAME name of the project") - print(" -version VERSION version of the project") - print(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") - print(" -t SW360_TOKEN use this token for access to SW360") - print(" -oa, this is an oauth2 token") - print(" -url SW360_URL use this URL for access to SW360") - print(" -o OUTPUTFILE output file to write project details to") + print(" -h, --help show this help message and exit") + print(" -name NAME name of the project") + print(" -version VERSION version of the project") + print(" -id PROJECT_ID SW360 id of the project, supersedes name and version parameters") + print(" -t SW360_TOKEN use this token for access to SW360") + print(" -oa, this is an oauth2 token") + print(" -url SW360_URL use this URL for access to SW360") + print(" -o OUTPUTFILE output file to write project details to") + print(" -client_id CLIENT_ID the SW360 client_id to be used for token generation") + print(" -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation") return + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + + if args.sw360_token and args.oauth2: + self.analyze_token(args.sw360_token) + 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) diff --git a/capycli/project/show_vulnerabilities.py b/capycli/project/show_vulnerabilities.py index 6512573..e807835 100644 --- a/capycli/project/show_vulnerabilities.py +++ b/capycli/project/show_vulnerabilities.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# Copyright (c) 2020-2024 Siemens +# Copyright (c) 2020-2026 Siemens # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -12,7 +12,7 @@ import requests from colorama import Fore, Style -from sw360 import SW360Error +from sw360 import SW360Error, SW360Keycloak import capycli.common.json_support import capycli.common.script_base @@ -206,16 +206,18 @@ def show_command_help(self) -> None: print("\nusage: CaPyCli project vulnerabilities [options]") print("Options:") print(""" - -id ID SW360 id of the project - -t SW360_TOKEN use this token for access to SW360 - -oa, this is an oauth2 token - -url SW360_URL use this URL for access to SW360 - -name name of the project, component or release - -version version of the project, component or release - -v be verbose - -format FMT output format, one of [text, json], default is text - -fe PRIO minimum vulnerability priority to force exit code != 0 - """) + -id ID SW360 id of the project + -t SW360_TOKEN use this token for access to SW360 + -oa, this is an oauth2 token + -url SW360_URL use this URL for access to SW360 + -name name of the project, component or release + -version version of the project, component or release + -v be verbose + -format FMT output format, one of [text, json], default is text + -fe PRIO minimum vulnerability priority to force exit code != 0 + -client_id CLIENT_ID the SW360 client_id to be used for token generation + -client_secret CLIENT_SECRET the SW360 client_secret to be used for token generation + """) print() @@ -245,6 +247,20 @@ def run(self, args: Any) -> None: if args.verbose: print("Output format is", self.format) + if not args.sw360_token and args.client_id and args.client_secret: + print_text("Creating token using client id and secret...") + kc = SW360Keycloak(args.sw360_url) + args.sw360_token = kc.get_keycloak_token(args.client_id, args.client_secret, write_access=False) + if args.sw360_token: + args.oauth2 = True + print_text(" Got token.") + else: + print_red(" Failed to get token!") + sys.exit(ResultCode.RESULT_AUTH_ERROR) + + if args.sw360_token and args.oauth2: + self.analyze_token(args.sw360_token) + 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) diff --git a/tests/test_base.py b/tests/test_base.py index 390248d..2b549df 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -71,6 +71,8 @@ def __init__(self) -> None: self.copy_from = "" self.remote_check_list: str = "" self.local_checklist_list: str = "" + self.client_id: str = "" + self.client_secret: str = "" class TestBasePytest: diff --git a/tox.ini b/tox.ini index 494a822..702019a 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # note: encoding must be UTF-8 without any BOM [flake8] -max-line-length = 120 +max-line-length = 130 ignore = W504, W503 exclude = Legacy, xxcli, xxsw360, demo extend-exclude = .venv