From e6bea9f25aeb9b9f8039688cc3effb9f40c569a3 Mon Sep 17 00:00:00 2001 From: Daniel Barranquero <74871504+danibarranqueroo@users.noreply.github.com> Date: Thu, 12 Feb 2026 14:35:43 +0100 Subject: [PATCH] feat(oraclecloud): add automated OCI regions updater script and CI workflow (#10020) Co-authored-by: Pepe Fagoaga --- .github/workflows/sdk-refresh-oci-regions.yml | 95 ++++++++ prowler/CHANGELOG.md | 1 + util/update_oci_regions.py | 205 ++++++++++++++++++ 3 files changed, 301 insertions(+) create mode 100644 .github/workflows/sdk-refresh-oci-regions.yml create mode 100644 util/update_oci_regions.py diff --git a/.github/workflows/sdk-refresh-oci-regions.yml b/.github/workflows/sdk-refresh-oci-regions.yml new file mode 100644 index 0000000000..337e3e2cda --- /dev/null +++ b/.github/workflows/sdk-refresh-oci-regions.yml @@ -0,0 +1,95 @@ +name: 'SDK: Refresh OCI Regions' + +on: + schedule: + - cron: '0 9 * * 1' # Every Monday at 09:00 UTC + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +env: + PYTHON_VERSION: '3.12' + +jobs: + refresh-oci-regions: + if: github.repository == 'prowler-cloud/prowler' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + pull-requests: write + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: 'master' + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + + - name: Install dependencies + run: pip install oci + + - name: Update OCI regions + env: + OCI_CLI_USER: ${{ secrets.E2E_OCI_USER_ID }} + OCI_CLI_FINGERPRINT: ${{ secrets.E2E_OCI_FINGERPRINT }} + OCI_CLI_TENANCY: ${{ secrets.E2E_OCI_TENANCY_ID }} + OCI_CLI_KEY_CONTENT: ${{ secrets.E2E_OCI_KEY_CONTENT }} + OCI_CLI_REGION: ${{ secrets.E2E_OCI_REGION }} + run: python util/update_oci_regions.py + + - name: Create pull request + id: create-pr + uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 + with: + token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }} + author: 'prowler-bot <179230569+prowler-bot@users.noreply.github.com>' + committer: 'github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>' + commit-message: 'feat(oraclecloud): update commercial regions' + branch: 'oci-regions-update-${{ github.run_number }}' + title: 'feat(oraclecloud): Update commercial regions' + labels: | + status/waiting-for-revision + severity/low + provider/oraclecloud + no-changelog + body: | + ### Description + + Automated update of OCI commercial regions from the official Oracle Cloud Infrastructure Identity service. + + **Trigger:** ${{ github.event_name == 'schedule' && 'Scheduled (weekly)' || github.event_name == 'workflow_dispatch' && 'Manual' || 'Workflow update' }} + **Run:** [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + + ### Changes + + This PR updates the `OCI_COMMERCIAL_REGIONS` dictionary in `prowler/providers/oraclecloud/config.py` with the latest regions fetched from the OCI Identity API (`list_regions()`). + + - Government regions (`OCI_GOVERNMENT_REGIONS`) are preserved unchanged + - Region display names are mapped from Oracle's official documentation + + ### Checklist + + - [x] This is an automated update from OCI official sources + - [x] Government regions (us-langley-1, us-luke-1) preserved + - [x] No manual review of region data required + + ### License + + By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. + + - name: PR creation result + run: | + if [[ "${{ steps.create-pr.outputs.pull-request-number }}" ]]; then + echo "✓ Pull request #${{ steps.create-pr.outputs.pull-request-number }} created successfully" + echo "URL: ${{ steps.create-pr.outputs.pull-request-url }}" + else + echo "✓ No changes detected - OCI regions are up to date" + fi diff --git a/prowler/CHANGELOG.md b/prowler/CHANGELOG.md index 47258ca87c..a0128d64fe 100644 --- a/prowler/CHANGELOG.md +++ b/prowler/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to the **Prowler SDK** are documented in this file. - CSA CCM 4.0 for the AWS provider [(#10018)](https://github.com/prowler-cloud/prowler/pull/10018) - CSA CCM 4.0 for the GCP provider [(#10042)](https://github.com/prowler-cloud/prowler/pull/10042) - CSA CCM for the Azure provider [(#10039)](https://github.com/prowler-cloud/prowler/pull/10039) +- OCI regions updater script and CI workflow [(#10020)](https://github.com/prowler-cloud/prowler/pull/10020) ### 🔄 Changed diff --git a/util/update_oci_regions.py b/util/update_oci_regions.py new file mode 100644 index 0000000000..3dbfe07668 --- /dev/null +++ b/util/update_oci_regions.py @@ -0,0 +1,205 @@ +import base64 +import logging +import os +import re +import sys + +import oci + +# Logging config +logging.basicConfig( + stream=sys.stdout, + format="%(asctime)s [File: %(filename)s:%(lineno)d] \t[Module: %(module)s]\t %(levelname)s: %(message)s", + datefmt="%m/%d/%Y %I:%M:%S %p", + level=logging.INFO, +) + +# OCI regions are fetched dynamically from the Identity API +# No hardcoded display names - following AWS regions updater pattern +# This ensures full automation without manual maintenance + + +def setup_oci_client(): + """ + Set up OCI Identity client using credentials from environment variables. + + Returns: + oci.identity.IdentityClient: Authenticated OCI Identity client + + Raises: + ValueError: If required environment variables are missing + Exception: If authentication fails + """ + logging.info("Setting up OCI client authentication") + + # Validate required environment variables + required_vars = [ + "OCI_CLI_USER", + "OCI_CLI_FINGERPRINT", + "OCI_CLI_TENANCY", + "OCI_CLI_KEY_CONTENT", + ] + + missing_vars = [var for var in required_vars if not os.getenv(var)] + if missing_vars: + raise ValueError( + f"Missing required environment variables: {', '.join(missing_vars)}" + ) + + # Get credentials from environment + user_ocid = os.getenv("OCI_CLI_USER") + fingerprint = os.getenv("OCI_CLI_FINGERPRINT") + tenancy_ocid = os.getenv("OCI_CLI_TENANCY") + key_content_b64 = os.getenv("OCI_CLI_KEY_CONTENT") + region = os.getenv("OCI_CLI_REGION", "us-ashburn-1") + + # Decode base64 private key + try: + key_content = base64.b64decode(key_content_b64).decode("utf-8") + except Exception as e: + raise ValueError(f"Failed to decode OCI_CLI_KEY_CONTENT: {e}") + + # Create OCI config dictionary + config = { + "user": user_ocid, + "fingerprint": fingerprint, + "tenancy": tenancy_ocid, + "key_content": key_content, + "region": region, + } + + # Initialize Identity client + try: + identity_client = oci.identity.IdentityClient(config) + logging.info(f"Successfully authenticated to OCI region: {region}") + return identity_client + except Exception as e: + raise Exception(f"Failed to create OCI Identity client: {e}") + + +def fetch_oci_regions(identity_client): + """ + Fetch all OCI commercial regions using the Identity service API. + + Args: + identity_client (oci.identity.IdentityClient): Authenticated OCI client + + Returns: + dict: Dictionary mapping region identifiers to themselves (for consistency) + + Raises: + Exception: If API call fails + """ + logging.info("Fetching OCI commercial regions from Identity service") + + try: + # Call list_regions() API - returns all OC1 commercial regions + response = identity_client.list_regions() + regions = response.data + + logging.info(f"Successfully fetched {len(regions)} regions from OCI API") + + # Build region dictionary (region_id -> region_id) + # Following AWS pattern: no display names, fully automated + region_dict = {} + for region in regions: + region_id = region.name + region_dict[region_id] = region_id + logging.debug(f"Added region: {region_id}") + + # Sort regions alphabetically by key + region_dict = dict(sorted(region_dict.items())) + + logging.info(f"Processed {len(region_dict)} commercial regions") + return region_dict + + except Exception as e: + raise Exception(f"Failed to fetch OCI regions: {e}") + + +def update_config_file(regions, config_file_path): + """ + Update the OCI config file with new commercial regions while preserving government regions. + + Args: + regions (dict): Dictionary of region identifiers to region identifiers + config_file_path (str): Path to the config.py file + + Raises: + Exception: If file operations fail or validation fails + """ + logging.info(f"Updating config file: {config_file_path}") + + # Read current config file + try: + with open(config_file_path, "r") as f: + config_content = f.read() + except Exception as e: + raise Exception(f"Failed to read config file: {e}") + + # Generate new OCI_COMMERCIAL_REGIONS dictionary + new_regions_dict = "OCI_COMMERCIAL_REGIONS = {\n" + for region_id in regions.keys(): + new_regions_dict += f' "{region_id}": "{region_id}",\n' + new_regions_dict += "}" + + # Replace OCI_COMMERCIAL_REGIONS using regex + pattern = r"OCI_COMMERCIAL_REGIONS\s*=\s*\{[^}]*\}" + updated_content = re.sub(pattern, new_regions_dict, config_content) + + # Validate that government regions still exist + if "OCI_GOVERNMENT_REGIONS" not in updated_content: + raise Exception( + "Validation failed: OCI_GOVERNMENT_REGIONS section missing after update. Aborting to prevent data loss." + ) + + # Verify the replacement was successful + if updated_content == config_content: + logging.warning("No changes detected in config file") + return + + # Write updated content back to file + try: + with open(config_file_path, "w") as f: + f.write(updated_content) + logging.info("Successfully updated config file") + except Exception as e: + raise Exception(f"Failed to write updated config file: {e}") + + # Log summary of changes + logging.info(f"Updated OCI_COMMERCIAL_REGIONS with {len(regions)} regions") + + +def main(): + """ + Main execution function for OCI regions updater. + """ + try: + # Setup OCI client with authentication + identity_client = setup_oci_client() + + # Fetch all commercial regions from OCI API + commercial_regions = fetch_oci_regions(identity_client) + + # Update config.py file + config_file_path = os.path.join( + os.path.dirname(os.path.realpath(__file__)), + "..", + "prowler", + "providers", + "oraclecloud", + "config.py", + ) + + update_config_file(commercial_regions, config_file_path) + + logging.info("OCI regions update completed successfully") + return 0 + + except Exception as e: + logging.error(f"Error during OCI regions update: {e}") + return 1 + + +if __name__ == "__main__": + sys.exit(main())