Skip to content
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
2 changes: 2 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 21 additions & 9 deletions capycli/bom/check_bom.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -------------------------------------------------------------------------------
# Copyright (c) 2019-2024 Siemens
# Copyright (c) 2019-2026 Siemens
# All Rights Reserved.
# Author: thomas.graf@siemens.com
#
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand Down
23 changes: 18 additions & 5 deletions capycli/bom/check_bom_item_status.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -------------------------------------------------------------------------------
# Copyright (c) 2019-2024 Siemens
# Copyright (c) 2019-2026 Siemens
# All Rights Reserved.
# Author: thomas.graf@siemens.com
#
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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)

Expand Down
42 changes: 29 additions & 13 deletions capycli/bom/create_components.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -------------------------------------------------------------------------------
# Copyright (c) 2019-2025 Siemens
# Copyright (c) 2019-2026 Siemens
# All Rights Reserved.
# Author: thomas.graf@siemens.com
#
Expand All @@ -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
Expand All @@ -43,17 +43,19 @@ class BomCreateComponents(capycli.common.script_base.ScriptBase):
"usage: CaPyCLI bom {} -i bom.json -o bom_created.json [-source <folder>]",
"",
"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:
Expand Down Expand Up @@ -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)
Expand Down
32 changes: 23 additions & 9 deletions capycli/bom/findsources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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")
Expand Down
17 changes: 16 additions & 1 deletion capycli/bom/map_bom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()"""
Expand Down Expand Up @@ -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)

Expand Down
32 changes: 25 additions & 7 deletions capycli/common/script_base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -------------------------------------------------------------------------------
# Copyright (c) 2019-2024 Siemens
# Copyright (c) 2019-2026 Siemens
# All Rights Reserved.
# Author: thomas.graf@siemens.com
#
Expand Down Expand Up @@ -78,19 +78,37 @@ 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)
print_text(" Token will expire on " + str(exp))

# 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:
Expand Down
12 changes: 12 additions & 0 deletions capycli/main/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading