Repository: open-component-model/delivery-service
Pull request #350: Factor out bdba-client to dedicated package
Author: login: zkdev
Created: 2025-02-07T15:14:33Z | Merged: 2025-03-10T10:06:52Z

**What this PR does / why we need it**:

**Which issue(s) this PR fixes**:
Fixes #

**Special notes for your reviewer**:

**Release note**:
<!--  Write your release note:
1. Enter your release note in the below block.
2. If no release note is required, just write "NONE" within the block.

Format of block header: <category> <target_group>
Possible values:
- category:       improvement|noteworthy|action
- target_group:   user|operator|developer
-->
```noteworthy developer
BDBA-Client can now be consumed standalone, via python distribution package `bdba`
```



Diff:
diff --git a/.ci/build_bdba_python_package b/.ci/build_bdba_python_package
new file mode 100755
index 00000000..1420356d
--- /dev/null
+++ b/.ci/build_bdba_python_package
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+
+set -eu
+
+repo_dir="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")/..")"
+out_dir="$(readlink -f "${repo_dir}/../${PACKAGES_PATH:-}/dist")"
+mkdir "${out_dir}"
+
+apk add --no-cache xz
+
+cd $repo_dir
+
+# install build-only dependencies (no need to include in image)
+pip3 install --upgrade pip wheel setuptools semver
+
+# build into "${repo_dir}/dist"
+python3 "${repo_dir}/setup.bdba.py" bdist_wheel
+rm -rf "${repo_dir}/build"
+
+# keep for subsequent docker build
+cp dist/* ${out_dir}
diff --git a/.ci/component_descriptor b/.ci/component_descriptor
index da352ce9..56565779 100755
--- a/.ci/component_descriptor
+++ b/.ci/component_descriptor
@@ -16,6 +16,7 @@ own_dir = os.path.abspath(os.path.dirname(__file__))
 repo_dir = os.path.abspath(os.path.join(own_dir, os.pardir))
 CC_UTILS_VERSIONFILE = os.path.join(repo_dir, 'CC_UTILS_VERSION')
 FRESHCLAM_VERSIONFILE = os.path.join(repo_dir, 'FRESHCLAM_VERSION')
+BDBA_VERSIONFILE = os.path.join(repo_dir, 'BDBA_DEPENDENCY_VERSION')
 
 
 def parse_component_descriptor():
@@ -51,7 +52,15 @@ def main():
         labels=[],
     )
 
+    bdba_cref = ocm.ComponentReference(
+        name='bdba',
+        componentName='ocm.software/ocm-gear/bdba-client',
+        version=version_from_file(BDBA_VERSIONFILE),
+        labels=[],
+    )
+
     for cref in (
+        bdba_cref,
         cc_utils_cref,
         freshclam_cref,
     ):
diff --git a/.ci/pipeline_definitions b/.ci/pipeline_definitions
index 4ef214fa..119019ac 100644
--- a/.ci/pipeline_definitions
+++ b/.ci/pipeline_definitions
@@ -137,3 +137,49 @@ delivery-service:
                 - setuptools
                 - pip
                 - distribution-package
+
+bdba:
+  jobs:
+    manual-release:
+      repo:
+        disable_ci_skip: True
+
+      steps:
+        build_bdba_python_package:
+          output_dir: packages
+
+      traits:
+        component_descriptor:
+          component_name: ocm.software/ocm-gear/bdba-client
+          ocm_repository: europe-docker.pkg.dev/gardener-project/releases
+          ocm_repository_mappings:
+          - repository: europe-docker.pkg.dev/gardener-project/releases
+            prefix: ''
+          - repository: europe-docker.pkg.dev/gardener-project/snapshots
+            prefix: ''
+
+        version:
+          preprocess: finalize
+          inject_effective_version: True
+          versionfile: BDBA_VERSION
+
+        release:
+          nextversion: bump_minor
+          release_notes_policy: disabled
+          release_commit_publishing_policy: tag_and_merge_back
+          git_tags:
+          - ref_template: refs/tags/bdba-{VERSION}
+          release_on_github: False
+          assets:
+            - type: build-step-file
+              mode: tar
+              step_name: build_bdba_python_package
+              step_output_dir: packages
+              path: 'dist/bdba*'
+              prefix: dist/
+              name: bdba
+              purposes:
+                - python
+                - setuptools
+                - pip
+                - distribution-package
diff --git a/.ci/set_dependency_version b/.ci/set_dependency_version
index cca25e9b..9bec58ea 100755
--- a/.ci/set_dependency_version
+++ b/.ci/set_dependency_version
@@ -8,6 +8,7 @@ import yaml
 repo_dir = os.path.abspath(os.environ['MAIN_REPO_DIR'])
 CC_UTILS_VERSIONFILE = os.path.join(repo_dir, 'CC_UTILS_VERSION')
 FRESHCLAM_VERSIONFILE = os.path.join(repo_dir, 'FRESHCLAM_VERSION')
+BDBA_VERSIONFILE = os.path.join(repo_dir, 'BDBA_DEPENDENCY_VERSION')
 REQUIREMENTS_FILE = os.path.join(repo_dir, 'requirements.utils.txt')
 EXTENSIONS_CHART_DIR = os.path.join(repo_dir, 'charts', 'extensions')
 
@@ -52,6 +53,15 @@ def main():
         write_component_version_to_file(CC_UTILS_VERSIONFILE)
         write_package_version_to_requirements_file(package_name, REQUIREMENTS_FILE)
 
+    elif dependency_name == 'ocm.software/ocm-gear/bdba-client':
+        python_distribution_package_name = 'bdba'
+
+        write_component_version_to_file(BDBA_VERSIONFILE)
+        write_package_version_to_requirements_file(
+            package_name=python_distribution_package_name,
+            file=REQUIREMENTS_FILE,
+        )
+
     elif dependency_name == 'ocm.software/ocm-gear/freshclam':
         write_component_version_to_file(FRESHCLAM_VERSIONFILE)
 
diff --git a/BDBA_DEPENDENCY_VERSION b/BDBA_DEPENDENCY_VERSION
new file mode 100644
index 00000000..6e8bf73a
--- /dev/null
+++ b/BDBA_DEPENDENCY_VERSION
@@ -0,0 +1 @@
+0.1.0
diff --git a/BDBA_VERSION b/BDBA_VERSION
new file mode 100644
index 00000000..49ffebca
--- /dev/null
+++ b/BDBA_VERSION
@@ -0,0 +1 @@
+0.1.0-dev
\ No newline at end of file
diff --git a/bdba/__init__.py b/bdba/__init__.py
index 5b46d874..e69de29b 100644
--- a/bdba/__init__.py
+++ b/bdba/__init__.py
@@ -1,3 +0,0 @@
-# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
-#
-# SPDX-License-Identifier: Apache-2.0
diff --git a/bdba/client.py b/bdba/client.py
index 4c949a3d..8ef50393 100644
--- a/bdba/client.py
+++ b/bdba/client.py
@@ -3,6 +3,7 @@
 # SPDX-License-Identifier: Apache-2.0
 
 
+import cachecontrol
 import collections.abc
 import datetime
 import enum
@@ -11,23 +12,17 @@
 import time
 import traceback
 import urllib.parse
+import urllib3.util.retry
 
 import dacite
 import dateutil.parser
 import requests
 
-import ci.log
-import http_requests
-
 import bdba.model as bm
-import ctx_util
-import secret_mgmt
-import secret_mgmt.bdba
-import util
+import bdba.util as bu
 
 
 logger = logging.getLogger(__name__)
-ci.log.configure_default_logging()
 
 
 def kebab_to_snake_case_keys(d: dict[str, dict | list | str | int]) -> dict:
@@ -71,12 +66,12 @@ def __init__(self, base_url):
         self._rest_url = functools.partial(self._url, 'rest')
 
     def _url(self, *parts):
-        return util.urljoin(self._base_url, *parts)
+        return bu.urljoin(self._base_url, *parts)
 
     def apps(self, group_id=None, custom_attribs={}):
         url = self._api_url('apps')
         if group_id is not None:
-            url = util.urljoin(url, str(group_id))
+            url = bu.urljoin(url, str(group_id))
 
         search_query = ' '.join(['meta:' + str(k) + '=' + str(v) for k,v in custom_attribs.items()])
         if search_query:
@@ -117,28 +112,98 @@ def scans(self, product_id: int):
         return self._rest_url('scans', str(product_id)) + '/'
 
 
+def check_http_code(function):
+    '''
+    a decorator that will check on `requests.Response` instances returned by HTTP requests
+    issued with `requests`. In case the response code indicates an error, a warning is logged
+    and a `requests.HTTPError` is raised.
+
+    @param: the function to wrap; should be `requests.<http-verb>`, e.g. requests.get
+    @raises: `requests.HTTPError` if response's status code indicates an error
+    '''
+    @functools.wraps(function)
+    def http_checker(*args, **kwargs):
+        result = function(*args, **kwargs)
+        if not result.ok:
+            url = kwargs.get('url', None)
+            logger.warning(f'{result.status_code=} - {result.content=}: {url=}')
+        result.raise_for_status()
+        return result
+    return http_checker
+
+
+class LoggingRetry(urllib3.util.retry.Retry):
+    def __init__(
+        self,
+        **kwargs,
+    ):
+        defaults = dict(
+            total=3,
+            connect=3,
+            read=3,
+            status=3,
+            redirect=False,
+            status_forcelist=(429, 500, 502, 503, 504),
+            raise_on_status=False,
+            respect_retry_after_header=True,
+            backoff_factor=1.0,
+        )
+
+        super().__init__(**(defaults | kwargs))
+
+    def increment(self,
+        method=None,
+        url=None,
+        response=None,
+        error=None,
+        _pool=None,
+        _stacktrace=None
+    ):
+        # super().increment will either raise an exception indicating that no retry is to
+        # be performed or return a new, modified instance of this class
+        retry = super().increment(method, url, response, error, _pool, _stacktrace)
+        # Use the Retry history to determine the number of retries.
+        num_retries = len(self.history) if self.history else 0
+        logger.warning(
+            f'{method=} {url=} returned {response=} {error=} {num_retries=} - trying again'
+        )
+        return retry
+
+
+def _mount_default_adapter(
+    session: requests.Session,
+    connection_pool_cache_size=32, # requests-library default
+    max_pool_size=32, # requests-library default
+    retry_cfg: urllib3.util.retry.Retry=LoggingRetry(),
+):
+    http_adapter = cachecontrol.CacheControlAdapter(
+        max_retries=retry_cfg,
+        pool_connections=connection_pool_cache_size,
+        pool_maxsize=max_pool_size,
+    )
+
+    session.mount('http://', http_adapter)
+    session.mount('https://', http_adapter)
+
+    return session
+
+
 class BDBAApi:
     def __init__(
         self,
         api_routes: BDBAApiRoutes,
-        bdba_cfg: secret_mgmt.bdba.BDBA,
+        token: str,
+        tls_verify: bool=True,
     ):
         self._routes = api_routes
-        self._token = bdba_cfg.token
-        self._tls_verify = bdba_cfg.tls_verify
+        self._token = token
+        self._tls_verify = tls_verify
         self._session = requests.Session()
-        http_requests.mount_default_adapter(
-            session=self._session,
-        )
-
-    def set_maximum_concurrent_connections(self, maximum_concurrent_connections: int):
-        # mount new adapter with new parameters
-        http_requests.mount_default_adapter(
+        _mount_default_adapter(
             session=self._session,
-            max_pool_size=maximum_concurrent_connections,
         )
 
-    @http_requests.check_http_code
+    @check_http_code
     def _request(self, method, *args, **kwargs):
         if 'headers' in kwargs:
             headers = kwargs['headers']
@@ -161,23 +226,23 @@ def _request(self, method, *args, **kwargs):
             timeout=timeout,
         )(*args, **kwargs)
 
-    @http_requests.check_http_code
+    @check_http_code
     def _get(self, *args, **kwargs):
         return self._request(self._session.get, *args, **kwargs)
 
-    @http_requests.check_http_code
+    @check_http_code
     def _post(self, *args, **kwargs):
         return self._request(self._session.post, *args, **kwargs)
 
-    @http_requests.check_http_code
+    @check_http_code
     def _put(self, *args, **kwargs):
         return self._request(self._session.put, *args, **kwargs)
 
-    @http_requests.check_http_code
+    @check_http_code
     def _delete(self, *args, **kwargs):
         return self._request(self._session.delete, *args, **kwargs)
 
-    @http_requests.check_http_code
+    @check_http_code
     def _patch(self, *args, **kwargs):
         return self._request(self._session.patch, *args, **kwargs)
 
@@ -507,43 +572,3 @@ def pdf_report(self, product_id: int, cvss_version: bm.CVSSVersion=bm.CVSSVersio
         )
 
         return response.content
-
-
-def client(
-    bdba_cfg: secret_mgmt.bdba.BDBA | None=None,
-    group_id: int | None=None,
-    url: str | None=None,
-    secret_factory: secret_mgmt.SecretFactory | None=None,
-) -> BDBAApi:
-    '''
-    Either pass `bdba_cfg` directly (or reference by name) or lookup `bdba_cfg` based on `group_id`
-    and `url`, most specific cfg wins.
-    '''
-
-    if not secret_factory:
-        secret_factory = ctx_util.secret_factory()
-
-    if bdba_cfg:
-        if isinstance(bdba_cfg, str):
-            bdba_cfg = secret_factory.bdba(bdba_cfg)
-
-        if bdba_cfg.matches(
-            group_id=group_id,
-            url=url,
-        ) is secret_mgmt.bdba.MatchScore.NO_MATCH:
-            raise ValueError(f'BDBA cfg does not match {group_id=} and {url=}')
-
-    else:
-        if not (bdba_cfg := secret_mgmt.bdba.find_cfg(
-            secret_factory=secret_factory,
-            group_id=group_id,
-            url=url,
-        )):
-            raise ValueError(f'No BDBA cfg found for {group_id=} and {url=}')
-
-    routes = BDBAApiRoutes(base_url=bdba_cfg.api_url)
-    api = BDBAApi(
-        api_routes=routes,
-        bdba_cfg=bdba_cfg,
-    )
-    return api
diff --git a/bdba/model.py b/bdba/model.py
index 3cdf74b3..7bad5cdf 100644
--- a/bdba/model.py
+++ b/bdba/model.py
@@ -8,17 +8,11 @@
 import datetime
 import enum
 import logging
-import traceback
 
 import dacite
 import dateutil.parser
 
-import dso.cvss
-import dso.labels
-import dso.model
-import ocm
-
-import util
+import bdba.util as bu
 
 
 logger = logging.getLogger()
@@ -71,7 +65,7 @@ class Triage:
     reason: str
     description: str | None
     modified: datetime.datetime
-    user: dso.model.BDBAUser
+    user: dict
 
     def __repr__(self):
         return (
@@ -120,12 +114,12 @@ def cve_severity(
             raise ValueError(f'{cvss_version} not supported')
 
     @property
-    def cvss(self) -> dso.cvss.CVSSV3 | None:
+    def cvss(self) -> dict | None:
         # ignore cvss2_vector for now
         if not (cvss_vector := self.vuln.get('cvss3_vector')):
             return None
 
-        return dso.cvss.CVSSV3.parse(cvss_vector)
+        return cvss_vector
 
     @property
     def summary(self) -> str:
@@ -231,7 +225,7 @@ class Result:
 
     @property
     def base_url(self) -> str:
-        parsed_url = util.urlparse(self.report_url)
+        parsed_url = bu.urlparse(self.report_url)
         return f'{parsed_url.scheme}://{parsed_url.netloc}'
 
     @property
@@ -259,64 +253,3 @@ class UploadStatus(enum.IntEnum):
     SKIPPED = 1
     PENDING = 2
     DONE = 4
-
-
-@dataclasses.dataclass
-class ScanRequest:
-    '''
-    a scan request of an artefact (referenced by component and artefact).
-
-    if a previous scan result was found, its "product-id" is stored as `target_product_id`
-    '''
-    component: ocm.Component
-    artefact: ocm.Artifact
-    # The actual content to be scanned.
-    scan_content: collections.abc.Generator[bytes, None, None]
-    display_name: str
-    target_product_id: int | None
-    custom_metadata: dict
-
-    @property
-    def skip_vulnerability_scan(self) -> bool:
-        # hardcode skip-info to be determined by artefact
-        artefact = self.artefact
-
-        if not (label := artefact.find_label(name=dso.labels.BinaryIdScanLabel.name)):
-            return False
-
-        label: dso.labels.BinaryIdScanLabel = dso.labels.deserialise_label(label=label)
-        return label.value.policy is dso.labels.ScanPolicy.SKIP
-
-    def __str__(self):
-        return (
-            f"ScanRequest(name='{self.display_name}', target_product_id='{self.target_product_id}' "
-            f"custom_metadata='{self.custom_metadata}')"
-        )
-
-
-class BdbaScanError(Exception):
-    def __init__(
-        self,
-        scan_request: ScanRequest,
-        component: ocm.Component,
-        artefact: ocm.Artifact,
-        exception=None,
-        *args,
-        **kwargs,
-    ):
-        self.scan_request = scan_request
-        self.component = component
-        self.artefact = artefact
-        self.exception = exception
-
-        super().__init__(*args, **kwargs)
-
-    def print_stacktrace(self):
-        c = self.component
-        a = self.artefact
-        name = f'{c.name}/{a.name}:{a.version}'
-
-        if not self.exception:
-            return name + ' - no exception available'
-
-        return name + '\n' + ''.join(traceback.format_tb(self.exception.__traceback__))
diff --git a/bdba/util.py b/bdba/util.py
index 4563bed3..fb2954db 100644
--- a/bdba/util.py
+++ b/bdba/util.py
@@ -1,360 +1,22 @@
-# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
-#
-# SPDX-License-Identifier: Apache-2.0
+import urllib.parse
 
 
-import collections.abc
-import dataclasses
-import datetime
-import logging
+def urljoin(*parts):
+    if len(parts) == 1:
+        return parts[0]
+    first = parts[0]
+    last = parts[-1]
+    middle = parts[1:-1]
 
-import ci.log
-import cnudie.iter
-import delivery.client
-import dso.model
+    first = first.rstrip('/')
+    middle = list(map(lambda s: s.strip('/'), middle))
+    last = last.lstrip('/')
 
-import bdba.model as bm
-import odg.findings
+    return '/'.join([first] + middle + [last])
 
 
-logger = logging.getLogger(__name__)
-ci.log.configure_default_logging(print_thread_id=True)
+def urlparse(url: str) -> urllib.parse.ParseResult:
+    if not '://' in url:
+        url = f'x://{url}'
 
-
-def iter_existing_findings(
-    delivery_client: delivery.client.DeliveryServiceClient,
-    resource_node: cnudie.iter.ResourceNode,
-    finding_type: str | tuple[str],
-    datasource: str=dso.model.Datasource.BDBA,
-) -> collections.abc.Generator[dso.model.ArtefactMetadata, None, None]:
-    artefact = dso.model.component_artefact_id_from_ocm(
-        component=resource_node.component_id,
-        artefact=resource_node.resource,
-    )
-
-    findings = delivery_client.query_metadata(
-        artefacts=(artefact,),
-        type=finding_type,
-    )
-
-    return (
-        finding for finding in findings
-        if finding.meta.datasource == datasource
-    )
-
-
-def iter_artefact_metadata(
-    scanned_element: cnudie.iter.ResourceNode,
-    scan_result: bm.AnalysisResult,
-    delivery_client: delivery.client.DeliveryServiceClient=None,
-    vulnerability_cfg: odg.findings.Finding | None=None,
-    license_cfg: odg.findings.Finding | None=None,
-) -> collections.abc.Generator[dso.model.ArtefactMetadata, None, None]:
-    now = datetime.datetime.now(tz=datetime.timezone.utc)
-    discovery_date = datetime.date.today()
-    datasource = dso.model.Datasource.BDBA
-
-    artefact_ref = dso.model.component_artefact_id_from_ocm(
-        component=scanned_element.component,
-        artefact=scanned_element.resource,
-    )
-
-    # don't include component version here since it is also not considered in the BDBA scan
-    # -> this will deduplicate findings of the same artefact version across different
-    # component versions
-    finding_artefact_ref = dataclasses.replace(
-        artefact_ref,
-        component_version=None,
-    )
-
-    # rescoring should not reference artefact version so that the `ARTEFACT` rescoring scope will be
-    # used -> rescoring will be used for future versions as well, so there is no need to replicate
-    # BDBA triages to new BDBA scans
-    rescoring_artefact_ref = dataclasses.replace(
-        finding_artefact_ref,
-        artefact=dataclasses.replace(
-            finding_artefact_ref.artefact,
-            artefact_version=None,
-        ),
-    )
-
-    yield dso.model.artefact_scan_info(
-        artefact_node=scanned_element,
-        datasource=datasource,
-        data={
-            'report_url': scan_result.report_url,
-        },
-    )
-
-    findings: list[dso.model.ArtefactMetadata] = []
-    for package in scan_result.components:
-        package_name = package.name
-        package_version = package.version
-
-        filesystem_paths = list(iter_filesystem_paths(component=package))
-
-        licenses = list({
-            dso.model.License(
-                name=license.name,
-            ) for license in package.iter_licenses
-        })
-
-        meta = dso.model.Metadata(
-            datasource=datasource,
-            type=dso.model.Datatype.STRUCTURE_INFO,
-            creation_date=now,
-        )
-
-        structure_info = dso.model.StructureInfo(
-            package_name=package_name,
-            package_version=package_version,
-            base_url=scan_result.base_url,
-            report_url=scan_result.report_url,
-            product_id=scan_result.product_id,
-            group_id=scan_result.group_id,
-            licenses=licenses,
-            filesystem_paths=filesystem_paths,
-        )
-
-        yield dso.model.ArtefactMetadata(
-            artefact=finding_artefact_ref,
-            meta=meta,
-            data=structure_info,
-            discovery_date=discovery_date,
-        )
-
-        if license_cfg and license_cfg.matches(artefact_ref):
-            meta = dso.model.Metadata(
-                datasource=datasource,
-                type=odg.findings.FindingType.LICENSE,
-                creation_date=now,
-            )
-
-            for license in licenses:
-                categorisation = odg.findings.categorise_finding(
-                    finding_cfg=license_cfg,
-                    finding_property=license.name,
-                )
-
-                if not categorisation:
-                    continue
-
-                license_finding = dso.model.LicenseFinding(
-                    package_name=package_name,
-                    package_version=package_version,
-                    base_url=scan_result.base_url,
-                    report_url=scan_result.report_url,
-                    product_id=scan_result.product_id,
-                    group_id=scan_result.group_id,
-                    severity=categorisation.id,
-                    license=license,
-                )
-
-                artefact_metadata = dso.model.ArtefactMetadata(
-                    artefact=finding_artefact_ref,
-                    meta=meta,
-                    data=license_finding,
-                    discovery_date=discovery_date,
-                )
-
-                findings.append(artefact_metadata)
-                yield artefact_metadata
-
-        if vulnerability_cfg and vulnerability_cfg.matches(artefact_ref):
-            meta = dso.model.Metadata(
-                datasource=datasource,
-                type=odg.findings.FindingType.VULNERABILITY,
-                creation_date=now,
-            )
-
-            for vulnerability in package.vulnerabilities:
-                if vulnerability.okay_to_skip:
-                    continue # we only support active vulnerabilities with a valid cvss v3 vector
-
-                categorisation = odg.findings.categorise_finding(
-                    finding_cfg=vulnerability_cfg,
-                    finding_property=vulnerability.cve_severity(),
-                )
-
-                if not categorisation:
-                    continue
-
-                for triage in vulnerability.triages:
-                    meta_rescoring = dso.model.Metadata(
-                        datasource=datasource,
-                        type=dso.model.Datatype.RESCORING,
-                        creation_date=triage.modified.astimezone(datetime.UTC),
-                    )
-
-                    vulnerability_rescoring = dso.model.CustomRescoring(
-                        finding=dso.model.RescoringVulnerabilityFinding(
-                            package_name=package_name,
-                            cve=vulnerability.cve,
-                        ),
-                        referenced_type=odg.findings.FindingType.VULNERABILITY,
-                        severity=vulnerability_cfg.none_categorisation.id,
-                        user=triage.user,
-                        matching_rules=[dso.model.MetaRescoringRules.BDBA_TRIAGE],
-                        comment=triage.description,
-                    )
-
-                    yield dso.model.ArtefactMetadata(
-                        artefact=rescoring_artefact_ref,
-                        meta=meta_rescoring,
-                        data=vulnerability_rescoring,
-                    )
-
-                vulnerability_finding = dso.model.VulnerabilityFinding(
-                    package_name=package_name,
-                    package_version=package_version,
-                    base_url=scan_result.base_url,
-                    report_url=scan_result.report_url,
-                    product_id=scan_result.product_id,
-                    group_id=scan_result.group_id,
-                    severity=categorisation.id,
-                    cve=vulnerability.cve,
-                    cvss_v3_score=vulnerability.cve_severity(),
-                    cvss=vulnerability.cvss,
-                    summary=vulnerability.summary,
-                )
-
-                artefact_metadata = dso.model.ArtefactMetadata(
-                    artefact=finding_artefact_ref,
-                    meta=meta,
-                    data=vulnerability_finding,
-                    discovery_date=discovery_date,
-                )
-
-                findings.append(artefact_metadata)
-                yield artefact_metadata
-
-    if delivery_client:
-        # delete those BDBA findings which were found before for this scan but which are not part
-        # of the current scan anymore -> those are either solved license findings or (now)
-        # historical vulnerability findings (e.g. because a custom version was entered)
-        existing_findings = iter_existing_findings(
-            delivery_client=delivery_client,
-            resource_node=scanned_element,
-            finding_type=(
-                odg.findings.FindingType.VULNERABILITY,
-                odg.findings.FindingType.LICENSE,
-            ),
-        )
-
-        stale_findings = []
-        for existing_finding in existing_findings:
-            for finding in findings:
-                if (
-                    existing_finding.meta.type == finding.meta.type
-                    and existing_finding.data.key == finding.data.key
-                ):
-                    # finding still appeared in current scan result -> keep it
-                    break
-            else:
-                # finding did not appear in current scan result -> delete it
-                stale_findings.append(existing_finding)
-
-        if stale_findings:
-            delivery_client.delete_metadata(data=stale_findings)
-
-
-def iter_filesystem_paths(
-    component: bm.Component,
-    file_type: str | None=None,
-) -> collections.abc.Generator[dso.model.FilesystemPath, None, None]:
-    for ext_obj in component.extended_objects:
-        path = [
-            dso.model.FilesystemPathEntry(
-                path=path,
-                type=type,
-            ) for path_infos in ext_obj.extended_fullpath
-            if (
-                (path := path_infos.get('path')) and (type := path_infos.get('type'))
-                and (not file_type or file_type == type)
-            )
-        ]
-
-        yield dso.model.FilesystemPath(
-            path=path,
-            digest=ext_obj.sha1,
-        )
-
-
-def enum_triages(
-    result: bm.AnalysisResult,
-) -> collections.abc.Generator[tuple[bm.Component, bm.Triage], None, None]:
-    for component in result.components:
-        for vulnerability in component.vulnerabilities:
-            for triage in vulnerability.triages:
-                yield component, triage
-
-
-def component_artifact_metadata(
-    resource_node: cnudie.iter.ResourceNode,
-    omit_resource_strict_id: bool=False,
-) -> dict:
-    '''
-    Returns a dict for querying bdba scan results (use for custom-data query). If
-    `omit_resource_strict_id` is set, the resource version and extra id are not included in the
-    result to allow querying of related (but not equal) scans.
-    '''
-    component = resource_node.component
-    resource = resource_node.resource
-
-    metadata = {
-        'COMPONENT_NAME': component.name,
-        'IMAGE_REFERENCE_NAME': resource.name, # deprecated, will be replaced with `RESOURCE_NAME`
-        'RESOURCE_TYPE': resource.type,
-    }
-
-    if omit_resource_strict_id:
-        return metadata
-
-    metadata['RESOURCE_NAME'] = resource.name
-    metadata['RESOURCE_VERSION'] = resource.version
-
-    if resource.extraIdentity:
-        # peers are not required here as version is considered anyways
-        metadata['RESOURCE_EXTRA_ID'] = f'{resource.identity(peers=())}'
-
-    return metadata
-
-
-def _matching_analysis_result_id(
-    component_artifact_metadata: dict[str, str],
-    analysis_results: collections.abc.Iterable[bm.Product],
-) -> int | None:
-    # This is a helper function that is used when we create new ScanRequests for a given artifact
-    # group. Since a given artifact group can trigger multiple scans in bdba, we want to be
-    # able to find the correct one from a set of possible choices (if there is one).
-    def filter_func(other_dict: dict[str, str]):
-        # filter-function to find the correct match. We consider a given dict a match if
-        # it contains all keys we have and the values associated with these keys are identical.
-        # Note: That means that (manually) added bdba-metadata will not interfere.
-        for key in component_artifact_metadata:
-            if key not in other_dict.keys():
-                return False
-            if other_dict[key] != component_artifact_metadata[key]:
-                return False
-        return True
-
-    filtered_results = tuple(r for r in analysis_results if filter_func(r.custom_data))
-
-    if not filtered_results:
-        return None
-
-    # there may be multiple possible candidates since we switched from including the component
-    # version in the component artefact metadata to excluding it
-    if len(filtered_results) > 1:
-        logger.warning(
-            'more than one scan result found for component artefact with '
-            f'{component_artifact_metadata=}, will use latest scan result...'
-        )
-        filtered_results = sorted(
-            filtered_results,
-            key=lambda result: result.product_id,
-            reverse=True,
-        )
-
-    # there is at least one result and they are ordered (latest product id first)
-    return filtered_results[0].product_id
+    return urllib.parse.urlparse(url)
diff --git a/bdba_extension/__init__.py b/bdba_extension/__init__.py
new file mode 100644
index 00000000..5b46d874
--- /dev/null
+++ b/bdba_extension/__init__.py
@@ -0,0 +1,3 @@
+# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
+#
+# SPDX-License-Identifier: Apache-2.0
diff --git a/bdba/__main__.py b/bdba_extension/__main__.py
similarity index 95%
rename from bdba/__main__.py
rename to bdba_extension/__main__.py
index ae754fba..ac1ea193 100644
--- a/bdba/__main__.py
+++ b/bdba_extension/__main__.py
@@ -16,7 +16,7 @@
 import tarutil
 
 import bdba.client
-import bdba.scanning
+import bdba_extension.scanning
 import consts
 import ctx_util
 import deliverydb_cache.model as dcm
@@ -31,6 +31,7 @@
 import odg.findings
 import paths
 import secret_mgmt
+import secret_mgmt.bdba
 
 
 logger = logging.getLogger(__name__)
@@ -138,20 +139,24 @@ def scan(
     mapping = bdba_cfg.mapping(artefact.component_name)
 
     logger.info(f'using BDBA secret element "{mapping.bdba_secret_name}"')
-    bdba_secret = secret_factory.bdba(mapping.bdba_secret_name)
-    bdba_client = bdba.client.client(
-        bdba_cfg=bdba_secret,
-        group_id=mapping.group_id,
-        secret_factory=secret_factory,
+    bdba_secret: secret_mgmt.bdba.BDBA = secret_factory.bdba(mapping.bdba_secret_name)
+
+    if bdba_secret.matches(group_id=mapping.group_id) is secret_mgmt.bdba.MatchScore.NO_MATCH:
+        raise ValueError(f'BDBA cfg does not match {mapping.group_id=}')
+
+    bdba_client = bdba.client.BDBAApi(
+        api_routes=bdba.client.BDBAApiRoutes(base_url=bdba_secret.api_url),
+        token=bdba_secret.token,
+        tls_verify=bdba_secret.tls_verify,
     )
 
-    known_scan_results = bdba.scanning.retrieve_existing_scan_results(
+    known_scan_results = bdba_extension.scanning.retrieve_existing_scan_results(
         bdba_client=bdba_client,
         group_id=mapping.group_id,
         resource_node=resource_node,
     )
 
-    processor = bdba.scanning.ResourceGroupProcessor(
+    processor = bdba_extension.scanning.ResourceGroupProcessor(
         bdba_client=bdba_client,
         group_id=mapping.group_id,
     )
diff --git a/bdba/assessments.py b/bdba_extension/assessments.py
similarity index 91%
rename from bdba/assessments.py
rename to bdba_extension/assessments.py
index 83026054..c13af2e9 100644
--- a/bdba/assessments.py
+++ b/bdba_extension/assessments.py
@@ -9,7 +9,7 @@
 def upload_version_hints(
     scan_result: bm.AnalysisResult,
     hints: collections.abc.Iterable[dso.labels.PackageVersionHint],
-    client: bdba.client.BDBAApi,
+    bdba_client: bdba.client.BDBAApi,
 ) -> bm.AnalysisResult:
     for component in scan_result.components:
         name = component.name
@@ -29,7 +29,7 @@ def upload_version_hints(
 
         digests = [eo.sha1 for eo in component.extended_objects]
 
-        client.set_component_version(
+        bdba_client.set_component_version(
             component_name=name,
             component_version=hint.version,
             objects=digests,
@@ -40,7 +40,7 @@ def upload_version_hints(
         # a short period of time. This even stays true if all component versions are set
         # using one single api request. That's why, adding a small delay in case multiple
         # hints and thus possible version overrides exist by retrieving scan result again
-        scan_result = client.wait_for_scan_result(
+        scan_result = bdba_client.wait_for_scan_result(
             product_id=scan_result.product_id,
             polling_interval_seconds=15, # re-scanning usually don't take a minute
         )
diff --git a/bdba_extension/model.py b/bdba_extension/model.py
new file mode 100644
index 00000000..0da395bf
--- /dev/null
+++ b/bdba_extension/model.py
@@ -0,0 +1,67 @@
+import collections.abc
+import dataclasses
+import traceback
+
+import dso.labels
+import ocm
+
+
+@dataclasses.dataclass
+class ScanRequest:
+    '''
+    a scan request of an artefact (referenced by component and artefact).
+
+    if a previous scan result was found, its "product-id" is stored as `target_product_id`
+    '''
+    component: ocm.Component
+    artefact: ocm.Artifact
+    # The actual content to be scanned.
+    scan_content: collections.abc.Generator[bytes, None, None]
+    display_name: str
+    target_product_id: int | None
+    custom_metadata: dict
+
+    @property
+    def skip_vulnerability_scan(self) -> bool:
+        # hardcode skip-info to be determined by artefact
+        artefact = self.artefact
+
+        if not (label := artefact.find_label(name=dso.labels.BinaryIdScanLabel.name)):
+            return False
+
+        label: dso.labels.BinaryIdScanLabel = dso.labels.deserialise_label(label=label)
+        return label.value.policy is dso.labels.ScanPolicy.SKIP
+
+    def __str__(self):
+        return (
+            f"ScanRequest(name='{self.display_name}', target_product_id='{self.target_product_id}' "
+            f"custom_metadata='{self.custom_metadata}')"
+        )
+
+
+class BdbaScanError(Exception):
+    def __init__(
+        self,
+        scan_request: ScanRequest,
+        component: ocm.Component,
+        artefact: ocm.Artifact,
+        exception=None,
+        *args,
+        **kwargs,
+    ):
+        self.scan_request = scan_request
+        self.component = component
+        self.artefact = artefact
+        self.exception = exception
+
+        super().__init__(*args, **kwargs)
+
+    def print_stacktrace(self):
+        c = self.component
+        a = self.artefact
+        name = f'{c.name}/{a.name}:{a.version}'
+
+        if not self.exception:
+            return name + ' - no exception available'
+
+        return name + '\n' + ''.join(traceback.format_tb(self.exception.__traceback__))
diff --git a/bdba/rescore.py b/bdba_extension/rescore.py
similarity index 98%
rename from bdba/rescore.py
rename to bdba_extension/rescore.py
index 1b90ec2a..38dbc5bd 100644
--- a/bdba/rescore.py
+++ b/bdba_extension/rescore.py
@@ -1,6 +1,7 @@
 import logging
 
 import cnudie.iter
+import dso.cvss
 import dso.model
 
 import bdba.client
@@ -76,7 +77,7 @@ def rescore(
             matching_rules = ru.matching_rescore_rules(
                 rescoring_rules=vulnerability_cfg.rescoring_ruleset.rules,
                 categorisation=cve_categorisation,
-                cvss=v.cvss,
+                cvss=dso.cvss.CVSSV3.parse(v.cvss),
             )
 
             rescored_categorisation = ru.rescore_finding(
diff --git a/bdba/scanning.py b/bdba_extension/scanning.py
similarity index 92%
rename from bdba/scanning.py
rename to bdba_extension/scanning.py
index b2b32e12..217e06e1 100644
--- a/bdba/scanning.py
+++ b/bdba_extension/scanning.py
@@ -11,13 +11,15 @@
 import dso.model
 import ocm
 
-import bdba.assessments
 import bdba.client
 import bdba.model as bm
-import bdba.rescore
-import bdba.util
+import bdba_extension.assessments
+import bdba_extension.rescore
+import bdba_extension.util
+import bdba_extension.model
 import odg.findings
 
+
 logger = logging.getLogger(__name__)
 ci.log.configure_default_logging(print_thread_id=True)
 
@@ -36,7 +38,7 @@ def scan_request(
         resource_node: cnudie.iter.ResourceNode,
         content_iterator: collections.abc.Generator[bytes, None, None],
         known_artifact_scans: collections.abc.Iterable[bm.Product],
-    ) -> bm.ScanRequest:
+    ) -> bdba_extension.model.ScanRequest:
         component = resource_node.component
         resource = resource_node.resource
         display_name = f'{resource.name}_{resource.version}_{component.name}_{resource.type}'.replace('/', '_') # noqa: E501
@@ -45,11 +47,11 @@ def scan_request(
             # peers are not required here as version is considered anyways
             display_name += f'_{resource.identity(peers=())}'.replace('/', '_')
 
-        component_artifact_metadata = bdba.util.component_artifact_metadata(
+        component_artifact_metadata = bdba_extension.util.component_artifact_metadata(
             resource_node=resource_node,
         )
 
-        target_product_id = bdba.util._matching_analysis_result_id(
+        target_product_id = bdba_extension.util._matching_analysis_result_id(
             component_artifact_metadata=component_artifact_metadata,
             analysis_results=known_artifact_scans,
         )
@@ -59,7 +61,7 @@ def scan_request(
         else:
             logger.info(f'{display_name=}: did not find old scan')
 
-        return bm.ScanRequest(
+        return bdba_extension.model.ScanRequest(
             component=component,
             artefact=resource,
             scan_content=content_iterator,
@@ -70,11 +72,11 @@ def scan_request(
 
     def process_scan_request(
         self,
-        scan_request: bm.ScanRequest,
+        scan_request: bdba_extension.model.ScanRequest,
         processing_mode: bm.ProcessingMode,
     ) -> bm.Result:
         def raise_on_error(exception):
-            raise bm.BdbaScanError(
+            raise bdba_extension.model.BdbaScanError(
                 scan_request=scan_request,
                 component=scan_request.component,
                 artefact=scan_request.artefact,
@@ -191,7 +193,7 @@ def process(
             )
             scan_result = self.bdba_client.wait_for_scan_result(result.product_id)
             scan_failed = False
-        except bm.BdbaScanError as bse:
+        except bdba_extension.model.BdbaScanError as bse:
             scan_result = bse
             scan_failed = True
             logger.warning(bse.print_stacktrace())
@@ -211,10 +213,10 @@ def process(
 
         if version_hints := _package_version_hints(resource=resource_node.resource):
             logger.info(f'uploading package-version-hints for {scan_result.display_name}')
-            scan_result = bdba.assessments.upload_version_hints(
+            scan_result = bdba_extension.assessments.upload_version_hints(
                 scan_result=scan_result,
                 hints=version_hints,
-                client=self.bdba_client,
+                bdba_client=self.bdba_client,
             )
 
         if scan_request.skip_vulnerability_scan:
@@ -222,7 +224,7 @@ def process(
             vulnerability_cfg = None
 
         if vulnerability_cfg:
-            refetching_required = bdba.rescore.rescore(
+            refetching_required = bdba_extension.rescore.rescore(
                 bdba_client=self.bdba_client,
                 scan_result=scan_result,
                 scanned_element=scanned_element,
@@ -237,7 +239,7 @@ def process(
 
         logger.info(f'post-processing of {scan_result.display_name} done')
 
-        yield from bdba.util.iter_artefact_metadata(
+        yield from bdba_extension.util.iter_artefact_metadata(
             scanned_element=scanned_element,
             scan_result=scan_result,
             delivery_client=delivery_client,
@@ -267,7 +269,7 @@ def retrieve_existing_scan_results(
     group_id: int,
     resource_node: cnudie.iter.ResourceNode,
 ) -> list[bm.Product]:
-    query_data = bdba.util.component_artifact_metadata(
+    query_data = bdba_extension.util.component_artifact_metadata(
         resource_node=resource_node,
         omit_resource_strict_id=True,
     )
diff --git a/bdba_extension/util.py b/bdba_extension/util.py
new file mode 100644
index 00000000..ef128ac6
--- /dev/null
+++ b/bdba_extension/util.py
@@ -0,0 +1,366 @@
+# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors
+#
+# SPDX-License-Identifier: Apache-2.0
+
+
+import collections.abc
+import dataclasses
+import datetime
+import logging
+
+import dacite
+
+import ci.log
+import cnudie.iter
+import delivery.client
+import dso.cvss
+import dso.model
+
+import bdba.model as bm
+import odg.findings
+
+
+logger = logging.getLogger(__name__)
+ci.log.configure_default_logging(print_thread_id=True)
+
+
+def iter_existing_findings(
+    delivery_client: delivery.client.DeliveryServiceClient,
+    resource_node: cnudie.iter.ResourceNode,
+    finding_type: str | tuple[str],
+    datasource: str=dso.model.Datasource.BDBA,
+) -> collections.abc.Generator[dso.model.ArtefactMetadata, None, None]:
+    artefact = dso.model.component_artefact_id_from_ocm(
+        component=resource_node.component_id,
+        artefact=resource_node.resource,
+    )
+
+    findings = delivery_client.query_metadata(
+        artefacts=(artefact,),
+        type=finding_type,
+    )
+
+    return (
+        finding for finding in findings
+        if finding.meta.datasource == datasource
+    )
+
+
+def iter_artefact_metadata(
+    scanned_element: cnudie.iter.ResourceNode,
+    scan_result: bm.AnalysisResult,
+    delivery_client: delivery.client.DeliveryServiceClient=None,
+    vulnerability_cfg: odg.findings.Finding | None=None,
+    license_cfg: odg.findings.Finding | None=None,
+) -> collections.abc.Generator[dso.model.ArtefactMetadata, None, None]:
+    now = datetime.datetime.now(tz=datetime.timezone.utc)
+    discovery_date = datetime.date.today()
+    datasource = dso.model.Datasource.BDBA
+
+    artefact_ref = dso.model.component_artefact_id_from_ocm(
+        component=scanned_element.component,
+        artefact=scanned_element.resource,
+    )
+
+    # don't include component version here since it is also not considered in the BDBA scan
+    # -> this will deduplicate findings of the same artefact version across different
+    # component versions
+    finding_artefact_ref = dataclasses.replace(
+        artefact_ref,
+        component_version=None,
+    )
+
+    # rescoring should not reference artefact version so that the `ARTEFACT` rescoring scope will be
+    # used -> rescoring will be used for future versions as well, so there is no need to replicate
+    # BDBA triages to new BDBA scans
+    rescoring_artefact_ref = dataclasses.replace(
+        finding_artefact_ref,
+        artefact=dataclasses.replace(
+            finding_artefact_ref.artefact,
+            artefact_version=None,
+        ),
+    )
+
+    yield dso.model.artefact_scan_info(
+        artefact_node=scanned_element,
+        datasource=datasource,
+        data={
+            'report_url': scan_result.report_url,
+        },
+    )
+
+    findings: list[dso.model.ArtefactMetadata] = []
+    for package in scan_result.components:
+        package_name = package.name
+        package_version = package.version
+
+        filesystem_paths = list(iter_filesystem_paths(component=package))
+
+        licenses = list({
+            dso.model.License(
+                name=license.name,
+            ) for license in package.iter_licenses
+        })
+
+        meta = dso.model.Metadata(
+            datasource=datasource,
+            type=dso.model.Datatype.STRUCTURE_INFO,
+            creation_date=now,
+        )
+
+        structure_info = dso.model.StructureInfo(
+            package_name=package_name,
+            package_version=package_version,
+            base_url=scan_result.base_url,
+            report_url=scan_result.report_url,
+            product_id=scan_result.product_id,
+            group_id=scan_result.group_id,
+            licenses=licenses,
+            filesystem_paths=filesystem_paths,
+        )
+
+        yield dso.model.ArtefactMetadata(
+            artefact=finding_artefact_ref,
+            meta=meta,
+            data=structure_info,
+            discovery_date=discovery_date,
+        )
+
+        if license_cfg and license_cfg.matches(artefact_ref):
+            meta = dso.model.Metadata(
+                datasource=datasource,
+                type=odg.findings.FindingType.LICENSE,
+                creation_date=now,
+            )
+
+            for license in licenses:
+                categorisation = odg.findings.categorise_finding(
+                    finding_cfg=license_cfg,
+                    finding_property=license.name,
+                )
+
+                if not categorisation:
+                    continue
+
+                license_finding = dso.model.LicenseFinding(
+                    package_name=package_name,
+                    package_version=package_version,
+                    base_url=scan_result.base_url,
+                    report_url=scan_result.report_url,
+                    product_id=scan_result.product_id,
+                    group_id=scan_result.group_id,
+                    severity=categorisation.id,
+                    license=license,
+                )
+
+                artefact_metadata = dso.model.ArtefactMetadata(
+                    artefact=finding_artefact_ref,
+                    meta=meta,
+                    data=license_finding,
+                    discovery_date=discovery_date,
+                )
+
+                findings.append(artefact_metadata)
+                yield artefact_metadata
+
+        if vulnerability_cfg and vulnerability_cfg.matches(artefact_ref):
+            meta = dso.model.Metadata(
+                datasource=datasource,
+                type=odg.findings.FindingType.VULNERABILITY,
+                creation_date=now,
+            )
+
+            for vulnerability in package.vulnerabilities:
+                if vulnerability.okay_to_skip:
+                    continue # we only support active vulnerabilities with a valid cvss v3 vector
+
+                categorisation = odg.findings.categorise_finding(
+                    finding_cfg=vulnerability_cfg,
+                    finding_property=vulnerability.cve_severity(),
+                )
+
+                if not categorisation:
+                    continue
+
+                for triage in vulnerability.triages:
+                    meta_rescoring = dso.model.Metadata(
+                        datasource=datasource,
+                        type=dso.model.Datatype.RESCORING,
+                        creation_date=triage.modified.astimezone(datetime.UTC),
+                    )
+
+                    vulnerability_rescoring = dso.model.CustomRescoring(
+                        finding=dso.model.RescoringVulnerabilityFinding(
+                            package_name=package_name,
+                            cve=vulnerability.cve,
+                        ),
+                        referenced_type=odg.findings.FindingType.VULNERABILITY,
+                        severity=vulnerability_cfg.none_categorisation.id,
+                        user=dacite.from_dict(
+                            data_class=dso.model.BDBAUser,
+                            data=triage.user,
+                        ),
+                        matching_rules=[dso.model.MetaRescoringRules.BDBA_TRIAGE],
+                        comment=triage.description,
+                    )
+
+                    yield dso.model.ArtefactMetadata(
+                        artefact=rescoring_artefact_ref,
+                        meta=meta_rescoring,
+                        data=vulnerability_rescoring,
+                    )
+
+                vulnerability_finding = dso.model.VulnerabilityFinding(
+                    package_name=package_name,
+                    package_version=package_version,
+                    base_url=scan_result.base_url,
+                    report_url=scan_result.report_url,
+                    product_id=scan_result.product_id,
+                    group_id=scan_result.group_id,
+                    severity=categorisation.id,
+                    cve=vulnerability.cve,
+                    cvss_v3_score=vulnerability.cve_severity(),
+                    cvss=dso.cvss.CVSSV3.parse(vulnerability.cvss),
+                    summary=vulnerability.summary,
+                )
+
+                artefact_metadata = dso.model.ArtefactMetadata(
+                    artefact=finding_artefact_ref,
+                    meta=meta,
+                    data=vulnerability_finding,
+                    discovery_date=discovery_date,
+                )
+
+                findings.append(artefact_metadata)
+                yield artefact_metadata
+
+    if delivery_client:
+        # delete those BDBA findings which were found before for this scan but which are not part
+        # of the current scan anymore -> those are either solved license findings or (now)
+        # historical vulnerability findings (e.g. because a custom version was entered)
+        existing_findings = iter_existing_findings(
+            delivery_client=delivery_client,
+            resource_node=scanned_element,
+            finding_type=(
+                odg.findings.FindingType.VULNERABILITY,
+                odg.findings.FindingType.LICENSE,
+            ),
+        )
+
+        stale_findings = []
+        for existing_finding in existing_findings:
+            for finding in findings:
+                if (
+                    existing_finding.meta.type == finding.meta.type
+                    and existing_finding.data.key == finding.data.key
+                ):
+                    # finding still appeared in current scan result -> keep it
+                    break
+            else:
+                # finding did not appear in current scan result -> delete it
+                stale_findings.append(existing_finding)
+
+        if stale_findings:
+            delivery_client.delete_metadata(data=stale_findings)
+
+
+def iter_filesystem_paths(
+    component: bm.Component,
+    file_type: str | None=None,
+) -> collections.abc.Generator[dso.model.FilesystemPath, None, None]:
+    for ext_obj in component.extended_objects:
+        path = [
+            dso.model.FilesystemPathEntry(
+                path=path,
+                type=type,
+            ) for path_infos in ext_obj.extended_fullpath
+            if (
+                (path := path_infos.get('path')) and (type := path_infos.get('type'))
+                and (not file_type or file_type == type)
+            )
+        ]
+
+        yield dso.model.FilesystemPath(
+            path=path,
+            digest=ext_obj.sha1,
+        )
+
+
+def enum_triages(
+    result: bm.AnalysisResult,
+) -> collections.abc.Generator[tuple[bm.Component, bm.Triage], None, None]:
+    for component in result.components:
+        for vulnerability in component.vulnerabilities:
+            for triage in vulnerability.triages:
+                yield component, triage
+
+
+def component_artifact_metadata(
+    resource_node: cnudie.iter.ResourceNode,
+    omit_resource_strict_id: bool=False,
+) -> dict:
+    '''
+    Returns a dict for querying bdba scan results (use for custom-data query). If
+    `omit_resource_strict_id` is set, the resource version and extra id are not included in the
+    result to allow querying of related (but not equal) scans.
+    '''
+    component = resource_node.component
+    resource = resource_node.resource
+
+    metadata = {
+        'COMPONENT_NAME': component.name,
+        'IMAGE_REFERENCE_NAME': resource.name, # deprecated, will be replaced with `RESOURCE_NAME`
+        'RESOURCE_TYPE': resource.type,
+    }
+
+    if omit_resource_strict_id:
+        return metadata
+
+    metadata['RESOURCE_NAME'] = resource.name
+    metadata['RESOURCE_VERSION'] = resource.version
+
+    if resource.extraIdentity:
+        # peers are not required here as version is considered anyways
+        metadata['RESOURCE_EXTRA_ID'] = f'{resource.identity(peers=())}'
+
+    return metadata
+
+
+def _matching_analysis_result_id(
+    component_artifact_metadata: dict[str, str],
+    analysis_results: collections.abc.Iterable[bm.Product],
+) -> int | None:
+    # This is a helper function that is used when we create new ScanRequests for a given artifact
+    # group. Since a given artifact group can trigger multiple scans in bdba, we want to be
+    # able to find the correct one from a set of possible choices (if there is one).
+    def filter_func(other_dict: dict[str, str]):
+        # filter-function to find the correct match. We consider a given dict a match if
+        # it contains all keys we have and the values associated with these keys are identical.
+        # Note: That means that (manually) added bdba-metadata will not interfere.
+        for key in component_artifact_metadata:
+            if key not in other_dict.keys():
+                return False
+            if other_dict[key] != component_artifact_metadata[key]:
+                return False
+        return True
+
+    filtered_results = tuple(r for r in analysis_results if filter_func(r.custom_data))
+
+    if not filtered_results:
+        return None
+
+    # there may be multiple possible candidates since we switched from including the component
+    # version in the component artefact metadata to excluding it
+    if len(filtered_results) > 1:
+        logger.warning(
+            'more than one scan result found for component artefact with '
+            f'{component_artifact_metadata=}, will use latest scan result...'
+        )
+        filtered_results = sorted(
+            filtered_results,
+            key=lambda result: result.product_id,
+            reverse=True,
+        )
+
+    # there is at least one result and they are ordered (latest product id first)
+    return filtered_results[0].product_id
diff --git a/cli/_bdba.py b/cli/_bdba.py
index 5550f6ed..c0b86925 100644
--- a/cli/_bdba.py
+++ b/cli/_bdba.py
@@ -14,8 +14,8 @@
 import tarutil
 
 import bdba.client
-import bdba.model
-import bdba.scanning
+import bdba.model as bm
+import bdba_extension.scanning
 import ctx_util
 import lookups
 import ocm_util
@@ -33,9 +33,15 @@ def retrieve(
     product_id: str,
     bdba_cfg_name='gardener',
 ):
-    client = bdba.client.client(bdba_cfg_name)
+    secret_factory = ctx_util.secret_factory()
+    bdba_cfg = secret_factory.bdba(bdba_cfg_name)
+    bdba_client = bdba.client.BDBAApi(
+        api_routes=bdba.client.BDBAApiRoutes(base_url=bdba_cfg.api_url),
+        token=bdba_cfg.token,
+        tls_verify=bdba_cfg.tls_verify,
+    )
 
-    scan_result = client.wait_for_scan_result(
+    scan_result = bdba_client.wait_for_scan_result(
         product_id=product_id,
     )
 
@@ -49,7 +55,13 @@ def ls_products(
 ):
     ocm_lookup = lookups.init_component_descriptor_lookup()
 
-    client = bdba.client.client(bdba_cfg_name)
+    secret_factory = ctx_util.secret_factory()
+    bdba_cfg = secret_factory.bdba(bdba_cfg_name)
+    bdba_client = bdba.client.BDBAApi(
+        api_routes=bdba.client.BDBAApiRoutes(base_url=bdba_cfg.api_url),
+        token=bdba_cfg.token,
+        tls_verify=bdba_cfg.tls_verify,
+    )
 
     if not ':' in ocm_component:
         raise ValueError('ocm_component must have form <name>:<version>')
@@ -68,7 +80,7 @@ def ls_products(
             'COMPONENT_VERSION': component.version,
         }
 
-        for app in client.list_apps(group_id=group_id, custom_attribs=metadata):
+        for app in bdba_client.list_apps(group_id=group_id, custom_attribs=metadata):
             print(app.product_id)
 
 
@@ -104,7 +116,7 @@ def scan(
     )
     component_descriptor = lookup(component_id)
 
-    cvss_version = bdba.model.CVSSVersion.V3
+    cvss_version = bm.CVSSVersion.V3
 
     headers = ('BDBA Scan Configuration', '')
     entries = (
@@ -117,11 +129,12 @@ def scan(
 
     logger.info('running BDBA scan for all components')
 
-    bdba_client = bdba.client.client(
-        bdba_cfg=bdba_cfg,
-        group_id=bdba_group_id,
-        url=bdba_api_url,
-        secret_factory=secret_factory,
+    secret_factory = ctx_util.secret_factory()
+    bdba_cfg = secret_factory.bdba(bdba_cfg_name)
+    bdba_client = bdba.client.BDBAApi(
+        api_routes=bdba.client.BDBAApiRoutes(base_url=bdba_cfg.api_url),
+        token=bdba_cfg.token,
+        tls_verify=bdba_cfg.tls_verify,
     )
 
     def iter_resource_scans() -> collections.abc.Generator[dso.model.ArtefactMetadata, None, None]:
@@ -130,12 +143,12 @@ def iter_resource_scans() -> collections.abc.Generator[dso.model.ArtefactMetadat
             lookup=lookup,
             node_filter=cnudie.iter.Filter.resources,
         ):
-            known_scan_results = bdba.scanning.retrieve_existing_scan_results(
+            known_scan_results = bdba_extension.scanning.retrieve_existing_scan_results(
                 bdba_client=bdba_client,
                 group_id=bdba_group_id,
                 resource_node=resource_node,
             )
-            processor = bdba.scanning.ResourceGroupProcessor(
+            processor = bdba_extension.scanning.ResourceGroupProcessor(
                 bdba_client=bdba_client,
                 group_id=bdba_group_id,
             )
@@ -179,7 +192,7 @@ def iter_resource_scans() -> collections.abc.Generator[dso.model.ArtefactMetadat
             yield from processor.process(
                 resource_node=resource_node,
                 content_iterator=content_iterator,
-                processing_mode=bdba.model.ProcessingMode.RESCAN,
+                processing_mode=bm.ProcessingMode.RESCAN,
                 known_scan_results=known_scan_results,
             )
 
@@ -260,11 +273,17 @@ def transport_triages(
     to_group_id: int,
     to_product_ids: list[int],
 ):
-    api = bdba.client.client(bdba_cfg_name)
+    secret_factory = ctx_util.secret_factory()
+    bdba_cfg = secret_factory.bdba(bdba_cfg_name)
+    bdba_client = bdba.client.BDBAApi(
+        api_routes=bdba.client.BDBAApiRoutes(base_url=bdba_cfg.api_url),
+        token=bdba_cfg.token,
+        tls_verify=bdba_cfg.tls_verify,
+    )
 
-    scan_result_from = api.scan_result(product_id=from_product_id)
+    scan_result_from = bdba_client.scan_result(product_id=from_product_id)
     scan_results_to = {
-        product_id: api.scan_result(product_id=product_id)
+        product_id: bdba_client.scan_result(product_id=product_id)
         for product_id in to_product_ids
     }
 
@@ -293,7 +312,7 @@ def enum_triages():
             component_name=component.name,
         ):
             logger.info(f'adding triage for {triage.component}:{target_component_version}')
-            api.add_triage(
+            bdba_client.add_triage(
                 triage=triage,
                 product_id=to_product_id,
                 group_id=to_group_id,
diff --git a/odg/extensions_cfg.py b/odg/extensions_cfg.py
index 6a65242d..da4fc97b 100644
--- a/odg/extensions_cfg.py
+++ b/odg/extensions_cfg.py
@@ -14,7 +14,7 @@
 import github.compliance.milestone as gcmi
 import ocm
 
-import bdba.model
+import bdba.model as bm
 import crypto_extension.config
 import lookups
 import odg.shared_cfg
@@ -169,7 +169,7 @@ class BDBAMapping(Mapping):
     group_id: int
     bdba_secret_name: str
     aws_secret_name: str | None
-    processing_mode: bdba.model.ProcessingMode = bdba.model.ProcessingMode.RESCAN
+    processing_mode: bm.ProcessingMode = bm.ProcessingMode.RESCAN
 
 
 @dataclasses.dataclass
diff --git a/requirements.bdba.txt b/requirements.bdba.txt
new file mode 100644
index 00000000..be68851c
--- /dev/null
+++ b/requirements.bdba.txt
@@ -0,0 +1,3 @@
+dacite
+python-dateutil
+requests
\ No newline at end of file
diff --git a/requirements.utils.txt b/requirements.utils.txt
index 98b7a609..499b2c44 100644
--- a/requirements.utils.txt
+++ b/requirements.utils.txt
@@ -1,4 +1,5 @@
 aiohttp
+bdba==0.1.0
 boto3
 dacite
 gardener-cicd-libs==1.2606.0
diff --git a/setup.bdba.py b/setup.bdba.py
new file mode 100644
index 00000000..8bdee69f
--- /dev/null
+++ b/setup.bdba.py
@@ -0,0 +1,31 @@
+import os
+
+import semver
+import setuptools
+
+
+own_dir = os.path.abspath(os.path.dirname(__file__))
+
+
+def requirements():
+    with open(os.path.join(own_dir, 'requirements.bdba.txt')) as f:
+        for line in f.readlines():
+            line = line.strip()
+            if not line or line.startswith('#'):
+                continue
+
+            yield line
+
+
+def finalize_version():
+    with open(os.path.join(own_dir, 'BDBA_VERSION')) as f:
+        return semver.finalize_version(f.read().strip())
+
+
+setuptools.setup(
+    name='bdba',
+    version=finalize_version(),
+    py_modules=[],
+    packages=['bdba'],
+    install_requires=list(requirements()),
+)
diff --git a/setup.extensions.py b/setup.extensions.py
index 95560bac..63431d14 100644
--- a/setup.extensions.py
+++ b/setup.extensions.py
@@ -31,7 +31,7 @@ def modules():
 
 def packages():
     return [
-        'bdba',
+        'bdba_extension',
         'crypto_extension',
         'issue_replicator',
         'malware',
diff --git a/setup.utils.py b/setup.utils.py
index b8ff4105..6a99a98c 100644
--- a/setup.utils.py
+++ b/setup.utils.py
@@ -20,7 +20,6 @@ def requirements():
 
 def modules():
     return [
-        'bdba.model',
         'config',
         'consts',
         'crypto_extension.config',


Reviews:
user: login: ccwienk
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-10T10:02:38Z
user: login: zkdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-10T10:06:59Z
user: login: zkdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-12T11:06:54Z
user: login: 8R0WNI3
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-13T10:34:21Z
user: login: 8R0WNI3
body: Also, you'll have to include the bdba-client package in the `.ci/install-dependencies` script (see failing checks).
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-13T10:36:54Z
user: login: ccwienk
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-13T15:16:48Z
user: login: zkdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-13T15:32:17Z
user: login: ccwienk
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-26T10:43:25Z
user: login: zkdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T13:43:08Z
user: login: 8R0WNI3
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-07T14:58:06Z
user: login: zkdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-07T15:01:09Z
user: login: zkdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T09:15:15Z
user: login: zkdev
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T09:17:45Z
user: login: 8R0WNI3
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-10T09:32:57Z
user: login: 8R0WNI3
body: /lgtm
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-10T10:04:49Z

Review comments:
diff_hunk: @@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+own_dir="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")"
+
+if [ -z "${EFFECTIVE_VERSION:-}" ];then
+  echo "environment variable EFFECTIVE_VERSION must be set!"
+  exit 1
+fi
+
+# replaces bdba-client=<semver> with bdba-client=<EFFECTIVE_VERSION>
+version_regex="s/\(bdba-client==\)[0-9]\+\.[0-9]\+\.[0-9]\+/\1${EFFECTIVE_VERSION}/g"
+
+# patch new bdba-client package version into consumer's requirements file
+sed -i "${version_regex}" "${own_dir}/../requirements.util.txt"
path: .ci/bump_dependency_on_release
user: login: ccwienk
body: we might deduplicate the amount of locations where we keep track of bdba-client-version if we switch to reading version in consuming setup-scripts
created_at: 2025-02-10T09:59:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,7 +15,7 @@
 import ocm
 import tarutil
 
-import bdba.client
+import bdba_client as bc
path: bdba/__main__.py
user: login: ccwienk
body: admittedly, this is a matter of taste. however, I would prefer to re-organise packages like so:

```
bdba # for reusable bdba-client
bdba_extension # for code for bdba-extension`
```

rationale: bdba has a wider scope according to its name, which matches well to general-purpose bdba-client-code. whereas bdba_(odg)-extension is more special, hence I suggest to use a more specific name. Also, to make it more obvious for others to understand this package is related to an ODG-extension. wdyt?
created_at: 2025-02-10T10:02:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -15,7 +15,7 @@
 import ocm
 import tarutil
 
-import bdba.client
+import bdba_client as bc
path: bdba/__main__.py
user: login: zkdev
body: Thought about that as well, since I don't have a strong opinion here I chose the approach involving less changes 😅 
I have to admit, your rational makes sense.

Let's change it 👍 
created_at: 2025-02-10T10:06:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+own_dir="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")"
+
+if [ -z "${EFFECTIVE_VERSION:-}" ];then
+  echo "environment variable EFFECTIVE_VERSION must be set!"
+  exit 1
+fi
+
+# replaces bdba-client=<semver> with bdba-client=<EFFECTIVE_VERSION>
+version_regex="s/\(bdba-client==\)[0-9]\+\.[0-9]\+\.[0-9]\+/\1${EFFECTIVE_VERSION}/g"
+
+# patch new bdba-client package version into consumer's requirements file
+sed -i "${version_regex}" "${own_dir}/../requirements.util.txt"
path: .ci/bump_dependency_on_release
user: login: zkdev
body: These versions are no real duplicates as they are used for two different things: consumption and publishing.
Consumption part will never care about `-dev` suffix (dev cycle) versions, whereas publishing does ...
created_at: 2025-02-12T11:06:54Z
author_association: MEMBER
path: bdba/__init__.py
user: login: 8R0WNI3
body: Shouldn't we keep the client-code in `bdba/client.py` to be consistent with our other client classes (e.g. `oci/client.py`, `delivery/client.py`)?
created_at: 2025-02-13T10:24:15Z
author_association: MEMBER
diff_hunk: @@ -11,21 +11,22 @@
 import dso.model
 import ocm
 
-import bdba.assessments
-import bdba.client
+import bdba_extension.assessments
+import bdba
 import bdba.model as bm
-import bdba.rescore
-import bdba.util
+import bdba_extension.rescore
+import bdba_extension.util
path: bdba_extension/scanning.py
user: login: 8R0WNI3
body: nit: Order alphabetically (again)
created_at: 2025-02-13T10:26:21Z
author_association: MEMBER
diff_hunk: @@ -138,3 +138,4 @@ delivery-service:
                 - setuptools
                 - pip
                 - distribution-package
+          release_callback: '.ci/bump_dependency_on_release'
path: .ci/pipeline_definitions
user: login: 8R0WNI3
body: This way, the bdba-client version e.g. `0.10.0` will only be used in delivery-gear-utils package starting from `0.11.0`, right? Can't we bump the version prior to the build step so that it is already used in current image builds?
created_at: 2025-02-13T10:34:17Z
author_association: MEMBER
diff_hunk: @@ -138,3 +138,4 @@ delivery-service:
                 - setuptools
                 - pip
                 - distribution-package
+          release_callback: '.ci/bump_dependency_on_release'
path: .ci/pipeline_definitions
user: login: ccwienk
body: I guess that will also become nicer once we switch to GHA
created_at: 2025-02-13T15:14:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+import os
+
+import setuptools
+
+import setup
+
+
+own_dir = os.path.abspath(os.path.dirname(__file__))
+
+
+def requirements():
+    with open(os.path.join(own_dir, 'requirements.bdba_client.txt')) as f:
+        for line in f.readlines():
+            line = line.strip()
+            if not line or line.startswith('#'):
+                continue
+
+            yield line
+
+
+setuptools.setup(
+    name='bdba-client',
path: setup.bdba_client.py
user: login: ccwienk
body: is `bdba` already taken? if not, I think we should name the package so (also following conventions on pypi).
created_at: 2025-02-13T15:16:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+import os
+
+import setuptools
+
+import setup
+
+
+own_dir = os.path.abspath(os.path.dirname(__file__))
+
+
+def requirements():
+    with open(os.path.join(own_dir, 'requirements.bdba_client.txt')) as f:
+        for line in f.readlines():
+            line = line.strip()
+            if not line or line.startswith('#'):
+                continue
+
+            yield line
+
+
+setuptools.setup(
+    name='bdba-client',
path: setup.bdba_client.py
user: login: zkdev
body: still available, will adjust :) 
created_at: 2025-02-13T15:32:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,27 @@
+import os
+
+import setuptools
+
+import setup
+
+
+own_dir = os.path.abspath(os.path.dirname(__file__))
+
+
+def requirements():
+    with open(os.path.join(own_dir, 'requirements.bdba.txt')) as f:
+        for line in f.readlines():
+            line = line.strip()
+            if not line or line.startswith('#'):
+                continue
+
+            yield line
+
+
+setuptools.setup(
+    name='bdba',
+    version=setup.finalize_version(),
path: setup.bdba.py
user: login: ccwienk
body: not sure we should hardcode version-changes here. in our other package-builds, we do that on pipeline-level; in particular, we do build non-final versions for headupdate-jobs..
created_at: 2025-02-26T10:43:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+import os
+
+import setuptools
+
+import setup
+
+
+own_dir = os.path.abspath(os.path.dirname(__file__))
+
+
+def requirements():
+    with open(os.path.join(own_dir, 'requirements.bdba.txt')) as f:
+        for line in f.readlines():
+            line = line.strip()
+            if not line or line.startswith('#'):
+                continue
+
+            yield line
+
+
+setuptools.setup(
+    name='bdba',
+    version=setup.finalize_version(),
path: setup.bdba.py
user: login: zkdev
body: apparently my comment vanished ???

I prefer to keep it consistent within delivery-service, we will re-iterate this when migrating to github-actions anyways
created_at: 2025-03-06T13:43:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,22 @@
+import urllib.parse
path: bdba/util.py
user: login: 8R0WNI3
body: This is already part of regular `util.py` module.
created_at: 2025-03-07T14:50:28Z
author_association: MEMBER
diff_hunk: @@ -137,3 +137,46 @@ delivery-service:
                 - setuptools
                 - pip
                 - distribution-package
+
+bdba:
+  jobs:
+    manual-release:
+      repo:
+        disable_ci_skip: True
+
+      steps:
+        build_bdba_python_package:
+          output_dir: packages
+
+      traits:
+        component_descriptor:
+          component_name: ocm.software/ocm-gear/bdba
path: .ci/pipeline_definitions
user: login: 8R0WNI3
body: Should we maybe rather call this `ocm.software/ocm-gear/bdba-client` to prevent confusion with BDBA extension (I know the BDBA extension is not a component itself but one might think so looking at this name).
created_at: 2025-03-07T14:54:01Z
author_association: MEMBER
diff_hunk: @@ -20,7 +20,6 @@ def requirements():
 
 def modules():
     return [
-        'bdba.model',
path: setup.utils.py
user: login: 8R0WNI3
body: I think this is still required here for the `odg` package as of now 🤔
created_at: 2025-03-07T14:55:39Z
author_association: MEMBER
diff_hunk: @@ -51,7 +52,15 @@ def main():
         labels=[],
     )
 
+    bdba_cref = ocm.ComponentReference(
path: .ci/component_descriptor
user: login: 8R0WNI3
body: You'll also have to include the BDBA reference in the `.ci/set_dependency_version` script.
created_at: 2025-03-07T14:58:04Z
author_association: MEMBER
diff_hunk: @@ -137,3 +137,46 @@ delivery-service:
                 - setuptools
                 - pip
                 - distribution-package
+
+bdba:
+  jobs:
+    manual-release:
+      repo:
+        disable_ci_skip: True
+
+      steps:
+        build_bdba_python_package:
+          output_dir: packages
+
+      traits:
+        component_descriptor:
+          component_name: ocm.software/ocm-gear/bdba
path: .ci/pipeline_definitions
user: login: zkdev
body: hmm, we agreed to use `bdba` over `bdba-client` as python distribution package name and I think consistency would be helpful

still, I see your point
the ocm-gear namespace is probably not the best fit at all..

let's follow-up after we decide in which repository we want to put the bdba "client" :) 
created_at: 2025-03-07T15:01:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,22 @@
+import urllib.parse
path: bdba/util.py
user: login: zkdev
body: This would introduce a dependency from bdba towards delivery-service 🤔 
created_at: 2025-03-10T09:15:15Z
author_association: MEMBER
diff_hunk: @@ -20,7 +20,6 @@ def requirements():
 
 def modules():
     return [
-        'bdba.model',
path: setup.utils.py
user: login: zkdev
body: `delivery-gear-utils` declares a dependencies towards bdba python package, why should `bdba.model` be included directly?
created_at: 2025-03-10T09:17:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,22 @@
+import urllib.parse
path: bdba/util.py
user: login: 8R0WNI3
body: You're right 😞
created_at: 2025-03-10T09:32:57Z
author_association: MEMBER

PR comments:
user: login: ghost
created_at: 2025-03-07T12:43:06Z
body: This PR proposes changes that would break the pipeline definition:
```
bdba-zkdev-separate-bdba-client: Traceback (most recent call last):
  File "/usr/lib/python3.12/site-packages/concourse/replicator.py", line 141, in render
    definition_descriptor = self._render(definition_descriptor)
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/concourse/replicator.py", line 185, in _render
    'definition': factory.create_pipeline_definition(),
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/site-packages/concourse/factory.py", line 88, in create_pipeline_definition
    self._apply_traits(variant)
  File "/usr/lib/python3.12/site-packages/concourse/factory.py", line 177, in _apply_traits
    transformer.process_pipeline_args(pipeline_def)
  File "/usr/lib/python3.12/site-packages/concourse/model/traits/release.py", line 520, in process_pipeline_args
    raise ValueError(textwrap.dedent(f'''\
ValueError: asset=BuildstepFileAsset(ocm_labels=[{'name': 'gardener.cloud/purposes', 'value': ['python', 'setuptools', 'pip', 'distribution-package']}], type='build-step-file', name='bdba', step_name='build_python_packages', artefact_type='application/data', artefact_extra_id={}, purposes=['python', 'setuptools', 'pip', 'distribution-package'], comment=None, upload_as_github_asset=True, github_asset_name=None, step_output_dir='packages', path='dist/bdba*', prefix='dist/', mode=<FileAssetMode.TAR: 'tar'>)'s step_name refers to an absent build-step. If the step in question is
declared branch-specifically, i.e. via `branch.cfg`, and the current branch is
going to be merged with a branch declaring the pipeline step, this error can be
safely ignored, iff the branch is transient only (not used for release).


```

author_association: NONE
user: login: ghost
created_at: 2025-03-07T12:45:52Z
body: The pipeline-definition has been fixed.
author_association: NONE
user: login: zkdev
created_at: 2025-03-07T13:15:43Z
body: tests and linters will fail because bdba package does not yet exist
-> once merged we will have to make sure to (as an one time activity) release bdba prior to delivery-service
author_association: MEMBER