Skip to content

feat(stackit): add ske_cluster_no_public_endpoint check - #11943

Merged
HugoPBrito merged 5 commits into
prowler-cloud:masterfrom
johannes-engler-mw:feat/stackit-ske-checks
Aug 20, 2026
Merged

HugoPBrito merged 5 commits into
prowler-cloud:masterfrom
johannes-engler-mw:feat/stackit-ske-checks

Conversation

@johannes-engler-mw

@johannes-engler-mw johannes-engler-mw commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Context

Adds the STACKIT Kubernetes Engine (SKE) service and its first check, ske_cluster_no_public_endpoint. This continues the effort to build STACKIT coverage in Prowler (follows #11397 and #11549).

Description

A new SKEService fetches clusters from all audited project regions via list_clusters(project_id, region), and the check reports whether a cluster's Kubernetes API endpoint is reachable from the whole internet.

New check: ske_cluster_no_public_endpoint

  • Severity: High
  • Service: ske
  • Resource group: container
  • Category: internet-exposed

The check is built on Cluster.extensions.acl, which is the generally available control for restricting API server access. STACKIT's security how-to describes it as "The ACL functionality allows you to restrict access to the API server of your cluster to certain IP ranges" and explicitly warns against 0.0.0.0/0. The default is unrestricted.

Cluster state Result
Control plane access scope SNA PASS — private control plane, not internet-reachable
ACL extension disabled / absent FAIL — API server accepts any source address
Allowlist contains 0.0.0.0/0 or ::/0 FAIL
ACL enabled with a restricted allowlist PASS

Why not access_scope as the primary signal? Cluster.network.control_plane.access_scope (PUBLIC/SNA) is a private-preview field — both the SDK docstring and the private clusters how-to state it is behind a feature flag and not yet GA. All three levels of the chain (network, control_plane, access_scope) are optional and default to None, so it is absent on virtually every observable cluster. It is used only to short-circuit SNA clusters to PASS, since a private control plane is not internet-reachable regardless of its ACL.

Dict- and object-shaped responses. Per the review on #11549, nested SDK fields are read through _get_field, which handles both raw dict and SDK model shapes, because _extract_items yields either. Reading only attributes would silently drop the ACL of a dict-shaped response and report an exposed cluster as PASS. There is regression coverage for both shapes plus a parity test asserting they parse identically.

Files changed:

  • prowler/providers/stackit/services/ske/ske_service.pySKEService, Cluster model, ACL / access-scope parsing
  • prowler/providers/stackit/services/ske/ske_cluster_no_public_endpoint/ — check, metadata, __init__
  • prowler/providers/stackit/stackit_provider.py, stackit_regions_by_service.json — register the ske service (eu01, eu02)
  • pyproject.toml, uv.lock — pin stackit-ske==1.12.0
  • docs/user-guide/providers/stackit/getting-started-stackit.mdx — add SKE to the supported services table (and the Object Storage row, which was missing)
  • tests/providers/stackit/services/ske/ — 55 tests

Steps to review

  1. Check out the branch and run the unit tests:
    uv run pytest tests/providers/stackit -q --cov=./prowler/providers/stackit --cov-report=term-missing
    
    The new files are at 100% coverage (128 statements, 0 missed).
  2. Inspect ske_service.py — particularly _parse_acl and _parse_access_scope — and Cluster.has_public_endpoint().
  3. Review the metadata JSON for accuracy (severity, remediation text, AdditionalURLs — all verified to return HTTP 200).

Checklist

SDK/CLI

  • Are there new checks included in this PR? Yes
    • ske_cluster_no_public_endpoint. This adds a new STACKIT API dependency (stackit-ske). The service account needs read access to SKE clusters on the project (project.owner covers it); no change to the existing IaaS/Object Storage credentials.

UI

N/A

API

N/A

License

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Summary by CodeRabbit

  • New Features

    • Added StackIT Kubernetes Engine (SKE) support, including cluster discovery across supported regions.
    • Added a high-severity check for publicly reachable Kubernetes control-plane endpoints.
    • Added detection for unrestricted ACLs and support for private control-plane configurations.
    • Improved connection handling when optional Resource Manager permissions are unavailable.
  • Documentation

    • Updated StackIT prerequisites, supported services, permissions, and endpoint security guidance.
  • Tests

    • Added coverage for SKE discovery, endpoint exposure, ACLs, private control planes, and permission handling.

Adds the STACKIT Kubernetes Engine (SKE) service and its first check,
verifying that a cluster's Kubernetes API endpoint is not reachable from
the whole internet.

The check evaluates Cluster.extensions.acl, which is the generally
available control for restricting API server access ("The ACL
functionality allows you to restrict access to the API server of your
cluster to certain IP ranges"), and whose default is unrestricted:

  - control plane access scope SNA -> PASS (private control plane)
  - ACL extension disabled         -> FAIL (API open to any source)
  - allowlist contains 0.0.0.0/0
    or ::/0                        -> FAIL
  - otherwise                      -> PASS

Cluster.network.control_plane.access_scope is deliberately not the
primary signal: it is a private-preview field behind a feature flag, and
all three levels of the chain are optional, so it is absent on virtually
every cluster. It is only used to short-circuit SNA clusters to PASS,
since a private control plane is not internet-reachable regardless of
its ACL.

Nested SDK fields are read through _get_field, which handles both raw
dict and SDK model shapes, because _extract_items yields either.
Reading only attributes would silently drop the ACL of a dict-shaped
response and report an exposed cluster as PASS.

- prowler/providers/stackit/services/ske/ske_service.py: SKEService,
  Cluster model, ACL/access-scope parsing
- prowler/providers/stackit/services/ske/ske_cluster_no_public_endpoint/:
  check, metadata, __init__
- stackit_provider.py + stackit_regions_by_service.json: register the
  ske service (eu01, eu02)
- pyproject.toml + uv.lock: pin stackit-ske==1.12.0
- docs: add SKE and Object Storage rows to the supported services table
- tests: 55 tests, 100% coverage of the new files, covering dict-shaped
  and SDK-model-shaped responses
@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e377a916-af47-4ce7-bee8-7bcf175aa108

📥 Commits

Reviewing files that changed from the base of the PR and between 71c4f82 and c6d5596.

📒 Files selected for processing (2)
  • prowler/providers/stackit/services/ske/ske_service.py
  • tests/providers/stackit/services/ske/ske_service_test.py

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Adds STACKIT Kubernetes Engine discovery across supported regions. It introduces a check that reports clusters with publicly reachable Kubernetes API endpoints based on control-plane scope and ACL CIDRs.

Changes

STACKIT SKE endpoint security check

Layer / File(s) Summary
SKE provider wiring
pyproject.toml, prowler/providers/stackit/..., docs/user-guide/providers/stackit/...
Registers the SKE SDK, supported regions, provider client mapping, global service client, and documented service and check entries.
Provider permission handling
prowler/providers/stackit/stackit_provider.py, tests/providers/stackit/stackit_provider_test.py, docs/user-guide/providers/stackit/...
Treats Resource Manager HTTP 403 responses as non-fatal during connection testing. It preserves invalid-token handling and documents service permission checks.
SKE discovery and endpoint model
prowler/providers/stackit/services/ske/ske_service.py, tests/providers/stackit/services/ske/ske_service_test.py
Fetches clusters across regions, normalizes SDK responses, handles regional API errors, parses access scope and ACL data, and determines public endpoint reachability.
Public endpoint check and validation
prowler/providers/stackit/services/ske/ske_cluster_no_public_endpoint/..., tests/providers/stackit/services/ske/ske_cluster_no_public_endpoint/..., prowler/changelog.d/...
Adds metadata and PASS/FAIL reporting for private control planes, restricted ACLs, disabled ACLs, and unrestricted CIDRs. Tests cover these scenarios and the changelog documents the check.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to c6d55

The change is merge-ready after normal checks and review; no actionable merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant SKEService
  participant StackitProvider
  participant SkeDefaultApi
  participant EndpointCheck
  SKEService->>StackitProvider: generate regional SKE clients
  StackitProvider->>SkeDefaultApi: create regional API client
  SKEService->>SkeDefaultApi: list clusters by region
  SkeDefaultApi-->>SKEService: return cluster data
  SKEService->>SKEService: parse access scope and ACL
  EndpointCheck->>SKEService: read discovered clusters
  EndpointCheck-->>EndpointCheck: emit PASS or FAIL report
Loading

Possibly related PRs

  • prowler-cloud/prowler#12181: Adds a provider-specific check for publicly exposed Kubernetes cluster API endpoints.
  • prowler-cloud/prowler#12192: Adds a provider-specific Kubernetes cluster security check with a service client, model, metadata, implementation, and tests.

Suggested reviewers: danibarranqueroo, hugopbrito

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: adding the STACKIT SKE public-endpoint check.
Description check ✅ Passed The description includes context, implementation details, review steps, testing, checklist items, dependencies, and applicable SDK, UI, and API notes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the community Opened by the Community label Jul 10, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No Conflicts

No conflict markers, and the branch merges cleanly into its base.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@prowler/providers/stackit/services/ske/ske_cluster_no_public_endpoint/ske_cluster_no_public_endpoint.metadata.json`:
- Around line 20-26: Populate Remediation.Code.CLI with a non-empty STACKIT CLI
remediation command or descriptive CLI guidance for enabling the SKE ACL
extension and restricting API source CIDRs; follow the project’s metadata
conventions if no direct command exists, while preserving the existing
Remediation.Code.Other instructions.

In
`@tests/providers/stackit/services/ske/ske_cluster_no_public_endpoint/ske_cluster_no_public_endpoint_test.py`:
- Around line 43-45: Replace every bare mock.MagicMock reference with a
mock.MagicMock() instance in all test methods of the affected test class/file,
including test_no_clusters, so setting clusters remains isolated to each test
and does not mutate the mock class globally.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c9350071-64d4-469c-82a3-7cd09d8657e0

📥 Commits

Reviewing files that changed from the base of the PR and between 3369e48 and 63a24d9.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock, !**/uv.lock
📒 Files selected for processing (13)
  • docs/user-guide/providers/stackit/getting-started-stackit.mdx
  • prowler/changelog.d/ske-cluster-no-public-endpoint.added.md
  • prowler/providers/stackit/services/ske/__init__.py
  • prowler/providers/stackit/services/ske/ske_client.py
  • prowler/providers/stackit/services/ske/ske_cluster_no_public_endpoint/__init__.py
  • prowler/providers/stackit/services/ske/ske_cluster_no_public_endpoint/ske_cluster_no_public_endpoint.metadata.json
  • prowler/providers/stackit/services/ske/ske_cluster_no_public_endpoint/ske_cluster_no_public_endpoint.py
  • prowler/providers/stackit/services/ske/ske_service.py
  • prowler/providers/stackit/stackit_provider.py
  • prowler/providers/stackit/stackit_regions_by_service.json
  • pyproject.toml
  • tests/providers/stackit/services/ske/ske_cluster_no_public_endpoint/ske_cluster_no_public_endpoint_test.py
  • tests/providers/stackit/services/ske/ske_service_test.py

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.15385% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.89%. Comparing base (3f2e592) to head (c6d5596).
⚠️ Report is 298 commits behind head on master.

❗ There is a different number of reports uploaded between BASE (3f2e592) and HEAD (c6d5596). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (3f2e592) HEAD (c6d5596)
api 1 0
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #11943      +/-   ##
==========================================
- Coverage   93.92%   87.89%   -6.04%     
==========================================
  Files         266     2110    +1844     
  Lines       39576    73490   +33914     
==========================================
+ Hits        37173    64593   +27420     
- Misses       2403     8897    +6494     
Flag Coverage Δ
api ?
prowler-py3.10-aws 90.36% <ø> (?)
prowler-py3.10-azure 90.24% <ø> (?)
prowler-py3.10-config 87.88% <96.15%> (?)
prowler-py3.10-e2enetworks 88.72% <96.15%> (?)
prowler-py3.10-external 79.65% <96.15%> (?)
prowler-py3.10-gcp 90.61% <ø> (?)
prowler-py3.10-github 90.03% <ø> (?)
prowler-py3.10-googleworkspace 89.01% <ø> (?)
prowler-py3.10-iac 90.04% <ø> (?)
prowler-py3.10-kubernetes 90.33% <ø> (?)
prowler-py3.10-lib 87.34% <96.15%> (?)
prowler-py3.10-linode 88.67% <96.15%> (?)
prowler-py3.10-m365 90.24% <ø> (?)
prowler-py3.10-mongodbatlas 89.95% <ø> (?)
prowler-py3.10-nhn 90.28% <ø> (?)
prowler-py3.10-okta 90.33% <ø> (?)
prowler-py3.10-openstack 88.69% <ø> (?)
prowler-py3.10-oraclecloud 88.55% <ø> (?)
prowler-py3.10-scaleway 88.77% <ø> (?)
prowler-py3.10-stackit 88.71% <96.15%> (?)
prowler-py3.10-vercel 88.84% <ø> (?)
prowler-py3.11-aws 90.35% <ø> (?)
prowler-py3.11-azure 90.21% <ø> (?)
prowler-py3.11-config 87.81% <96.15%> (?)
prowler-py3.11-e2enetworks 88.65% <96.15%> (?)
prowler-py3.11-external 79.58% <96.15%> (?)
prowler-py3.11-gcp 90.58% <ø> (?)
prowler-py3.11-github 90.01% <ø> (?)
prowler-py3.11-googleworkspace 88.93% <ø> (?)
prowler-py3.11-iac 89.95% <ø> (?)
prowler-py3.11-kubernetes 90.30% <ø> (?)
prowler-py3.11-lib 87.28% <96.15%> (?)
prowler-py3.11-linode 88.59% <96.15%> (?)
prowler-py3.11-m365 90.15% <ø> (?)
prowler-py3.11-mongodbatlas 89.86% <ø> (?)
prowler-py3.11-nhn 90.26% <ø> (?)
prowler-py3.11-okta 90.31% <ø> (?)
prowler-py3.11-openstack 88.60% <ø> (?)
prowler-py3.11-oraclecloud 88.46% <ø> (?)
prowler-py3.11-scaleway 88.70% <ø> (?)
prowler-py3.11-stackit 88.63% <96.15%> (?)
prowler-py3.11-vercel 88.76% <ø> (?)
prowler-py3.12-aws 90.34% <ø> (?)
prowler-py3.12-azure 90.23% <ø> (?)
prowler-py3.12-config 87.87% <96.15%> (?)
prowler-py3.12-e2enetworks 88.72% <96.15%> (?)
prowler-py3.12-external 79.64% <96.15%> (?)
prowler-py3.12-gcp 90.60% <ø> (?)
prowler-py3.12-github 90.02% <ø> (?)
prowler-py3.12-googleworkspace 89.00% <ø> (?)
prowler-py3.12-iac 90.03% <ø> (?)
prowler-py3.12-kubernetes 90.32% <ø> (?)
prowler-py3.12-lib 87.34% <96.15%> (?)
prowler-py3.12-linode 88.66% <96.15%> (?)
prowler-py3.12-m365 90.23% <ø> (?)
prowler-py3.12-mongodbatlas 89.94% <ø> (?)
prowler-py3.12-nhn 90.27% <ø> (?)
prowler-py3.12-okta 90.32% <ø> (?)
prowler-py3.12-openstack 88.68% <ø> (?)
prowler-py3.12-oraclecloud 88.54% <ø> (?)
prowler-py3.12-scaleway 88.77% <ø> (?)
prowler-py3.12-stackit 88.70% <96.15%> (?)
prowler-py3.12-vercel 88.83% <ø> (?)
prowler-py3.13-aws 90.34% <ø> (?)
prowler-py3.13-azure 90.23% <ø> (?)
prowler-py3.13-config 87.87% <96.15%> (?)
prowler-py3.13-e2enetworks 88.72% <96.15%> (?)
prowler-py3.13-external 79.64% <96.15%> (?)
prowler-py3.13-gcp 90.60% <ø> (?)
prowler-py3.13-github 90.02% <ø> (?)
prowler-py3.13-googleworkspace 89.00% <ø> (?)
prowler-py3.13-iac 90.04% <ø> (?)
prowler-py3.13-kubernetes 90.32% <ø> (?)
prowler-py3.13-lib 87.34% <96.15%> (?)
prowler-py3.13-linode 88.66% <96.15%> (?)
prowler-py3.13-m365 90.24% <ø> (?)
prowler-py3.13-mongodbatlas 89.94% <ø> (?)
prowler-py3.13-nhn 90.28% <ø> (?)
prowler-py3.13-okta 90.32% <ø> (?)
prowler-py3.13-openstack 88.68% <ø> (?)
prowler-py3.13-oraclecloud 88.54% <ø> (?)
prowler-py3.13-scaleway 88.77% <ø> (?)
prowler-py3.13-stackit 88.70% <96.15%> (?)
prowler-py3.13-vercel 88.83% <ø> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
prowler 87.90% <96.15%> (∅)
api ∅ <ø> (∅)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@johannes-engler-mw

Copy link
Copy Markdown
Contributor Author

Question for maintainers: mock.MagicMock (class) vs mock.MagicMock() (instance) in check tests

CodeRabbit flagged the check test in this PR for using ske_client = mock.MagicMock (the class) instead of mock.MagicMock() (an instance). Before changing it I tested both forms, and the result is mixed: the suggested fix breaks the tests, but the underlying diagnosis is real. Since this pattern is repo-wide rather than STACKIT-specific, I'd rather ask than unilaterally change it.

1. The suggested fix (instances) breaks the tests

Each check module binds its client at import time:

from prowler.providers.stackit.services.ske.ske_client import ske_client

The test helper imports the check inside the with mock.patch(...) block, so that module-level global binds to whichever object existed at the first import. With a fresh instance per test, test #2 patches ske_client.ske_client to a new instance, but the already-imported check module still holds test #1's object.

Probe using instances exactly as suggested (test_a sets clusters=[], test_b sets one cluster):

test_a_empty       PASSED
test_b_one_cluster FAILED
  AssertionError: expected 1 finding, got 0 — the check module is still bound to test_a's instance

The same probe with the current class form passes both. Sharing one class object works precisely because every test mutates the same target.

2. But CodeRabbit's diagnosis is correct, and slightly worse than stated

some_client = mock.MagicMock followed by some_client.clusters = [...] sets a class attribute on unittest.mock.MagicMock itself, for the rest of the pytest session. It leaks into unrelated mocks, and it silently defeats spec=:

mock.MagicMock.clusters = ["polluted"]              # what the class form does

mock.MagicMock().clusters                            # -> ['polluted']
mock.MagicMock(spec=["items"]).clusters              # -> ['polluted']   (expected AttributeError)

A real class attribute is found by normal attribute lookup before __getattr__, so the spec guard never runs.

Today this is latent, not active: the polluted names (clusters, servers, buckets, subscriptions, …) don't collide with the spec'd names used elsewhere, and CI runs each provider in its own pytest process. The full STACKIT suite passes (256 tests).

3. A form that satisfies both

Keep instances, and additionally rebind the name the check module actually reads:

CHECK_MODULE = (
    "prowler.providers.stackit.services.ske.ske_cluster_no_public_endpoint"
    ".ske_cluster_no_public_endpoint"
)

def _run_check(ske_client):
    with (
        mock.patch(
            "prowler.providers.common.provider.Provider.get_global_provider",
            return_value=set_mocked_stackit_provider(),
        ),
        mock.patch(
            "prowler.providers.stackit.services.ske.ske_service.SKEService",
            new=ske_client,
        ),
        mock.patch(
            "prowler.providers.stackit.services.ske.ske_client.ske_client",
            new=ske_client,
        ),
        mock.patch(f"{CHECK_MODULE}.ske_client", new=ske_client),   # <- the only addition
    ):
        from prowler.providers.stackit.services.ske.ske_cluster_no_public_endpoint.ske_cluster_no_public_endpoint import (
            ske_cluster_no_public_endpoint,
        )

        return ske_cluster_no_public_endpoint().execute()

With that, ske_client = mock.MagicMock() works per-test, mock.MagicMock is never mutated (assert not hasattr(mock.MagicMock, "clusters") holds), and all tests pass.

4. Scope — this is why I'm asking rather than pushing

The class form is the established convention well beyond this PR:

Provider Check-test files using = mock.MagicMock
azure 165
stackit 9 (8 existing + this PR)
gcp / openstack 0

So the options are:

  1. Apply the fix to this PR's test only. Removes a real (if latent) defect, but makes this file inconsistent with the other 8 STACKIT check tests.
  2. Leave this PR as-is for consistency, and track the isolation fix separately as a repo-wide test-hygiene change.
  3. Leave it entirely — the pollution is latent and the class form is deliberate.

I'm happy to do whichever you prefer; (2) seems the least disruptive, and I'd be glad to open the follow-up. Just let me know and I'll update the PR accordingly.

For reference, the other CodeRabbit comment on this PR asks for a non-empty Remediation.Code.CLI. I've left it empty to match convention: 525 of 1503 check metadata files have an empty CLI, including all of the most recently merged checks and the sibling iaas_server_public_ip_attached. Happy to add descriptive text if you'd rather.

@johannes-engler-mw

Copy link
Copy Markdown
Contributor Author

@danibarranqueroo Any news about this?

@johannes-engler-mw

Copy link
Copy Markdown
Contributor Author

@HugoPBrito Any news about that?

@HugoPBrito HugoPBrito added the provider/stackit Issues/PRs related with the Stackit provider label Aug 18, 2026
@HugoPBrito

HugoPBrito commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Thanks for the thorough investigation @johannes-engler-mw, and sorry for the delay.

I believe option 2 is the way to go. Please keep the existing pattern in this PR for consistency, and track the MagicMock class pollution as a separate repository-wide test-hygiene change. Then feel free to open the follow-up issue.

@johannes-engler-mw

Copy link
Copy Markdown
Contributor Author

Thanks @HugoPBrito — going with option 2.

  • This PR stays as-is: the test keeps the existing mock.MagicMock class form, consistent with the other 8 STACKIT check tests, and Remediation.Code.CLI stays empty to match the convention used by the sibling checks.
  • Follow-up filed: test: check tests mutate the unittest.mock.MagicMock class, leaking attributes across the pytest session #12481. It covers the class-attribute pollution repo-wide (483 check-test files across 8 providers), the working fix, and a staged rollout starting with STACKIT as the reference implementation. Happy to take that on once you've had a look at the approach.

Nothing further pending on my side here — ready for review whenever you have a moment.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 18, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 18, 2026
@HugoPBrito

Copy link
Copy Markdown
Contributor

Hi @johannes-engler-mw, could you please provide some evidence of the check working as expected? It must be censored.

SKE enablement is per project and region. A region the project never
enabled answers list_clusters with 403 "Service not enabled", which is
byte-identical in status to a missing IAM role. handle_api_error mapped
it to StackITInvalidTokenError, so a project with SKE in eu01 but not
eu02 aborted the whole scan and reported zero findings behind the
message "service account lacks the required permissions".

_fetch_all_regions already skipped the 404 that IaaS and Object Storage
return for an unprovisioned region; SKE is the only STACKIT service of
the three that uses 403 for this. Measured on a project with neither
service enabled: object storage answers 404 project.not_found, SKE
answers 403 "Service not enabled". The body message is therefore the
only discriminator against a genuine permission failure, which still
aborts loudly.

Found by running the check against real infrastructure -- the mocked
tests passed both before and after.
@johannes-engler-mw

Copy link
Copy Markdown
Contributor Author

Evidence: ske_cluster_no_public_endpoint against a live SKE cluster

@HugoPBrito here you go — run against a real SKE cluster, censored. Also thanks for the revision commit; I rebased on top of it.

Scan output (identifiers redacted)
$ prowler stackit -c ske_cluster_no_public_endpoint \
    --stackit-project-id <REDACTED> \
    --stackit-service-account-key-path ./sa-key.json

-> Using the StackIT credentials below:
  ·   Project Name: <REDACTED>
  Project ID: <REDACTED>
  Service Account Key: ./sa-key.json
  Auth Method: service account key (auto-refresh)

-> Using the following configuration:
  · Config File: /path/to/prowler/prowler/config/config.yaml

Executing 1 check, please wait...
-> Scan completed! |▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉▉| 1/1 [100%] in 1.4s 

Overview Results:
╭─────────────────┬───────────────────┬────────────────╮
│ 0.0% (0) Failed │ 100.0% (1) Passed │ 0.0% (0) Muted │
╰─────────────────┴───────────────────┴────────────────╯

Project <REDACTED> Scan Results (severity columns are for fails only):
╭────────────┬───────────┬──────────┬────────────┬────────┬──────────┬───────┬─────────╮
│ Provider   │ Service   │ Status   │   Critical │   High │   Medium │   Low │   Muted │
├────────────┼───────────┼──────────┼────────────┼────────┼──────────┼───────┼─────────┤
│ stackit    │ ske       │ PASS (1) │          0 │      0 │        0 │     0 │       0 │
╰────────────┴───────────┴──────────┴────────────┴────────┴──────────┴───────┴─────────╯

--- finding detail (from the CSV output) ---

CHECK_ID        : ske_cluster_no_public_endpoint
STATUS          : PASS
SEVERITY        : high
SERVICE_NAME    : ske
REGION          : eu01
RESOURCE_NAME   : <REDACTED>
RESOURCE_UID    : <REDACTED>
STATUS_EXTENDED : SKE cluster <REDACTED> restricts access to its Kubernetes API endpoint to 7 allowed CIDR(s).

The cluster has the ACL extension enabled with 7 single-host CIDRs and no 0.0.0.0/0, so PASS is the expected verdict. The STATUS_EXTENDED text reports the CIDR count rather than the CIDRs themselves, which is what makes the output safe to share.

I did not include a live FAIL: producing one means standing up a cluster with its API server open to the internet, which I'd rather not do even briefly. The failing paths (ACL disabled, 0.0.0.0/0, ::/0) are covered by unit tests. Happy to spin up a throwaway cluster and post a censored FAIL too if you want it on the record.

A bug this surfaced — pushed as a follow-up commit

The first live run returned zero findings and logged CRITICAL: StackIT service account lacks the required permissions on this project. That diagnosis was wrong. SKE enablement is per project and region, and a region that never enabled it answers:

403 {"error":"Forbidden","message":"Service not enabled"}

_fetch_all_regions skipped only the 404 an unprovisioned region returns, so this 403 reached handle_api_error, became StackITInvalidTokenError, and aborted the entire scan. Any project with SKE in one region but not another silently scanned nothing — the test project and the one used above both hit it.

_is_service_not_enabled() now discriminates on the body message, since status alone cannot separate this from a genuine missing role — that case still aborts loudly, and both behaviours are covered by new tests (260 STACKIT tests pass). This complements your test_connection change rather than overlapping it: yours makes the probe tolerate 403, this one keeps a discovery 403 that means "not enabled here" from being read as "access denied".

I checked whether the other two services share the gap. They do not, and it is worth recording why:

Service Region without the service Skipped today?
IaaS 404 resource not found: project yes — no enablement concept at all
Object Storage 404 {"key":"project.not_found"} yes — has enable/disable, but reports it as a project 404
SKE 403 Service not enabled was not — fixed here

Measured on a throwaway project that existed but had neither service enabled, so the project-level 404 could not mask the answer. SKE is the only one of the three using 403, so this commit closes the actual exposure. What remains is hygiene: each service hand-rolls its own "which status means skip this region" logic, and these three don't agree — worth hoisting into StackitProvider in a separate PR, happy to open one.

Worth noting that this was only reachable by running against real infrastructure: the mocked tests passed both before and after. Good argument for asking for evidence.

@HugoPBrito

Copy link
Copy Markdown
Contributor

Once all tests pass I'll merge this and this new check will be available in next minor.

Thanks for the evidence, the nice catch and its fix and the contribution overall @johannes-engler-mw.
It's a pleasure reviewing your contributions.

@HugoPBrito
HugoPBrito merged commit 3da4209 into prowler-cloud:master Aug 20, 2026
53 of 54 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Opened by the Community documentation metadata-review new-check provider/stackit Issues/PRs related with the Stackit provider testing-pending

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants