From 5c510fe33ce3945ec2e996694d1372e02e042716 Mon Sep 17 00:00:00 2001 From: jmsteur Date: Wed, 8 Oct 2025 14:12:20 +0200 Subject: [PATCH] Commit for secure meta data extract from id --- .../LICENSE | 5 +- .../README.md | 126 ++++ .../main.tf | 39 ++ .../outputs.tf | 15 + .../providers.tf | 4 + ...confidential_app_and_get_saml_meta_data.sh | 277 +++++++++ .../terraform.tfvars.example | 8 + .../variables.tf | 41 ++ .../README.md | 298 --------- .../classes/__init__.py | 1 - .../classes/api_key_filter.py | 144 ----- .../classes/audit_fetcher.py | 403 ------------ .../classes/cloudguard_fetcher.py | 364 ----------- .../classes/command_parser.py | 42 -- .../classes/commands/__init__.py | 14 - .../classes/commands/audit_commands.py | 582 ------------------ .../classes/commands/base_command.py | 30 - .../classes/commands/cloudguard_commands.py | 507 --------------- .../classes/commands/command_history.py | 92 --- .../classes/commands/control_commands.py | 213 ------- .../classes/commands/exceptions.py | 13 - .../classes/commands/filter_commands.py | 102 --- .../classes/commands/registry.py | 33 - .../classes/commands/standard_commands.py | 156 ----- .../classes/compartment_structure.py | 174 ------ .../classes/config_manager.py | 55 -- .../classes/csv_loader_duckdb.py | 50 -- .../classes/data_validator.py | 16 - .../classes/directory_selector.py | 68 -- .../classes/file_selector.py | 43 -- .../classes/logger.py | 23 - .../classes/oci_config_selector.py | 219 ------- .../classes/output_formatter.py | 33 - .../classes/query_executor_duckdb.py | 37 -- .../classes/query_selector.py | 135 ---- .../config.yaml | 28 - .../healthcheck_forensic_tool.py | 374 ----------- .../json_file_analyzer_showoci.py | 397 ------------ ...chmark_Identity_And_Access_Management.yaml | 295 --------- .../query_files/FORENSIC_Audit.yaml | 26 - .../query_files/FORENSIC_Cloudguard.yaml | 40 -- .../requirements.txt | 8 - .../showoci_zip/showoci.zip | Bin 207505 -> 0 bytes 43 files changed, 513 insertions(+), 5017 deletions(-) rename security/security-design/shared-assets/{oci-security-health-check-forensics => iam-saml-metadata-from-identity-domain}/LICENSE (96%) create mode 100644 security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/README.md create mode 100644 security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/main.tf create mode 100644 security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/outputs.tf create mode 100644 security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/providers.tf create mode 100755 security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/scripts/create_confidential_app_and_get_saml_meta_data.sh create mode 100644 security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/terraform.tfvars.example create mode 100644 security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/variables.tf delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/README.md delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/__init__.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/api_key_filter.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/audit_fetcher.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/cloudguard_fetcher.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/command_parser.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/__init__.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/audit_commands.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/base_command.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/cloudguard_commands.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/command_history.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/control_commands.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/exceptions.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/filter_commands.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/registry.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/standard_commands.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/compartment_structure.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/config_manager.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/csv_loader_duckdb.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/data_validator.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/directory_selector.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/file_selector.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/logger.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/oci_config_selector.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/output_formatter.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/query_executor_duckdb.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/classes/query_selector.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/config.yaml delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/healthcheck_forensic_tool.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/json_file_analyzer_showoci.py delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/query_files/CIS_3.0.0_OCI_Foundations_Benchmark_Identity_And_Access_Management.yaml delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/query_files/FORENSIC_Audit.yaml delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/query_files/FORENSIC_Cloudguard.yaml delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/requirements.txt delete mode 100644 security/security-design/shared-assets/oci-security-health-check-forensics/showoci_zip/showoci.zip diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/LICENSE b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/LICENSE similarity index 96% rename from security/security-design/shared-assets/oci-security-health-check-forensics/LICENSE rename to security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/LICENSE index 5c3003e43..b6e54866b 100644 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/LICENSE +++ b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/LICENSE @@ -1,4 +1,5 @@ -Copyright (c) 2025 Oracle and/or its affiliates. + +Copyright (c) 2025 Oracle and/or its affiliates. The Universal Permissive License (UPL), Version 1.0 @@ -32,4 +33,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/README.md b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/README.md new file mode 100644 index 000000000..95a6ca652 --- /dev/null +++ b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/README.md @@ -0,0 +1,126 @@ +# Automating SAML Metadata Retrieval in OCI Identity Domains + +This solution provisions an **OCI Identity Domain** with Terraform and securely retrieves its **SAML metadata** for integration with external identity providers. + +Instead of making the SAML metadata publicly accessible by enabling the *“Configure client access”* option under *Access Signing Certificate*, this approach uses a **temporary confidential OAuth client** to authenticate and download the metadata. + +The workflow is: + +1. **Provisioning with Terraform (Optional)** + + * Deploy the OCI Identity Domain. + * Trigger a shell script via `null_resource` provisioner after domain creation. + +2. **Automated Metadata Retrieval via Shell Script** + + * Register a **temporary confidential application** that supports the *client credentials* grant type via OCI CLI. + * Request an `access_token` from the Identity Domain OAuth endpoint using the app's credentials. + * Call the `/fed/v1/metadata` endpoint with `Authorization: Bearer` header. + * Download and save the SAML metadata XML to a local file (`oci_idcs_metadata.xml`). + * Deactivate and delete the temporary confidential app (cleanup). + * Print the `client_id` and masked `client_secret` to stdout (not stored in Terraform state). + +3. **Security Benefits** + + * **No Public Exposure**: The SAML metadata endpoint remains private and is never exposed to the internet. + * **Authenticated Access**: Only the temporary, authenticated client can retrieve the metadata. + * **Ephemeral Credentials**: The OAuth client is destroyed immediately after use, minimizing the attack surface. + * **State-Free Secrets**: The client credentials are never written to the Terraform state file. + +This method provides both **automation** and **security**, ensuring SAML metadata can be retrieved programmatically without compromising the confidentiality of signing certificates. + + +## Resources Created + +### By Terraform (Optional): +1. **OCI Identity Domain** (`oci_identity_domain.identity_domain`) - A managed identity domain in OCI with specified compartment, display name, description, license type, and home region +2. **Null Resource** (`null_resource.configure_idcs_app`) - Triggers the shell script after domain creation +3. The **OCI Identity Domain** is not destroyed after creation. The SAML metadate would then be invalid. + +### By Shell Script (`create_confidential_app_and_get_saml_meta_data.sh`): +1. **Confidential OAuth Client App** - Created in the new identity domain with: + - Client credentials grant type + - Confidential client type + - Custom web app template +2. **SAML Metadata XML File** (`oci_idcs_metadata.xml`) - Downloaded to local filesystem +3. **Cleanup** - By default, the script deactivates and deletes the confidential app after downloading metadata (unless `KEEP_APP=true`) + +## Prerequisites + +### Required Tools: +- **OCI CLI** with Identity Domains commands +- **jq** (JSON processor) +- **curl** +- **python3** +- **Terraform** ≥ 1.5.0 + +### Required Configuration: +1. **OCI CLI configured** with valid profile (default: `DEFAULT`) +2. **terraform.tfvars** populated with: + - `compartment_id` (OCID of the enclosing compartment) + - `region` + - `domain_display_name` + - `domain_description` + - `license_type` + - `tenancy_ocid` + - `oci_profile` + +### Optional (for cleanup): +- `ADMIN_ACCESS_TOKEN` or +- `ADMIN_CLIENT_ID` + `ADMIN_CLIENT_SECRET` (for proper app cleanup with admin privileges) + +## How to Run + +1. **Initialize**: `terraform init` +2. **Plan**: `terraform plan -out tfplan` +3. **Apply**: `terraform apply tfplan` + +### Environment Variables (optional): +- `APP_NAME` - Custom app name (default: `saml-metadata-client`) +- `SCOPE` - OAuth scope (default: `urn:opc:idm:__myscopes__`) +- `OUT_XML` - Output file path (default: `oci_idcs_metadata.xml`) +- `KEEP_APP` - Set to `true` to prevent app deletion + +The script automatically receives `IDCS_ENDPOINT` and `PROFILE` from Terraform. + +## Terraform Flow + +During `apply`, the configuration creates the OCI Identity Domain. After the domain exists, Terraform triggers `scripts/create_confidential_app_and_get_saml_meta_data.sh`, which: + +- provisions a confidential OAuth client against the new domain via the OCI CLI, +- regenerates the client secret if necessary, +- retrieves a client-credentials token, and +- downloads the SAML metadata document to `oci_idcs_metadata.xml` (override via `OUT_XML`). + +The script prints the confidential app ID, client ID, and a masked client secret—store the full secret securely outside of Terraform state. + +## Running the Script Standalone + +If an identity domain already exists, you can run the script directly without Terraform: + +```bash +IDCS_ENDPOINT="https://idcs-.identity.oraclecloud.com" \ +PROFILE="oci_profile_name" \ +bash scripts/create_confidential_app_and_get_saml_meta_data.sh +``` + +### Optional Environment Variables: +- `APP_NAME` - Custom app name (default: `saml-metadata-client`) +- `SCOPE` - OAuth scope (default: `urn:opc:idm:__myscopes__`) +- `OUT_XML` - Output file path (default: `oci_idcs_metadata.xml`) +- `KEEP_APP` - Set to `true` to prevent app deletion after metadata retrieval +- `ADMIN_ACCESS_TOKEN` - Bearer token with Identity Domain Administrator rights (preferred for cleanup) +- `ADMIN_CLIENT_ID` / `ADMIN_CLIENT_SECRET` - Admin confidential app credentials (alternative for cleanup) +- `ADMIN_SCOPE` - Admin scope (default: `urn:opc:idm:__myscopes__`) + +### Example with custom settings: +```bash +IDCS_ENDPOINT="https://idcs-abc123.identity.oraclecloud.com" \ +PROFILE="my-oci-profile" \ +APP_NAME="my-metadata-app" \ +OUT_XML="custom_metadata.xml" \ +KEEP_APP="false" \ +bash scripts/create_confidential_app_and_get_saml_meta_data.sh +``` + +This is useful when you need to retrieve SAML metadata from an existing identity domain without provisioning a new one. When you want to keep the confidential application, set the variable KEEP_APP to false. diff --git a/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/main.tf b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/main.tf new file mode 100644 index 000000000..e35e83b72 --- /dev/null +++ b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/main.tf @@ -0,0 +1,39 @@ +terraform { + required_version = ">= 1.5.0" + + required_providers { + oci = { + source = "hashicorp/oci" + } + null = { + source = "hashicorp/null" + } + } +} + +resource "oci_identity_domain" "identity_domain" { + compartment_id = var.compartment_id + display_name = var.domain_display_name + description = var.domain_description + is_hidden_on_login = var.is_hidden_on_login + license_type = var.license_type + home_region = var.region +} + +# Wait until the thing exists. +resource "null_resource" "configure_idcs_app" { + depends_on = [oci_identity_domain.identity_domain] + + triggers = { + identity_domain_id = oci_identity_domain.identity_domain.id + } + + provisioner "local-exec" { + when = create + command = "${path.module}/scripts/create_confidential_app_and_get_saml_meta_data.sh" + environment = { + IDCS_ENDPOINT = oci_identity_domain.identity_domain.url + PROFILE = var.oci_profile + } + } +} diff --git a/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/outputs.tf b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/outputs.tf new file mode 100644 index 000000000..98a43e665 --- /dev/null +++ b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/outputs.tf @@ -0,0 +1,15 @@ + +output "identity_domain_id" { + description = "The OCID of the created identity domain." + value = oci_identity_domain.identity_domain.id +} + +output "identity_domain_url" { + description = "The URL of the created identity domain." + value = oci_identity_domain.identity_domain.url +} + +output "identity_domain_home_region_url" { + description = "The home region URL of the created identity domain." + value = oci_identity_domain.identity_domain.home_region_url +} diff --git a/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/providers.tf b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/providers.tf new file mode 100644 index 000000000..59f3566cb --- /dev/null +++ b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/providers.tf @@ -0,0 +1,4 @@ +provider "oci" { + # Uses ~/.oci/config by default; select profile via var.oci_profile + config_file_profile = var.oci_profile +} \ No newline at end of file diff --git a/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/scripts/create_confidential_app_and_get_saml_meta_data.sh b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/scripts/create_confidential_app_and_get_saml_meta_data.sh new file mode 100755 index 000000000..de7a46f92 --- /dev/null +++ b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/scripts/create_confidential_app_and_get_saml_meta_data.sh @@ -0,0 +1,277 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ------------------------------------------------------------------------------ +# Create a confidential OAuth client in an OCI Identity Domain (IDCS), +# mint a client_credentials access token, and fetch /fed/v1/metadata (SAML). +# Optionally deactivate+delete the app afterwards (default). +# +# Usage (examples): +# IDCS_ENDPOINT="https://idcs-.identity.oraclecloud.com" \ +# PROFILE="DEFAULT" \ +# KEEP_APP="true" \ +# bash create_confidential_app_and_get_saml_meta_data.sh +# +# Notes: +# - Do NOT put "urn:opc:idm:__myscopes__" in allowedScopes; it's a pseudo-scope +# used only at token request time. +# - For least privilege, grant the client only the admin role(s) needed. +# ------------------------------------------------------------------------------ + +umask 077 + +APP_NAME="${APP_NAME:-saml-metadata-client}" +IDCS_ENDPOINT="${IDCS_ENDPOINT:-}" # e.g. https://idcs-.identity.oraclecloud.com +SCOPE="${SCOPE:-urn:opc:idm:__myscopes__}" +OUT_XML="${OUT_XML:-oci_idcs_metadata.xml}" +PROFILE="${PROFILE:-}" +KEEP_APP="${KEEP_APP:-false}" + +# Optional admin creds for cleanup (preferred) +ADMIN_ACCESS_TOKEN="${ADMIN_ACCESS_TOKEN:-}" +ADMIN_CLIENT_ID="${ADMIN_CLIENT_ID:-}" +ADMIN_CLIENT_SECRET="${ADMIN_CLIENT_SECRET:-}" +ADMIN_SCOPE="${ADMIN_SCOPE:-urn:opc:idm:__myscopes__}" + +usage() { + cat <&2; exit 1; } + +need(){ command -v "$1" >/dev/null 2>&1 || { echo "ERROR: '$1' is required." >&2; exit 1; }; } +need oci; need jq; need curl + +profile_arg=(); [[ -n "$PROFILE" ]] && profile_arg=(--profile "$PROFILE") + +APP_JSON="$(mktemp -t app.json.XXXXXX)" +REGEN_JSON="$(mktemp -t regen.json.XXXXXX)" +DEACT_OUT="$(mktemp -t deact.out.XXXXXX)" +trap 'rm -f "$APP_JSON" "$REGEN_JSON" "$DEACT_OUT"' EXIT + +APP_ID_FOR_CLEANUP="" +APP_VERSION_FOR_CLEANUP="" +ACCESS_TOKEN="" # app token for /fed/v1/metadata +ADMIN_TOKEN="" # admin token for cleanup (if available) + +# --- helpers ------------------------------------------------------------------ + +b64_oneline() { base64 | tr -d '\n'; } + +urlenc() { jq -sRr @uri <<<"$1"; } + +get_token_cc () { + # args: client_id client_secret scope -> prints access_token + local cid="$1" sec="$2" sc="${3:-urn:opc:idm:__myscopes__}" + local basic + basic="$(printf '%s:%s' "$cid" "$sec" | b64_oneline)" + curl -sSf -X POST "$IDCS_ENDPOINT/oauth2/v1/token" \ + -H "Authorization: Basic $basic" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials&scope=$(urlenc "$sc")" \ + | jq -r '.access_token // empty' +} + +cleanup_app() { + if [[ "${KEEP_APP}" != "true" && -n "${APP_ID_FOR_CLEANUP}" ]]; then + echo "Cleaning up: deactivating then deleting app ${APP_ID_FOR_CLEANUP} ..." + + # Pick an admin token: explicit ADMIN_ACCESS_TOKEN, or mint from ADMIN_CLIENT_ID/SECRET, else fall back to app token (may fail) + if [[ -n "$ADMIN_ACCESS_TOKEN" ]]; then + ADMIN_TOKEN="$ADMIN_ACCESS_TOKEN" + elif [[ -n "$ADMIN_CLIENT_ID" && -n "$ADMIN_CLIENT_SECRET" ]]; then + echo "Minting admin token for cleanup ..." + ADMIN_TOKEN="$(get_token_cc "$ADMIN_CLIENT_ID" "$ADMIN_CLIENT_SECRET" "$ADMIN_SCOPE" || true)" + fi + [[ -z "$ADMIN_TOKEN" ]] && ADMIN_TOKEN="$ACCESS_TOKEN" + + # Deactivate with SCIM PATCH + If-Match: + if [[ -n "$ADMIN_TOKEN" ]]; then + # body: replace active -> false + read -r -d '' PATCH_BODY <<'JSON' || true +{ + "schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + "Operations": [ + { "op": "replace", "path": "active", "value": false } + ] +} +JSON + if [[ -z "$APP_VERSION_FOR_CLEANUP" ]]; then + echo "Warning: missing meta.version; deactivation may fail without If-Match." >&2 + fi + + # Use -f but don't kill the script; we want to try delete regardless + http_code="$(curl -sS -o "$DEACT_OUT" -w "%{http_code}" -X PATCH \ + "$IDCS_ENDPOINT/admin/v1/Apps/$APP_ID_FOR_CLEANUP" \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + -H "Content-Type: application/scim+json" \ + ${APP_VERSION_FOR_CLEANUP:+ -H "If-Match: ${APP_VERSION_FOR_CLEANUP}"} \ + --data-binary "$PATCH_BODY" || true)" + + if [[ "$http_code" =~ ^2 ]]; then + echo "App ${APP_ID_FOR_CLEANUP} deactivated." + else + echo "Warning: failed to deactivate app (HTTP $http_code)." >&2 + head -n 40 "$DEACT_OUT" >&2 || true + fi + else + echo "Warning: no token available for deactivation; skipping." >&2 + fi + + # Delete with OCI CLI (works once inactive) + if oci identity-domains app delete \ + --endpoint "$IDCS_ENDPOINT" \ + --app-id "$APP_ID_FOR_CLEANUP" \ + --force \ + "${profile_arg[@]}" >/dev/null 2>&1; then + echo "App ${APP_ID_FOR_CLEANUP} deleted." + else + echo "Warning: failed to delete app ${APP_ID_FOR_CLEANUP}." >&2 + fi + fi +} +trap 'cleanup_app' EXIT + +# --- create app --------------------------------------------------------------- + +cat >"$APP_JSON" <<'JSON' +{ + "schemas": ["urn:ietf:params:scim:schemas:oracle:idcs:App"], + "displayName": "__APP_NAME__", + "description": "CLI-created app to call /fed/v1/metadata", + "isOAuthClient": true, + "clientType": "confidential", + "active": true, + "allowedGrants": ["client_credentials"], + "basedOnTemplate": { "value": "CustomWebAppTemplateId" } +} +JSON +# NOTE: Intentionally NO "allowedScopes". "__myscopes__" is NOT grantable. + +sed -i.bak "s/__APP_NAME__/${APP_NAME//\//\\/}/" "$APP_JSON" && rm -f "$APP_JSON.bak" + +echo "Creating confidential app '${APP_NAME}' in ${IDCS_ENDPOINT} ..." +APP_NODE="$( + oci identity-domains app create \ + --endpoint "$IDCS_ENDPOINT" \ + --from-json "file://$APP_JSON" \ + "${profile_arg[@]}" \ + --query 'data' \ + --output json +)" + +# Parse required fields +APP_ID="$(jq -r '.id // empty' <<<"$APP_NODE")" +CLIENT_ID="$( + jq -r ' + (."urn:ietf:params:scim:schemas:oracle:idcs:extension:oauthclient:App".clientId // .clientId // .name // empty) + ' <<<"$APP_NODE" +)" +CREATED_SECRET="$(jq -r '."client-secret" // empty' <<<"$APP_NODE")" +APP_VERSION_FOR_CLEANUP="$(jq -r '.meta.version // empty' <<<"$APP_NODE")" + +if [[ -z "$APP_ID" || -z "$CLIENT_ID" ]]; then + echo "ERROR: Failed to parse APP_ID/CLIENT_ID." >&2 + echo "Full response follows for debugging:" >&2 + echo "$APP_NODE" >&2 + exit 1 +fi +APP_ID_FOR_CLEANUP="$APP_ID" + +echo "Created app: APP_ID=${APP_ID}" +echo "Client ID : ${CLIENT_ID}" + +# --- get secret --------------------------------------------------------------- + +if [[ -n "$CREATED_SECRET" && "$CREATED_SECRET" != "null" ]]; then + CLIENT_SECRET="$CREATED_SECRET" + echo "Client secret returned by create (using that)." +else + echo "Regenerating client secret ..." + cat >"$REGEN_JSON" <&2 + [[ -s "$OUT_XML" ]] && head -n 40 "$OUT_XML" >&2 + exit 1 +fi + +# Quick payload sanity (catch HTML/JSON error pages) +if ! head -n1 "$OUT_XML" | grep -q '&2 + head -n 40 "$OUT_XML" >&2 + exit 1 +fi + +# --- results ------------------------------------------------------------------ + +MASKED_SECRET="${CLIENT_SECRET:0:4}...${CLIENT_SECRET: -4}" +echo "---------------------------------------------" +echo "SUCCESS" +echo "Profile : ${PROFILE:-}" +echo "IDCS_ENDPOINT : $IDCS_ENDPOINT" +echo "APP_ID : $APP_ID" +echo "CLIENT_ID : $CLIENT_ID" +echo "CLIENT_SECRET : $MASKED_SECRET (store securely!)" +echo "METADATA FILE : $OUT_XML" +if [[ "${KEEP_APP}" == "true" ]]; then + echo "Cleanup : SKIPPED (KEEP_APP=true)" + echo + echo "To refresh metadata later with this client, mint a token and call:" + echo " curl -sSf -X POST \"$IDCS_ENDPOINT/oauth2/v1/token\" \\" + echo " -H \"Authorization: Basic \$(printf '%s:%s' '$CLIENT_ID' '***' | base64 | tr -d '\\n')\" \\" + echo " -H \"Content-Type: application/x-www-form-urlencoded\" \\" + echo " -d \"grant_type=client_credentials&scope=$(urlenc "$SCOPE")\" \\" + echo " | jq -r '.access_token' | xargs -I{} curl -sSf -H \"Authorization: Bearer {}\" \\" + echo " \"$IDCS_ENDPOINT/fed/v1/metadata\" > \"$OUT_XML\"" +else + echo "Cleanup : App scheduled for deactivation+deletion now." + cleanup_app + APP_ID_FOR_CLEANUP="" +fi +echo "---------------------------------------------" diff --git a/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/terraform.tfvars.example b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/terraform.tfvars.example new file mode 100644 index 000000000..5a878ff17 --- /dev/null +++ b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/terraform.tfvars.example @@ -0,0 +1,8 @@ +# The compartment where the Identity Domain will be placed in. +compartment_id = "ocid1.compartment.oc1..aaaaaaaa..." +tenancy_ocid = "ocid1.tenancy.oc1..aaaaaaaa..." +region = "eu-frankfurt-1" +domain_display_name = "my-identity-domain" +domain_description = "OCI Identity Domain created with Terraform." +license_type = "premium" +oci_profile = "DEFAULT" diff --git a/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/variables.tf b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/variables.tf new file mode 100644 index 000000000..f47357ebc --- /dev/null +++ b/security/security-design/shared-assets/iam-saml-metadata-from-identity-domain/variables.tf @@ -0,0 +1,41 @@ +variable "tenancy_ocid" { + description = "The OCID of your OCI tenancy." + type = string +} + +variable "region" { + description = "The OCI region where resources will be created." + type = string +} + +variable "compartment_id" { + description = "The OCID of the compartment where the identity domain will be created." + type = string +} + +variable "domain_display_name" { + description = "The display name for the identity domain." + type = string +} + +variable "domain_description" { + description = "A description for the identity domain." + type = string +} + +variable "is_hidden_on_login" { + description = "Whether the identity domain should be hidden on the login page." + type = bool + default = false +} + +variable "license_type" { + description = "The license type for the identity domain (e.g., 'premium', 'free')." + type = string +} + +variable "oci_profile" { + description = "The OCI profile to use from the config file." + type = string + default = "DEFAULT" +} diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/README.md b/security/security-design/shared-assets/oci-security-health-check-forensics/README.md deleted file mode 100644 index de2c6c0a5..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/README.md +++ /dev/null @@ -1,298 +0,0 @@ -# OCI Security Health Check Forensics Tool - -Last updated: 11 June 2025 - -The OCI Security Health Check Forensics Tool (the tool) is designed to load and analyze data from Oracle Cloud Infrastructure (OCI) environments. This tool enables users to import CSV files containing OCI resource information (e.g., compute instances, users, compartments) and perform SQL queries on the data. This data is used to investigate configuration issues etc. - -The tool can also digest audit events and cloud guard problems. These resources can be loaded with different snapshots from a certain date with a number of days prior to that date. - -This data can be used to investiage anomalies. - -## Features -- Automatic OCI data fetching using showoci integration -- **Audit events** and **Cloud Guard problems** fetching with parallel processing -- Advanced filtering capabilities for age-based and compartment analysis -- Interactive tenancy selection from combined OCI configuration files -- Load CSV files with OCI data from multiple tenancies -- Execute SQL queries on the loaded data using DuckDB backend. Stay tuned for autonomous DB support. -- Support for `SHOW TABLES` and `DESCRIBE table_name` commands -- Command history and help system -- Batch query execution from YAML files - -The tool will be used for forensic purposes. Data can be collected by the customer and shipped to Oracle for forensic research. - -The tool is in development and the following is on the backlog: -- Switch back-end DB for large data sets. ADB support. -- Customer documentation to extract data and ship to Oracle in a secure way - -## Know Errors -- Error shown when a query results in an empty data frame when a filter is applied. - -## Installation - -Clone the repository: -```bash -git clone -cd healthcheck-forensic -``` - -Set up a Python virtual environment and install dependencies: -```bash -python3.10 -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt -``` - -The `requirements.txt` file contains dependencies for DuckDB, pandas, OCI SDK, and other required libraries. - -### OCI Configuration Files - -The tool now supports split OCI configuration: - -- **`~/.oci/config`**: Contains only the DEFAULT domain configuration -- **`qt_config`**: Contains additional tenancy configurations - -The tool automatically combines these files when selecting tenancies. This separation allows you to keep your main OCI config clean while managing multiple tenancies in a separate file. - -## Usage - -### Running the Tool - -To start the tool, use: -```bash -python healthcheck_forensic_tool.py -``` -### Interactive Mode - -The tool supports an interactive mode for running SQL queries dynamically. Available commands include: - -#### Basic Commands -- `show tables`: Lists all loaded tables -- `describe `: Displays columns and data types for a given table -- `history`: Shows command history -- `help [command]`: Shows help for commands -- `exit` or `quit`: Exits the application - -#### Data Management -- `set tenancy`: Switch between different OCI tenancies -- `set queries [directory]`: Load queries from YAML files for batch execution -- `run queries`: Execute all loaded queries in sequence - -#### Data Fetching -- `audit_events fetch `: Fetch of audit events prior to specified date. -- `audit_events fetch`: Interactive loader for existing audit data -- `audit_events delete`: Delete audit events files and tables -- `cloudguard fetch `: Fetch of cloud guard problems prior to specified date. -- `cloudguard fetch`: Interactive loader for existing Cloud Guard data -- `cloudguard delete`: Delete Cloud Guard files and tables - -#### Filtering and Analysis -- `filter age `: Filter results by date age -- `filter compartment `: Analyze compartment structures - - `root`: Show root compartment - - `depth`: Show maximum depth - - `tree_view`: Display compartment tree - - `path_to `: Show path to specific compartment - -### Command-line Switches - -| Switch | Description | -|------------------|---------------------------------------------------| -| `--config-file` | Path to the configuration file (`config.yaml`). | -| `--interactive` | Enable interactive SQL mode. | - -Example usage: -```bash -python healthcheck_forensic_tool.py -``` - -## Configuration Options (`config.yaml`) - -| Setting | Description | -|----------------------------|-------------| -| `oci_config_file` | Path to the main OCI config file (default: `~/.oci/config`) | -| `tqt_config_file` | Path to the additional tenancies config file (default: `config/qt_config`) | -| `csv_dir` | Directory for CSV files | -| `prefix` | Filename prefix for filtering CSVs | -| `resource_argument` | Resource argument for showoci (a: all, i: identity, n: network, c: compute, etc.) | -| `delimiter` | Delimiter used in CSV files | -| `case_insensitive_headers` | Convert column headers to lowercase | -| `log_level` | Logging level (`INFO`, `DEBUG`, `ERROR`) | -| `interactive` | Enable interactive mode | -| `audit_worker_count` | Number of parallel workers for audit/Cloud Guard fetching (default: 10) | -| `audit_worker_window` | Hours per batch for parallel fetching (default: 1) | - -### Example `config.yaml` -```yaml -# OCI Configuration -oci_config_file: "~/.oci/config" # Main OCI config (DEFAULT domain) -tqt_config_file: "qt_config" # Additional tenancies - -# Data Management -csv_dir: "data" -prefix: "oci" -resource_argument: "a" - -# Output Settings -output_format: "DataFrame" -log_level: "INFO" -delimiter: "," -case_insensitive_headers: true - -# Interactive Mode -interactive: true - -# Parallel Fetching Configuration -audit_worker_count: 10 -audit_worker_window: 1 -``` - -## Predefined Queries - -Queries can be defined in YAML files for batch execution. Example `queries.yaml`: -```yaml -queries: - - description: "List all users with API access" - sql: "SELECT display_name, can_use_api_keys FROM identity_domains_users WHERE can_use_api_keys = 1" - - description: "Show compute instances by compartment" - sql: "SELECT server_name, compartment_name, status FROM compute WHERE status = 'STOPPED'" - filter: "age last_modified older 30" - sql: "sql: "SELECT server_name, compartment_name, status FROM compute WHERE compartment_name = ''" -``` - -## Example Usage Scenarios - -### Getting Started -```bash -# Start the tool -python healthcheck_forensic_tool.py - -# Select tenancy and load data -# Tool will prompt for tenancy selection from combined configs - -# Basic exploration -CMD> show tables -CMD> describe identity_domains_users -CMD> SELECT COUNT(*) FROM compute; -``` - -### Data Fetching -```bash -# Fetch 2 days of audit events ending June 15, 2025 -CMD> audit_events fetch 15-06-2025 2 - -# Fetch 30 days of Cloud Guard problems ending January 1, 2025 -CMD> cloudguard fetch 01-01-2025 30 - -# Load existing audit data interactively -CMD> audit_events fetch -``` - -### Advanced Analysis -```bash -# Filter API keys older than 90 days -CMD> SELECT display_name, api_keys FROM identity_domains_users; -CMD> filter age api_keys older 90 - -# Analyze compartment structure -CMD> SELECT path FROM identity_compartments; -CMD> filter compartment tree_view -CMD> filter compartment path_to my-compartment -``` - -### Batch Operations -```bash -# Load and run predefined queries -CMD> set queries < Select a query file using the query file browser > -CMD> run queries - -# Switch between tenancies -CMD> set tenancy -``` - -## Data Organization - -The tool organizes data in the following structure: -``` -data/ -├── tenancy1/ -│ ├── tenancy1_20241215_143022/ -│ │ ├── oci_compute.csv -│ │ ├── oci_identity_domains_users.csv -│ │ ├── audit_events_15-06-2025_7.json -│ │ └── cloudguard_problems_15062025_7.json -│ └── tenancy1_20241214_091545/ -└── tenancy2/ - └── tenancy2_20241215_100530/ -``` - -## Logging - -Logging is configured via the `log_level` setting in `config.yaml`. The tool provides detailed logging for: -- Configuration loading and validation -- CSV file loading and table creation -- Query execution and results -- Data fetching operations with progress tracking -- Error handling and troubleshooting information - -## Troubleshooting - -### Common Issues - -**OCI Configuration Problems** -- Ensure both `~/.oci/config` and `config/qt_config` exist and are readable -- Verify that tenancy profiles are properly configured with required keys -- Check that API keys and permissions are correctly set up - -**CSV Loading Issues** -- Ensure CSV files are properly formatted with consistent delimiters -- Column names in queries should match those in the loaded data (case-sensitive by default) -- Check that the specified prefix matches your CSV file naming convention - -**Data Fetching Problems** -- Verify OCI permissions for audit events and Cloud Guard APIs -- Check network connectivity and OCI service availability -- Ensure the date range doesn't exceed OCI's retention periods (365 days for audit events) - -**Query Execution** -- Use DuckDB-compatible SQL syntax -- Table names are derived from CSV filenames (minus prefix and extension) -- Check available tables with `show tables` and column structure with `describe ` - -### Getting Help - -For detailed command help: -```bash -CMD> help # Show all commands -CMD> help audit_events fetch # Show audit_events fetch options -CMD> help filter age # Show filter age options -``` - -## Advanced Features - -### Parallel Data Fetching -The tool supports parallel fetching for large datasets: -- Configurable worker count and time windows -- Progress tracking with detailed summaries -- Automatic retry handling for failed intervals -- Clean temporary file management - -### Smart Configuration Management -- Automatic detection and combination of split OCI configs -- Interactive tenancy selection with metadata display -- Temporary file creation for showoci integration -- Graceful handling of missing or invalid configurations - -### Comprehensive Filtering -- Date-based filtering with flexible column support -- Compartment hierarchy analysis and visualization -- Support for complex nested data structures -- Chainable filter operations on query results - -# License - -Copyright (c) 2025 Oracle and/or its affiliates. - -Licensed under the Universal Permissive License (UPL), Version 1.0. - -See [LICENSE](https://github.com/oracle-devrel/technology-engineering/blob/main/LICENSE) for more details. \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/__init__.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/__init__.py deleted file mode 100644 index c4c5fe72a..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .output_formatter import OutputFormatter \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/api_key_filter.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/api_key_filter.py deleted file mode 100644 index 3aa6ad55c..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/api_key_filter.py +++ /dev/null @@ -1,144 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -api_key_filter.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import pandas as pd -from datetime import datetime, timedelta -import re - -class ApiKeyFilter: - def __init__(self, column_name='api_keys', age_days=90, mode='older'): - """ - Initialize the ApiKeyFilter. - - Parameters: - - column_name (str): The name of the column containing API keys. - - age_days (int): The age threshold in days. - - mode (str): Either 'older' or 'younger' to filter dates accordingly. - 'older' shows dates older than age_days - 'younger' shows dates younger than or equal to age_days - """ - self.column_name = column_name - self.age_days = age_days - self.mode = mode.lower() - self.age_months = self.calculate_months(age_days) - - @staticmethod - def calculate_months(age_days): - """ - Calculate the number of months from the given days. - - Parameters: - - age_days (int): The number of days. - - Returns: - - int: The equivalent number of months. - """ - return (age_days + 29) // 30 # Round up to the nearest month - - def filter(self, df): - """ - Filter the DataFrame based on the age of API keys. - - Parameters: - - df (pd.DataFrame): The DataFrame to filter. - - Returns: - - pd.DataFrame: The filtered DataFrame. - """ - # Define the date threshold - today = datetime.now() - threshold_date = today - timedelta(days=self.age_days) - - # Check if the specified column exists in the DataFrame - if self.column_name not in df.columns: - print(f"Error: Column '{self.column_name}' does not exist in the DataFrame.") - return df - - # Extract the dates from the specified column - def extract_dates(key_str): - dates = [] - if pd.isnull(key_str): - return dates - - # Handle different formats by splitting entries by comma - entries = [entry.strip() for entry in key_str.split(',') if entry.strip()] - - date_formats = ['%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M'] - date_pattern = r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}(?::\d{2})?)' - - for entry in entries: - try: - # Case 1: Just a date string - if re.match(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?$', entry.strip()): - for fmt in date_formats: - try: - date = datetime.strptime(entry.strip(), fmt) - dates.append(date) - break - except ValueError: - continue - - # Case 2: OCID with date (separated by spaces) - else: - # Look for date pattern in the entry - date_matches = re.findall(date_pattern, entry) - if date_matches: - for date_str in date_matches: - for fmt in date_formats: - try: - date = datetime.strptime(date_str, fmt) - dates.append(date) - break - except ValueError: - continue - # Fall back to original colon-based parsing if no date pattern found - elif ':' in entry: - # Split on the first occurrence of ':' - parts = entry.split(':', 1) - if len(parts) > 1: - date_part = parts[1].strip() - for fmt in date_formats: - try: - date = datetime.strptime(date_part, fmt) - dates.append(date) - break - except ValueError: - continue - else: - print(f"Warning: No valid date format found in entry: '{entry}'") - except Exception as e: - print(f"Error parsing date in entry: '{entry}', error: {e}") - - return dates - - # Apply the date extraction to the specified column - df['key_dates'] = df[self.column_name].apply(extract_dates) - - # Determine if any keys match the age criteria based on mode - def check_dates(dates_list): - if not dates_list: - return False - for date in dates_list: - if self.mode == 'older' and date <= threshold_date: - return True - elif self.mode == 'younger' and date >= threshold_date: # Changed from > to >= for inclusive younger - return True - return False - - # Apply the filter to the DataFrame - mask = df['key_dates'].apply(check_dates) - - # Keep rows where the condition is met - filtered_df = df[mask].copy() - - # Drop the temporary 'key_dates' column - filtered_df.drop(columns=['key_dates'], inplace=True) - - return filtered_df \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/audit_fetcher.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/audit_fetcher.py deleted file mode 100644 index 48666e8e4..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/audit_fetcher.py +++ /dev/null @@ -1,403 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -audit_fetcher.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import glob -import json -import logging -import os -import time - -from datetime import datetime, timedelta, timezone -from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import List, Tuple - -import oci -from oci.util import to_dict - -# Configure module-level logger -logger = logging.getLogger(__name__) -logger.setLevel(logging.INFO) - -class AuditFetcher: - """ - Fetch OCI Audit logs in parallel batches and consolidate into a single JSON file. - The window is completely prior to the reference_date (end date). - - Attributes: - reference_date (datetime): End date for retrieval window (UTC). - window (int): Total window size in days prior to reference_date. - workers (int): Number of parallel worker threads. - worker_window (int): Hours per batch. - config (dict): OCI config loaded from file. - compartment_id (str): Tenancy OCID from config. - audit_client (AuditClient): OCI Audit client. - intervals (List[Tuple[datetime, datetime]]): List of (start, end) batches. - verbose (bool): Whether to print detailed progress messages. - status_messages (List[str]): Collected status messages for summary. - """ - def __init__( - self, - reference_date: str, - window: int, - workers: int, - worker_window: int, - profile_name: str = "DEFAULT", - config_file: str = None, - verbose: bool = True - ): - # Parse reference date (this becomes the END date) - try: - self.reference_date = datetime.strptime(reference_date, "%d-%m-%Y").replace(tzinfo=timezone.utc) - except ValueError as ve: - raise ValueError(f"Invalid reference_date format: {ve}") - - self.window = window - self.workers = workers - self.worker_window = worker_window - self.verbose = verbose # Set from parameter instead of defaulting to True - self.status_messages = [] # Store messages for later summary - - # Calculate start and end times (window days BEFORE reference_date) - self.end_time = self.reference_date.replace( - hour=0, minute=0, second=0, microsecond=0 - ) - self.start_time = (self.reference_date - timedelta(days=window)).replace( - hour=0, minute=0, second=0, microsecond=0 - ) - - self._log(f"Audit search window: {self.start_time.strftime('%Y-%m-%d %H:%M:%S UTC')} to {self.end_time.strftime('%Y-%m-%d %H:%M:%S UTC')}") - self._log(f"Window duration: {window} days prior to {self.reference_date.strftime('%Y-%m-%d')}") - - # Load OCI configuration - if config_file: - cfg_location = os.path.expanduser(config_file) - self.config = oci.config.from_file(file_location=cfg_location, profile_name=profile_name) - else: - self.config = oci.config.from_file(profile_name=profile_name) - - self.compartment_id = self.config.get("tenancy") - self.audit_client = oci.audit.AuditClient( - self.config, - retry_strategy=oci.retry.DEFAULT_RETRY_STRATEGY - ) - - # Prepare batch intervals - self.intervals = self._generate_intervals() - - def _log(self, message, level="INFO"): - """Store messages for later display instead of immediate printing when in quiet mode""" - self.status_messages.append(f"[{level}] {message}") - - # Only print immediately if in verbose mode - if self.verbose: - if level == "ERROR": - print(f"ERROR: {message}") - else: - print(message) - - def _generate_intervals(self) -> List[Tuple[datetime, datetime]]: - """Generate a list of (start, end) datetime tuples for each worker batch.""" - intervals: List[Tuple[datetime, datetime]] = [] - current = self.start_time - delta = timedelta(hours=self.worker_window) - - if self.verbose: - self._log(f"Generating audit intervals with {self.worker_window}-hour chunks...") - - while current < self.end_time: - next_end = min(current + delta, self.end_time) - intervals.append((current, next_end)) - if self.verbose: - self._log(f" Interval: {current.strftime('%Y-%m-%d %H:%M')} to {next_end.strftime('%Y-%m-%d %H:%M')}") - current = next_end - - self._log(f"Total audit intervals: {len(intervals)}") - return intervals - - def _fetch_and_write_events(self, start: datetime, end: datetime) -> Tuple[bool, str, str]: - """ - Fetch audit events for a single time window and write to a temp JSON file. - - Returns: - tuple: (success: bool, result: str, timeframe_string: str) - """ - timeframe_string = f"{start.strftime('%d-%m-%Y %H:%M')},{end.strftime('%d-%m-%Y %H:%M')}" - - try: - # Only log fetch attempts in verbose mode - if self.verbose: - self._log(f"Fetching audit events from {start.strftime('%Y-%m-%d %H:%M')} to {end.strftime('%Y-%m-%d %H:%M')}") - - # Use OCI pagination helper to get all events in this interval - response = oci.pagination.list_call_get_all_results( - self.audit_client.list_events, - compartment_id=self.compartment_id, - start_time=start, - end_time=end - ) - events = response.data - logger.info(f"Fetched {len(events)} events from {start} to {end}") - - # Convert to serializable dicts - dicts = [to_dict(ev) for ev in events] - - # Write to temporary file - filename = f"audit_events_{start.strftime('%Y-%m-%dT%H-%M')}_to_{end.strftime('%Y-%m-%dT%H-%M')}.json" - with open(filename, 'w', encoding='utf-8') as f: - json.dump(dicts, f, indent=2) - - # Store detailed results for summary (always store, regardless of verbose mode) - result_msg = f"✓ {start.strftime('%Y-%m-%d %H:%M')}-{end.strftime('%H:%M')}: {len(dicts)} events → {filename}" - self.status_messages.append(result_msg) - - # Only print immediately if verbose - if self.verbose: - print(f" → Found {len(dicts)} audit events, saved to {filename}") - - return (True, filename, timeframe_string) - - except Exception as e: - error_msg = f"Error fetching audit events {start.strftime('%Y-%m-%d %H:%M')} to {end.strftime('%Y-%m-%d %H:%M')}: {e}" - logger.error(error_msg) - self.status_messages.append(f"{error_msg}") - - if self.verbose: - print(error_msg) - return (False, error_msg, timeframe_string) - - def run(self, output_file: str, progress_callback=None) -> Tuple[str, List[str]]: - """ - Execute the fetcher across all intervals and consolidate into a single JSON file. - - Args: - output_file (str): Path to final consolidated JSON file. - progress_callback (callable): Optional function called with each completed batch index. - - Returns: - tuple: (output_file_path: str, failed_timeframes: list) - """ - if self.verbose: - print(f"\nStarting parallel audit fetch with {self.workers} workers...") - print(f"Target output file: {output_file}") - - temp_files: List[str] = [] - failed_timeframes: List[str] = [] - - # Parallel fetching - with ThreadPoolExecutor(max_workers=self.workers) as executor: - future_to_idx = { - executor.submit(self._fetch_and_write_events, s, e): idx - for idx, (s, e) in enumerate(self.intervals) - } - - completed = 0 - total = len(self.intervals) - - for future in as_completed(future_to_idx): - idx = future_to_idx[future] - try: - success, result, timeframe_string = future.result() - - if success: - temp_files.append(result) - else: - failed_timeframes.append(timeframe_string) - # Store failure in status messages - self.status_messages.append(f"FAILED AUDIT TIMEFRAME: {timeframe_string}") - - # Only print immediately if verbose - if self.verbose: - print(f"FAILED AUDIT TIMEFRAME: {timeframe_string}") - print(f"Error: {result}") - - completed += 1 - - # Only show progress in verbose mode (progress bar handles this in quiet mode) - if self.verbose: - print(f"Progress: {completed}/{total} audit intervals completed") - - if progress_callback: - progress_callback(idx) - - except Exception as e: - logger.error(f"Audit batch {idx} exception: {e}") - if self.verbose: - print(f"EXCEPTION in audit batch {idx}: {e}") - - # Consolidate - self._log(f"Consolidating {len(temp_files)} audit temporary files...") - all_events = [] - - for tf in temp_files: - try: - with open(tf, 'r', encoding='utf-8') as f: - batch_events = json.load(f) - all_events.extend(batch_events) - if self.verbose: - self._log(f" → Added {len(batch_events)} audit events from {tf}") - except Exception as e: - logger.error(f"Error reading audit temp file {tf}: {e}") - self._log(f"Error reading audit temp file {tf}: {e}", "ERROR") - - # Sort by event_time if present - self._log(f"Sorting {len(all_events)} total audit events by event time...") - all_events.sort(key=lambda ev: ev.get('eventTime', ev.get('event_time', ''))) - - # Write final file - try: - os.makedirs(os.path.dirname(output_file), exist_ok=True) - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(all_events, f, indent=2) - logger.info(f"Consolidated {len(all_events)} events to {output_file}") - - self._log(f"✓ Consolidated audit file written: {output_file}") - self._log(f"✓ Total audit events found: {len(all_events)}") - - # Show date range of actual data - if all_events: - first_event = all_events[0].get('eventTime', all_events[0].get('event_time', 'Unknown')) - last_event = all_events[-1].get('eventTime', all_events[-1].get('event_time', 'Unknown')) - self._log(f"✓ Event time range: {first_event} to {last_event}") - - return (output_file, failed_timeframes) - except Exception as e: - logger.error(f"Error writing consolidated audit file: {e}") - self._log(f"Error writing consolidated audit file: {e}", "ERROR") - return ("", failed_timeframes) - - def retry_failed_timeframes(self, failed_timeframes: List[str], output_file: str = None) -> Tuple[int, List[str]]: - """ - Retry fetching for specific failed timeframes. - - Args: - failed_timeframes: List of timeframe strings in format "DD-MM-YYYY HH:MM,DD-MM-YYYY HH:MM" - output_file: Optional output file for retry results - - Returns: - tuple: (success_count: int, still_failed: list) - """ - print(f"\n{'='*60}") - print(f"RETRYING {len(failed_timeframes)} FAILED AUDIT TIMEFRAMES") - print(f"{'='*60}") - - retry_intervals = [] - invalid_timeframes = [] - - # Parse timeframe strings back to datetime objects - for tf_string in failed_timeframes: - try: - start_str, end_str = tf_string.split(',') - start_dt = datetime.strptime(start_str, "%d-%m-%Y %H:%M").replace(tzinfo=timezone.utc) - end_dt = datetime.strptime(end_str, "%d-%m-%Y %H:%M").replace(tzinfo=timezone.utc) - retry_intervals.append((start_dt, end_dt)) - print(f" Queued audit retry: {start_dt.strftime('%Y-%m-%d %H:%M')} to {end_dt.strftime('%Y-%m-%d %H:%M')}") - except Exception as e: - print(f" Invalid audit timeframe format '{tf_string}': {e}") - invalid_timeframes.append(tf_string) - - if not retry_intervals: - print("No valid audit timeframes to retry.") - return (0, invalid_timeframes) - - # Execute retries - temp_files = [] - still_failed = [] - - with ThreadPoolExecutor(max_workers=self.workers) as executor: - future_to_timeframe = { - executor.submit(self._fetch_and_write_events, start, end): (start, end) - for start, end in retry_intervals - } - - for future in as_completed(future_to_timeframe): - start, end = future_to_timeframe[future] - success, result, timeframe_string = future.result() - - if success: - temp_files.append(result) - print(f" AUDIT SUCCESS: {timeframe_string}") - else: - still_failed.append(timeframe_string) - print(f" AUDIT STILL FAILED: {timeframe_string}") - - # Consolidate retry results if requested - if output_file and temp_files: - print(f"\nConsolidating {len(temp_files)} audit retry results...") - all_events = [] - - for tf in temp_files: - try: - with open(tf, 'r', encoding='utf-8') as f: - batch_events = json.load(f) - all_events.extend(batch_events) - except Exception as e: - print(f"Error reading audit retry temp file {tf}: {e}") - - # Sort and write - all_events.sort(key=lambda ev: ev.get('eventTime', ev.get('event_time', ''))) - - try: - os.makedirs(os.path.dirname(output_file), exist_ok=True) - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(all_events, f, indent=2) - print(f"✓ Audit retry results written to: {output_file}") - print(f"✓ Total retry audit events found: {len(all_events)}") - except Exception as e: - print(f"Error writing audit retry file: {e}") - - # Report final status - success_count = len(retry_intervals) - len(still_failed) - still_failed.extend(invalid_timeframes) # Include invalid formats - - print(f"\n{'='*60}") - print(f" AUDIT RETRY SUMMARY") - print(f"{'='*60}") - print(f" Successful audit retries: {success_count}") - print(f" Still failed audit: {len(still_failed)}") - - if still_failed: - print("\nAudit timeframes still failing:") - print("STILL_FAILED_AUDIT_TIMEFRAMES = [") - for tf in still_failed: - print(f' "{tf}",') - print("]") - - return (success_count, still_failed) - - def cleanup(self) -> None: - """Remove all temporary batch files matching the audit events pattern.""" - pattern = "audit_events_*_to_*.json" - temp_files = glob.glob(pattern) - - if temp_files: - self._log(f"Cleaning up {len(temp_files)} audit temporary files...") - for tmp in temp_files: - try: - os.remove(tmp) - logger.debug(f"Removed audit temp file {tmp}") - if self.verbose: - self._log(f" → Removed {tmp}") - except Exception as e: - logger.error(f"Failed to remove audit temp file {tmp}: {e}") - self._log(f" → Failed to remove {tmp}: {e}", "ERROR") - else: - self._log("No audit temporary files to clean up.") - - def get_date_range_info(self) -> dict: - """Return information about the calculated date range.""" - return { - "reference_date": self.reference_date.strftime('%Y-%m-%d'), - "window_days": self.window, - "start_time": self.start_time.strftime('%Y-%m-%d %H:%M:%S UTC'), - "end_time": self.end_time.strftime('%Y-%m-%d %H:%M:%S UTC'), - "total_hours": (self.end_time - self.start_time).total_seconds() / 3600, - "worker_window_hours": self.worker_window, - "number_of_intervals": len(self.intervals) - } \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/cloudguard_fetcher.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/cloudguard_fetcher.py deleted file mode 100644 index 26576c80b..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/cloudguard_fetcher.py +++ /dev/null @@ -1,364 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -cloudguard_fetcher.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import oci -import json -import os -import glob -from datetime import datetime, timedelta, timezone -from oci.util import to_dict -from oci.pagination import list_call_get_all_results -from concurrent.futures import ThreadPoolExecutor, as_completed - -class CloudGuardFetcher: - """ - Fetch OCI Cloud Guard problems in parallel batches and consolidate into a single JSON file. - The window is completely prior to the reference_date (end date). - """ - - def __init__( - self, - reference_date: str, - window: int, - workers: int, - worker_window: int, - profile_name: str = "DEFAULT", - config_file: str = None - ): - # Initialize status tracking - self.status_messages = [] - self.verbose = True # Set to False to suppress interval generation messages - - # Parse reference date (this becomes the END date) - try: - self.reference_date = datetime.strptime(reference_date, "%d-%m-%Y").replace(tzinfo=timezone.utc) - except ValueError as ve: - raise ValueError(f"Invalid reference_date format: {ve}") - - self.window = window - self.workers = workers - self.worker_window = worker_window - - # Calculate start and end times (window days BEFORE reference_date) - self.end_time = self.reference_date.replace( - hour=0, minute=0, second=0, microsecond=0 - ) - self.start_time = (self.reference_date - timedelta(days=window)).replace( - hour=0, minute=0, second=0, microsecond=0 - ) - - self._log(f"Search window: {self.start_time.strftime('%Y-%m-%d %H:%M:%S UTC')} to {self.end_time.strftime('%Y-%m-%d %H:%M:%S UTC')}") - self._log(f"Window duration: {window} days prior to {self.reference_date.strftime('%Y-%m-%d')}") - - # Load OCI config - if config_file: - cfg_loc = os.path.expanduser(config_file) - self.config = oci.config.from_file(file_location=cfg_loc, profile_name=profile_name) - else: - self.config = oci.config.from_file(profile_name=profile_name) - self.compartment_id = self.config.get("tenancy") - self.client = oci.cloud_guard.CloudGuardClient( - self.config, - retry_strategy=oci.retry.DEFAULT_RETRY_STRATEGY - ) - - # Prepare batch intervals - self.intervals = self._generate_intervals() - - def _log(self, message, level="INFO"): - """Store messages for later display instead of immediate printing""" - self.status_messages.append(f"[{level}] {message}") - - def _print_summary_report(self): - """Print collected status messages as a summary report""" - if not self.status_messages: - return - - print("\n" + "=" * 80) - print("CLOUD GUARD FETCH SUMMARY REPORT") - print("=" * 80) - for msg in self.status_messages: - print(msg) - print("=" * 80) - - def _generate_intervals(self): - """Generate time intervals for parallel processing.""" - intervals = [] - current = self.start_time - delta = timedelta(hours=self.worker_window) - - if self.verbose: - self._log(f"Generating intervals with {self.worker_window}-hour chunks...") - - while current < self.end_time: - next_end = min(current + delta, self.end_time) - intervals.append((current, next_end)) - if self.verbose: - self._log(f" Interval: {current.strftime('%Y-%m-%d %H:%M')} to {next_end.strftime('%Y-%m-%d %H:%M')}") - current = next_end - - self._log(f"Total intervals: {len(intervals)}") - return intervals - - def _fetch_and_write(self, start: datetime, end: datetime) -> tuple: - """ - Fetch problems for a single time window and write to a temp JSON file. - - Uses the correct parameters `time_last_detected_greater_than_or_equal_to` and - `time_last_detected_less_than_or_equal_to` as per the Python SDK. - - Returns: - tuple: (success: bool, result: str, timeframe_string: str) - """ - timeframe_string = f"{start.strftime('%d-%m-%Y %H:%M')},{end.strftime('%d-%m-%Y %H:%M')}" - - try: - # Only log fetch attempts, not every individual fetch - response = list_call_get_all_results( - self.client.list_problems, - compartment_id=self.compartment_id, - time_last_detected_greater_than_or_equal_to=start, - time_last_detected_less_than_or_equal_to=end - ) - problems = response.data - dicts = [to_dict(p) for p in problems] - - fname = f"cloudguard_problems_{start.strftime('%Y-%m-%dT%H-%M')}_to_{end.strftime('%Y-%m-%dT%H-%M')}.json" - with open(fname, 'w', encoding='utf-8') as f: - json.dump(dicts, f, indent=2) - - # Store detailed results for summary - self._log(f"✓ {start.strftime('%Y-%m-%d %H:%M')}-{end.strftime('%H:%M')}: {len(dicts)} problems → {fname}") - return (True, fname, timeframe_string) - - except Exception as e: - error_msg = f"Error fetching Cloud Guard problems {start.strftime('%Y-%m-%d %H:%M')} to {end.strftime('%Y-%m-%d %H:%M')}: {e}" - self._log(error_msg, "ERROR") - return (False, error_msg, timeframe_string) - - def run(self, output_file: str, progress_callback=None) -> tuple: - """ - Execute the fetching process and consolidate results. - - Args: - output_file: Path for the final consolidated JSON file - progress_callback: Optional callback function for progress updates - - Returns: - tuple: (output_file_path: str, failed_timeframes: list) - """ - # Clear messages and start fresh - self.status_messages = [] - self._log(f"Starting parallel fetch with {self.workers} workers") - self._log(f"Target output file: {output_file}") - - temp_files = [] - failed_timeframes = [] - - with ThreadPoolExecutor(max_workers=self.workers) as executor: - future_to_idx = { - executor.submit(self._fetch_and_write, s, e): idx - for idx, (s, e) in enumerate(self.intervals) - } - - completed = 0 - total = len(self.intervals) - - for future in as_completed(future_to_idx): - idx = future_to_idx[future] - success, result, timeframe_string = future.result() - - if success: - temp_files.append(result) - else: - failed_timeframes.append(timeframe_string) - self._log(f" FAILED: {timeframe_string} - {result}", "ERROR") - - completed += 1 - - if progress_callback: - progress_callback(idx) - - # Consolidate all temp files - self._log(f"Consolidating {len(temp_files)} temporary files...") - all_items = [] - - for tf in temp_files: - try: - with open(tf, 'r', encoding='utf-8') as f: - batch_items = json.load(f) - all_items.extend(batch_items) - # Removed the detailed log per file to clean up output - except Exception as e: - self._log(f"Error reading temp file {tf}: {e}", "ERROR") - - # Sort by last detected time (chronological order) - self._log(f"Sorting {len(all_items)} total problems by detection time...") - all_items.sort(key=lambda ev: ev.get('timeLastDetected', ev.get('time_last_detected', ''))) - - # Write consolidated file - try: - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(all_items, f, indent=2) - self._log(f"✓ Consolidated file written: {output_file}") - self._log(f"✓ Total problems found: {len(all_items)}") - - # Show date range of actual data - if all_items: - first_detection = all_items[0].get('timeLastDetected', 'Unknown') - last_detection = all_items[-1].get('timeLastDetected', 'Unknown') - self._log(f"✓ Detection time range: {first_detection} to {last_detection}") - - # Show summary report after progress bar completes - self._print_summary_report() - - # Report failed timeframes after summary - if failed_timeframes: - print(f"\n{'='*60}") - print(f" {len(failed_timeframes)} TIMEFRAMES FAILED") - print(f"{'='*60}") - print("Copy and paste these timeframes to retry failed intervals:") - print("\nFAILED_TIMEFRAMES = [") - for tf in failed_timeframes: - print(f' "{tf}",') - print("]") - print(f"{'='*60}") - - return (output_file, failed_timeframes) - except Exception as e: - self._log(f"Error writing consolidated file: {e}", "ERROR") - self._print_summary_report() - return ("", failed_timeframes) - - def cleanup(self) -> None: - """Remove temporary files created during processing.""" - temp_pattern = "cloudguard_problems_*_to_*.json" - temp_files = glob.glob(temp_pattern) - - if temp_files: - self._log(f"Cleaning up {len(temp_files)} temporary files...") - for tmp in temp_files: - try: - os.remove(tmp) - self._log(f" → Removed {tmp}") - except Exception as e: - self._log(f" → Failed to remove {tmp}: {e}", "ERROR") - else: - self._log("No temporary files to clean up.") - - def retry_failed_timeframes(self, failed_timeframes: list, output_file: str = None) -> tuple: - """ - Retry fetching for specific failed timeframes. - - Args: - failed_timeframes: List of timeframe strings in format "DD-MM-YYYY HH:MM,DD-MM-YYYY HH:MM" - output_file: Optional output file for retry results - - Returns: - tuple: (success_count: int, still_failed: list) - """ - print(f"\n{'='*60}") - print(f" RETRYING {len(failed_timeframes)} FAILED TIMEFRAMES") - print(f"{'='*60}") - - retry_intervals = [] - invalid_timeframes = [] - - # Parse timeframe strings back to datetime objects - for tf_string in failed_timeframes: - try: - start_str, end_str = tf_string.split(',') - start_dt = datetime.strptime(start_str, "%d-%m-%Y %H:%M").replace(tzinfo=timezone.utc) - end_dt = datetime.strptime(end_str, "%d-%m-%Y %H:%M").replace(tzinfo=timezone.utc) - retry_intervals.append((start_dt, end_dt)) - print(f" Queued: {start_dt.strftime('%Y-%m-%d %H:%M')} to {end_dt.strftime('%Y-%m-%d %H:%M')}") - except Exception as e: - print(f" Invalid timeframe format '{tf_string}': {e}") - invalid_timeframes.append(tf_string) - - if not retry_intervals: - print("No valid timeframes to retry.") - return (0, invalid_timeframes) - - # Execute retries - temp_files = [] - still_failed = [] - - with ThreadPoolExecutor(max_workers=self.workers) as executor: - future_to_timeframe = { - executor.submit(self._fetch_and_write, start, end): (start, end) - for start, end in retry_intervals - } - - for future in as_completed(future_to_timeframe): - start, end = future_to_timeframe[future] - success, result, timeframe_string = future.result() - - if success: - temp_files.append(result) - print(f" SUCCESS: {timeframe_string}") - else: - still_failed.append(timeframe_string) - print(f" STILL FAILED: {timeframe_string}") - - # Consolidate retry results if requested - if output_file and temp_files: - print(f"\nConsolidating {len(temp_files)} retry results...") - all_items = [] - - for tf in temp_files: - try: - with open(tf, 'r', encoding='utf-8') as f: - batch_items = json.load(f) - all_items.extend(batch_items) - except Exception as e: - print(f"Error reading retry temp file {tf}: {e}") - - # Sort and write - all_items.sort(key=lambda ev: ev.get('timeLastDetected', ev.get('time_last_detected', ''))) - - try: - with open(output_file, 'w', encoding='utf-8') as f: - json.dump(all_items, f, indent=2) - print(f"✓ Retry results written to: {output_file}") - print(f"✓ Total retry problems found: {len(all_items)}") - except Exception as e: - print(f"Error writing retry file: {e}") - - # Report final status - success_count = len(retry_intervals) - len(still_failed) - still_failed.extend(invalid_timeframes) # Include invalid formats - - print(f"\n{'='*60}") - print(f" RETRY SUMMARY") - print(f"{'='*60}") - print(f" Successful retries: {success_count}") - print(f" Still failed: {len(still_failed)}") - - if still_failed: - print("\nTimeframes still failing:") - print("STILL_FAILED_TIMEFRAMES = [") - for tf in still_failed: - print(f' "{tf}",') - print("]") - - return (success_count, still_failed) - - def get_date_range_info(self) -> dict: - """Return information about the calculated date range.""" - return { - "reference_date": self.reference_date.strftime('%Y-%m-%d'), - "window_days": self.window, - "start_time": self.start_time.strftime('%Y-%m-%d %H:%M:%S UTC'), - "end_time": self.end_time.strftime('%Y-%m-%d %H:%M:%S UTC'), - "total_hours": (self.end_time - self.start_time).total_seconds() / 3600, - "worker_window_hours": self.worker_window, - "number_of_intervals": len(self.intervals) - } \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/command_parser.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/command_parser.py deleted file mode 100644 index 829d3cc0b..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/command_parser.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -command_parser.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -class CommandParser: - ALIASES = { - 'ls': 'show tables', - 'desc': 'describe', - '!': 'history', - } - - def __init__(self, registry): - self.registry = registry - - def parse(self, user_input: str) -> (str, str): - text = user_input.strip() - if not text: - return None, None - - # 1) apply any aliases - for alias, full in self.ALIASES.items(): - if text == alias or text.startswith(alias + ' '): - text = text.replace(alias, full, 1) - break - - text_lower = text.lower() - - # 2) try to match one of the registered multi‑word commands - # (longest first so “show tables” wins over “show”) - for cmd in sorted(self.registry.all_commands(), key=len, reverse=True): - if text_lower.startswith(cmd): - args = text[len(cmd):].strip() - return cmd, args - - # 3) nothing matched → treat the *entire* line as SQL - return '', text diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/__init__.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/__init__.py deleted file mode 100644 index cc3173365..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# === classes/commands/__init__.py === - -# This file makes the `classes.commands` directory a Python package. -# It can be empty, or you can expose submodules here. - -__all__ = [ - "registry", - "base_command", - "standard_commands", - "filter_commands", - "control_commands", - "command_history", - "exceptions", -] diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/audit_commands.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/audit_commands.py deleted file mode 100644 index 95ec7c6fb..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/audit_commands.py +++ /dev/null @@ -1,582 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -audit_commands.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -from .base_command import Command -from datetime import datetime, timedelta -import os -import json -import glob -import questionary -import pandas as pd -from tqdm import tqdm -from ..audit_fetcher import AuditFetcher - -class AuditEventsFetchCommand(Command): - description = """Fetches OCI audit events or loads existing data. - -USAGE: - audit_events fetch # Fetch new data - audit_events fetch # Load existing data - -FETCH NEW DATA: - audit_events fetch 15-06-2025 7 - → Fetches audit events from June 8-15, 2025 (7 days ending on June 15) - - audit_events fetch 01-01-2025 30 - → Fetches audit events from December 2-31, 2024 (30 days ending on Jan 1) - -LOAD EXISTING DATA: - audit_events fetch - → Shows interactive file selector with details: - - Event count and file size - - Date range and creation time - - Target DuckDB table name - → Loads selected file into DuckDB for querying - -WHAT FETCH DOES: - ✓ Splits time window into parallel worker batches - ✓ Fetches all audit events using OCI Audit API - ✓ Shows clean progress bar with summary report - ✓ Creates: audit_events__.json - ✓ Loads into DuckDB table: audit_events_ - ✓ Provides retry instructions for failed periods - -CONFIGURATION: - audit_worker_count: 10 # Parallel workers (config.yaml) - audit_worker_window: 1 # Hours per batch (config.yaml) - -NOTE: OCI audit logs have a 365-day retention period. The window cannot extend -beyond this limit from the current date.""" - - def execute(self, args): - parts = args.split() - snapshot_dir = self.ctx.query_executor.current_snapshot_dir - if not snapshot_dir: - print("Error: No active tenancy snapshot. Use 'set tenancy' first.") - return - - # Mode 2: Interactive load of existing audit_events JSON files - if len(parts) == 0: - self._interactive_load_existing_data(snapshot_dir) - return - - # Mode 1: Fetch new audit events data - if len(parts) != 2: - print("Usage: audit_events fetch ") - print(" or: audit_events fetch (interactive mode)") - return - - self._fetch_new_data(parts, snapshot_dir) - - def _interactive_load_existing_data(self, snapshot_dir): - """Interactive mode to load existing audit events JSON files""" - pattern = os.path.join(snapshot_dir, "audit_events_*_*.json") - files = glob.glob(pattern) - - if not files: - print(f"No audit events JSON files found in {snapshot_dir}") - print("Use 'audit_events fetch ' to fetch new data first.") - return - - # Analyze files and create rich choices - file_choices = [] - for file_path in sorted(files, key=os.path.getmtime, reverse=True): - filename = os.path.basename(file_path) - file_info = self._analyze_file(file_path) - - choice_text = f"{filename}\n" \ - f" → {file_info['event_count']} events, {file_info['file_size']}, " \ - f"Created: {file_info['created']}\n" \ - f" → Date range: {file_info['date_range']}\n" \ - f" → Will load as table: {file_info['table_name']}" - - file_choices.append({ - 'name': choice_text, - 'value': { - 'path': file_path, - 'filename': filename, - 'table_name': file_info['table_name'] - } - }) - - print("\n" + "=" * 80) - print("LOAD EXISTING AUDIT EVENTS DATA") - print("=" * 80) - - selected = questionary.select( - "Select an audit events JSON file to load into DuckDB:", - choices=file_choices - ).ask() - - if not selected: - print("No file selected.") - return - - # Load the selected file - json_file = selected['path'] - table_name = selected['table_name'] - filename = selected['filename'] - - print(f"\nLoading {filename}...") - self._load_to_duckdb(json_file, table_name) - print(f"✓ Successfully loaded audit events into table: {table_name}") - print(f"✓ Use: SELECT event_name, event_time, source_name, resource_name, user_name FROM {table_name} ORDER BY event_time DESC LIMIT 10;") - - def _analyze_file(self, file_path): - """Analyze an audit events JSON file to extract metadata""" - filename = os.path.basename(file_path) - - # Get file stats - stat = os.stat(file_path) - file_size = self._format_file_size(stat.st_size) - created = datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M') - - # Extract date and window from filename - # Format: audit_events_DD-MM-YYYY_DAYS.json - try: - parts = filename.replace('audit_events_', '').replace('.json', '').split('_') - date_part = parts[0] # DD-MM-YYYY - days_part = parts[1] # DAYS - - # Parse date - end_date = datetime.strptime(date_part, "%d-%m-%Y") - start_date = end_date - pd.Timedelta(days=int(days_part)) - - date_range = f"{start_date.strftime('%B %d')} - {end_date.strftime('%B %d, %Y')} ({days_part} days)" - except: - date_range = "Unknown date range" - - # Count events in JSON - try: - with open(file_path, 'r') as f: - data = json.load(f) - event_count = len(data) if isinstance(data, list) else 0 - except: - event_count = "Unknown" - - # Generate table name - table_name = filename.replace('audit_events_', '').replace('.json', '').replace('-', '') - - return { - 'event_count': event_count, - 'file_size': file_size, - 'created': created, - 'date_range': date_range, - 'table_name': f"audit_events_{table_name}" - } - - def _format_file_size(self, size_bytes): - """Format file size in human readable format""" - if size_bytes == 0: - return "0 B" - size_names = ["B", "KB", "MB", "GB"] - import math - i = int(math.floor(math.log(size_bytes, 1024))) - p = math.pow(1024, i) - s = round(size_bytes / p, 1) - return f"{s} {size_names[i]}" - - def _fetch_new_data(self, parts, snapshot_dir): - """Fetch new audit events data from OCI API""" - reference_date, window = parts - - # Validate reference_date - try: - ref_date = datetime.strptime(reference_date, "%d-%m-%Y") - retention_days = 365 # OCI audit log retention period - if (datetime.now() - ref_date).days > retention_days: - print(f"Error: reference_date must be within the last {retention_days} days") - return - except ValueError: - print("Error: reference_date must be in format DD-MM-YYYY") - return - - # Validate window - try: - window = int(window) - if window < 1 or window > retention_days: - print(f"Error: window must be between 1 and {retention_days} days") - return - - # Check if the window extends beyond retention period - start_date = ref_date - timedelta(days=window) - if (datetime.now() - start_date).days > retention_days: - print(f"Error: The specified window extends beyond the {retention_days}-day audit log retention period") - return - except ValueError: - print("Error: window must be an integer") - return - - # Get configuration - worker_count = self.ctx.config_manager.get_setting("audit_worker_count") or 10 - worker_window = self.ctx.config_manager.get_setting("audit_worker_window") or 1 - - # Initialize fetcher - try: - # Create a quiet fetcher that doesn't print verbose messages during progress - fetcher = AuditFetcher( - reference_date=reference_date, - window=window, - workers=worker_count, - worker_window=worker_window, - profile_name=self.ctx.config_manager.get_setting("oci_profile") or "DEFAULT", - verbose=False # Suppress all verbose output including interval generation - ) - - # Use snapshot_dir for temporary batch files - original_cwd = os.getcwd() - os.chdir(snapshot_dir) - try: - total_intervals = len(fetcher.intervals) - - # Show clean progress without cluttered output - print(f"\nStarting parallel audit fetch with {worker_count} workers...") - print(f"Target: {total_intervals} intervals, {reference_date} ({window} days)") - - with tqdm(total=total_intervals, desc="Fetching audit events", - bar_format="{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]") as pbar: - def progress_callback(idx): - pbar.update(1) - - output_filename = f"audit_events_{reference_date}_{window}.json" - output_path = os.path.join(snapshot_dir, output_filename) - - # Fetch data with clean progress bar (no verbose output) - json_file, failed_timeframes = fetcher.run(output_path, progress_callback) - - # Now show the summary report that was collected during fetching - self._print_fetch_summary(fetcher, json_file, failed_timeframes) - - # Load into DuckDB if we got some data - if json_file and os.path.exists(json_file): - table_name = f"audit_events_{reference_date.replace('-', '')}_{window}" - self._load_to_duckdb(json_file, table_name) - fetcher.cleanup() - print(f"✓ Successfully loaded audit events into table: {table_name}") - print(f"✓ Use: SELECT event_name, event_time, source_name FROM {table_name} ORDER BY event_time DESC LIMIT 10;") - else: - print("❌ No data was successfully fetched") - - finally: - os.chdir(original_cwd) - - except Exception as e: - print(f"Error fetching audit events: {e}") - - def _print_fetch_summary(self, fetcher, json_file, failed_timeframes): - """Print a clean summary report after fetching is complete""" - print("\n" + "=" * 80) - print("AUDIT EVENTS FETCH SUMMARY REPORT") - print("=" * 80) - - # Show successful batches from fetcher's status messages - if hasattr(fetcher, 'status_messages'): - success_count = len([msg for msg in fetcher.status_messages if "✓" in msg]) - print(f"✓ Successful intervals: {success_count}") - - # Show a few examples of successful fetches - success_messages = [msg for msg in fetcher.status_messages if "✓" in msg] - if success_messages: - print("✓ Sample successful intervals:") - for msg in success_messages[:3]: # Show first 3 - print(f" {msg}") - if len(success_messages) > 3: - print(f" ... and {len(success_messages) - 3} more successful intervals") - - if json_file and os.path.exists(json_file): - # Get final file stats - stat = os.stat(json_file) - file_size = self._format_file_size(stat.st_size) - print(f"✓ Consolidated file: {os.path.basename(json_file)} ({file_size})") - - # Count total events - try: - with open(json_file, 'r') as f: - data = json.load(f) - total_events = len(data) if isinstance(data, list) else 0 - print(f"✓ Total events collected: {total_events:,}") - - if data and total_events > 0: - first_event = data[0].get('eventTime', data[0].get('event_time', 'Unknown')) - last_event = data[-1].get('eventTime', data[-1].get('event_time', 'Unknown')) - print(f"✓ Event time range: {first_event} to {last_event}") - except: - print("✓ Consolidated file created (event count unavailable)") - - # Handle failed timeframes - if failed_timeframes: - print(f"\n❌ Failed intervals: {len(failed_timeframes)}") - print("You can retry failed timeframes using the fetcher's retry method") - - print("=" * 80) - - def _load_to_duckdb(self, json_file, table_name): - """Load JSON file into DuckDB with flattening""" - try: - with open(json_file, 'r', encoding='utf-8') as f: - data = json.load(f) - - if not data: - print("Warning: JSON file contains no data") - return - - # Check if table already exists - existing_tables = self.ctx.query_executor.show_tables() - if table_name in existing_tables: - overwrite = questionary.confirm( - f"Table '{table_name}' already exists. Overwrite?" - ).ask() - if not overwrite: - print("Load cancelled.") - return - # Drop existing table - self.ctx.query_executor.conn.execute(f"DROP TABLE IF EXISTS {table_name}") - - # Flatten nested JSON - flattened = [] - for event in data: - flat_event = {} - self._flatten_dict(event, flat_event) - flattened.append(flat_event) - - df = pd.DataFrame(flattened) - - # Register and create table - self.ctx.query_executor.conn.register(table_name, df) - self.ctx.query_executor.conn.execute(f"CREATE TABLE {table_name} AS SELECT * FROM {table_name}") - print(f"Created table '{table_name}' with {len(df)} rows and {len(df.columns)} columns") - - except Exception as e: - print(f"Error loading audit events into DuckDB: {e}") - - def _flatten_dict(self, d, flat_dict, prefix=''): - """Recursively flatten nested dictionaries and handle lists""" - for k, v in d.items(): - key = f"{prefix}{k}" if prefix else k - key = key.replace(' ', '_').replace('-', '_').replace('.', '_') - - if isinstance(v, dict): - self._flatten_dict(v, flat_dict, f"{key}_") - elif isinstance(v, list): - flat_dict[key] = json.dumps(v) if v else None - else: - flat_dict[key] = v - - -class AuditEventsDeleteCommand(Command): - description = """Delete audit events JSON files and their corresponding DuckDB tables. - -USAGE: - audit_events delete - -FUNCTIONALITY: - ✓ Shows interactive list of all audit events files in current snapshot - ✓ Displays file details: size, event count, date range, creation time - ✓ Allows single or multiple file selection - ✓ Confirms deletion with detailed summary - ✓ Removes corresponding DuckDB tables if they exist - ✓ Shows cleanup summary with freed disk space - -EXAMPLE OUTPUT: - Select audit events files to delete: - - [✓] audit_events_15-06-2025_7.json - → 1,243 events, 2.1 MB, June 8-15 2025, Table: audit_events_15062025_7 - - [ ] audit_events_01-01-2025_30.json - → 5,678 events, 8.7 MB, Dec 2-Jan 1 2025, Table: audit_events_01012025_30 - -SAFETY FEATURES: - ✓ Confirmation prompt before deletion - ✓ Shows exactly what will be deleted - ✓ Option to cancel at any time - ✓ Graceful handling of missing tables""" - - def execute(self, args): - snapshot_dir = self.ctx.query_executor.current_snapshot_dir - if not snapshot_dir: - print("Error: No active tenancy snapshot. Use 'set tenancy' first.") - return - - pattern = os.path.join(snapshot_dir, "audit_events_*_*.json") - files = glob.glob(pattern) - - if not files: - print(f"No audit events JSON files found in {snapshot_dir}") - return - - # Analyze files and create choices - file_choices = [] - for file_path in sorted(files, key=os.path.getmtime, reverse=True): - filename = os.path.basename(file_path) - file_info = self._analyze_file(file_path) - - choice_text = f"{filename}\n" \ - f" → {file_info['event_count']} events, {file_info['file_size']}, " \ - f"{file_info['date_range']}\n" \ - f" → Table: {file_info['table_name']}, Created: {file_info['created']}" - - file_choices.append({ - 'name': choice_text, - 'value': { - 'path': file_path, - 'filename': filename, - 'table_name': file_info['table_name'], - 'size_bytes': file_info['size_bytes'] - } - }) - - print("\n" + "=" * 80) - print("DELETE AUDIT EVENTS DATA") - print("=" * 80) - - # Multiple selection - selected_files = questionary.checkbox( - "Select audit events files to delete:", - choices=file_choices - ).ask() - - if not selected_files: - print("No files selected for deletion.") - return - - # Show deletion summary - total_size = sum(f['size_bytes'] for f in selected_files) - total_files = len(selected_files) - - print(f"\n{'='*60}") - print("DELETION SUMMARY") - print(f"{'='*60}") - print(f"Files to delete: {total_files}") - print(f"Total disk space to free: {self._format_file_size(total_size)}") - print("\nFiles and tables to be removed:") - - for file_info in selected_files: - print(f" 📄 {file_info['filename']}") - print(f" 🗃️ {file_info['table_name']} (if exists)") - - # Final confirmation - confirm = questionary.confirm( - f"\n❗ Are you sure you want to delete {total_files} file(s) and their tables?" - ).ask() - - if not confirm: - print("Deletion cancelled.") - return - - # Perform deletion - deleted_files = 0 - deleted_tables = 0 - freed_space = 0 - - existing_tables = self.ctx.query_executor.show_tables() - - for file_info in selected_files: - try: - # Delete JSON file - os.remove(file_info['path']) - deleted_files += 1 - freed_space += file_info['size_bytes'] - print(f"✓ Deleted file: {file_info['filename']}") - - # Delete DuckDB table if it exists - table_name = file_info['table_name'] - if table_name in existing_tables: - self.ctx.query_executor.conn.execute(f"DROP TABLE IF EXISTS {table_name}") - deleted_tables += 1 - print(f"✓ Deleted table: {table_name}") - - except Exception as e: - print(f"❌ Error deleting {file_info['filename']}: {e}") - - # Final summary - print(f"\n{'='*60}") - print("DELETION COMPLETE") - print(f"{'='*60}") - print(f"✓ Files deleted: {deleted_files}") - print(f"✓ Tables deleted: {deleted_tables}") - print(f"✓ Disk space freed: {self._format_file_size(freed_space)}") - - def _analyze_file(self, file_path): - """Analyze an audit events JSON file to extract metadata""" - filename = os.path.basename(file_path) - - # Get file stats - stat = os.stat(file_path) - file_size = self._format_file_size(stat.st_size) - created = datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M') - - # Extract date and window from filename - try: - parts = filename.replace('audit_events_', '').replace('.json', '').split('_') - date_part = parts[0] - days_part = parts[1] - - end_date = datetime.strptime(date_part, "%d-%m-%Y") - start_date = end_date - pd.Timedelta(days=int(days_part)) - - date_range = f"{start_date.strftime('%b %d')} - {end_date.strftime('%b %d %Y')}" - except: - date_range = "Unknown" - - # Count events in JSON - try: - with open(file_path, 'r') as f: - data = json.load(f) - event_count = len(data) if isinstance(data, list) else 0 - except: - event_count = "Unknown" - - # Generate table name - table_name = filename.replace('audit_events_', '').replace('.json', '').replace('-', '') - - return { - 'event_count': event_count, - 'file_size': file_size, - 'size_bytes': stat.st_size, - 'created': created, - 'date_range': date_range, - 'table_name': f"audit_events_{table_name}" - } - - def _format_file_size(self, size_bytes): - """Format file size in human readable format""" - if size_bytes == 0: - return "0 B" - size_names = ["B", "KB", "MB", "GB"] - import math - i = int(math.floor(math.log(size_bytes, 1024))) - p = math.pow(1024, i) - s = round(size_bytes / p, 1) - return f"{s} {size_names[i]}" - - -# Remove the old FetchAuditEventsCommand class (keeping it for backward compatibility if needed) -class FetchAuditEventsCommand(Command): - """Deprecated: Use 'audit_events fetch' instead""" - description = """⚠️ DEPRECATED: Use 'audit_events fetch' instead. - -This command is kept for backward compatibility but will be removed in future versions. -Please use the new audit_events commands: -- audit_events fetch # Fetch new data -- audit_events fetch # Load existing data -- audit_events delete # Delete files""" - - def execute(self, args): - print("⚠️ DEPRECATED: 'fetch audit_events' is deprecated.") - print("Please use the new commands:") - print(" - audit_events fetch # Fetch new data") - print(" - audit_events fetch # Load existing data") - print(" - audit_events delete # Delete files") - print() - - # For now, redirect to the new fetch command - fetch_cmd = AuditEventsFetchCommand(self.ctx) - fetch_cmd.execute(args) \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/base_command.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/base_command.py deleted file mode 100644 index 4214080fd..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/base_command.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -base_command.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -from abc import ABC, abstractmethod - -class ShellContext: - def __init__(self, query_executor, config_manager, logger, history, query_selector, reload_tenancy_fn=None): - self.query_executor = query_executor - self.config_manager = config_manager - self.logger = logger - self.history = history - self.query_selector = query_selector - self.reload_tenancy = reload_tenancy_fn - -class Command(ABC): - description = "No description available." # Default description - - def __init__(self, ctx: ShellContext): - self.ctx = ctx - - @abstractmethod - def execute(self, args: str): - """Perform the command; args is the raw string after the keyword.""" diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/cloudguard_commands.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/cloudguard_commands.py deleted file mode 100644 index 6af5a92cf..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/cloudguard_commands.py +++ /dev/null @@ -1,507 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -cloudguard_commands.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -from .base_command import Command -from datetime import datetime -import os -import glob -import json -import questionary -import pandas as pd -from tqdm import tqdm -from ..cloudguard_fetcher import CloudGuardFetcher - -class CloudGuardFetchCommand(Command): - description = """Fetches OCI Cloud Guard problems or loads existing data. - -USAGE: - cloudguard fetch # Fetch new data - cloudguard fetch # Load existing data - -FETCH NEW DATA: - cloudguard fetch 15-06-2025 7 - → Fetches Cloud Guard problems from June 8-15, 2025 (7 days ending on June 15) - - cloudguard fetch 01-01-2025 30 - → Fetches Cloud Guard problems from December 2-31, 2024 (30 days ending on Jan 1) - -LOAD EXISTING DATA: - cloudguard fetch - → Shows interactive file selector with details: - - Problem count and file size - - Date range and creation time - - Target DuckDB table name - → Loads selected file into DuckDB for querying - -WHAT FETCH DOES: - ✓ Splits time window into parallel worker batches - ✓ Fetches all Cloud Guard problems using OCI API - ✓ Shows clean progress bar with summary report - ✓ Creates: cloudguard_problems__.json - ✓ Loads into DuckDB table: cloudguard_problems_ - ✓ Provides retry instructions for failed periods - -CONFIGURATION: - audit_worker_count: 5 # Parallel workers (config.yaml) - audit_worker_window: 1 # Hours per batch (config.yaml)""" - - def execute(self, args): - parts = args.split() - snapshot_dir = self.ctx.query_executor.current_snapshot_dir - if not snapshot_dir: - print("Error: No active tenancy snapshot. Use 'set tenancy' first.") - return - - # Mode 2: Interactive load of existing cloudguard JSON files - if len(parts) == 0: - self._interactive_load_existing_data(snapshot_dir) - return - - # Mode 1: Fetch new Cloud Guard data - if len(parts) != 2: - print("Usage: cloudguard fetch ") - print(" or: cloudguard fetch (interactive mode)") - return - - self._fetch_new_data(parts, snapshot_dir) - - def _interactive_load_existing_data(self, snapshot_dir): - """Interactive mode to load existing Cloud Guard JSON files""" - pattern = os.path.join(snapshot_dir, "cloudguard_problems_*_*.json") - files = glob.glob(pattern) - - if not files: - print(f"No Cloud Guard JSON files found in {snapshot_dir}") - print("Use 'cloudguard fetch ' to fetch new data first.") - return - - # Analyze files and create rich choices - file_choices = [] - for file_path in sorted(files, key=os.path.getmtime, reverse=True): - filename = os.path.basename(file_path) - file_info = self._analyze_file(file_path) - - choice_text = f"{filename}\n" \ - f" → {file_info['problem_count']} problems, {file_info['file_size']}, " \ - f"Created: {file_info['created']}\n" \ - f" → Date range: {file_info['date_range']}\n" \ - f" → Will load as table: {file_info['table_name']}" - - file_choices.append({ - 'name': choice_text, - 'value': { - 'path': file_path, - 'filename': filename, - 'table_name': file_info['table_name'] - } - }) - - print("\n" + "=" * 80) - print("LOAD EXISTING CLOUD GUARD DATA") - print("=" * 80) - - selected = questionary.select( - "Select a Cloud Guard JSON file to load into DuckDB:", - choices=file_choices - ).ask() - - if not selected: - print("No file selected.") - return - - # Load the selected file - json_file = selected['path'] - table_name = selected['table_name'] - filename = selected['filename'] - - print(f"\nLoading {filename}...") - self._load_to_duckdb(json_file, table_name) - print(f"✓ Successfully loaded Cloud Guard data into table: {table_name}") - print(f"✓ Use: select resource_name, detector_rule_id, risk_level, labels, time_first_detected, time_last_detected, lifecycle_state, lifecycle_detail, detector_id from {table_name} where risk_level = 'HIGH' ORDER BY resource_name") - - def _analyze_file(self, file_path): - """Analyze a Cloud Guard JSON file to extract metadata""" - filename = os.path.basename(file_path) - - # Get file stats - stat = os.stat(file_path) - file_size = self._format_file_size(stat.st_size) - created = datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M') - - # Extract date and window from filename - # Format: cloudguard_problems_DDMMYYYY_DAYS.json - try: - parts = filename.replace('cloudguard_problems_', '').replace('.json', '').split('_') - date_part = parts[0] # DDMMYYYY - days_part = parts[1] # DAYS - - # Parse date - day = date_part[:2] - month = date_part[2:4] - year = date_part[4:8] - end_date = datetime.strptime(f"{day}-{month}-{year}", "%d-%m-%Y") - start_date = end_date - pd.Timedelta(days=int(days_part)) - - date_range = f"{start_date.strftime('%B %d')} - {end_date.strftime('%B %d, %Y')} ({days_part} days)" - except: - date_range = "Unknown date range" - - # Count problems in JSON - try: - with open(file_path, 'r') as f: - data = json.load(f) - problem_count = len(data) if isinstance(data, list) else 0 - except: - problem_count = "Unknown" - - # Generate table name - table_name = filename.replace('cloudguard_problems_', '').replace('.json', '').replace('-', '_') - - return { - 'problem_count': problem_count, - 'file_size': file_size, - 'created': created, - 'date_range': date_range, - 'table_name': f"cloudguard_problems_{table_name}" - } - - def _format_file_size(self, size_bytes): - """Format file size in human readable format""" - if size_bytes == 0: - return "0 B" - size_names = ["B", "KB", "MB", "GB"] - import math - i = int(math.floor(math.log(size_bytes, 1024))) - p = math.pow(1024, i) - s = round(size_bytes / p, 1) - return f"{s} {size_names[i]}" - - def _fetch_new_data(self, parts, snapshot_dir): - """Fetch new Cloud Guard data from OCI API""" - reference_date, window = parts - - # Validate reference_date - try: - ref_date = datetime.strptime(reference_date, "%d-%m-%Y") - retention_days = 365 - if (datetime.now() - ref_date).days > retention_days: - print(f"Warning: reference_date is more than {retention_days} days ago. Data may not be available.") - except ValueError: - print("Error: reference_date must be in format DD-MM-YYYY") - return - - # Validate window - try: - window = int(window) - if window < 1: - print("Error: window must be a positive integer") - return - except ValueError: - print("Error: window must be an integer") - return - - # Get configuration - worker_count = self.ctx.config_manager.get_setting("audit_worker_count") or 5 - worker_window = self.ctx.config_manager.get_setting("audit_worker_window") or 1 - - # Initialize fetcher - try: - fetcher = CloudGuardFetcher( - reference_date=reference_date, - window=window, - workers=worker_count, - worker_window=worker_window, - profile_name=self.ctx.config_manager.get_setting("oci_profile") or "DEFAULT" - ) - - # Use snapshot_dir for temporary batch files - original_cwd = os.getcwd() - os.chdir(snapshot_dir) - try: - total_intervals = len(fetcher.intervals) - with tqdm(total=total_intervals, desc="Fetching Cloud Guard problems") as pbar: - def progress_callback(idx): - pbar.update(1) - - output_filename = f"cloudguard_problems_{reference_date.replace('-', '')}_{window}.json" - output_path = os.path.join(snapshot_dir, output_filename) - - # Fetch data with clean progress bar - json_file, failed_timeframes = fetcher.run(output_path, progress_callback) - - # Handle failed timeframes - if failed_timeframes: - print(f"\n⚠️ Warning: {len(failed_timeframes)} timeframes failed during fetch") - print("You can retry failed timeframes using:") - print("FAILED_TIMEFRAMES = [") - for tf in failed_timeframes[:3]: - print(f' "{tf}",') - if len(failed_timeframes) > 3: - print(f" # ... and {len(failed_timeframes) - 3} more") - print("]") - - # Load into DuckDB if we got some data - if json_file and os.path.exists(json_file): - table_name = f"cloudguard_problems_{reference_date.replace('-', '')}_{window}" - self._load_to_duckdb(json_file, table_name) - fetcher.cleanup() - print(f"✓ Successfully loaded Cloud Guard problems into table: {table_name}") - print(f"✓ Use: SELECT * FROM {table_name} LIMIT 10;") - else: - print("❌ No data was successfully fetched") - - finally: - os.chdir(original_cwd) - - except Exception as e: - print(f"Error fetching Cloud Guard problems: {e}") - - def _load_to_duckdb(self, json_file, table_name): - """Load JSON file into DuckDB with flattening""" - try: - with open(json_file, 'r', encoding='utf-8') as f: - data = json.load(f) - - if not data: - print("Warning: JSON file contains no data") - return - - # Check if table already exists - existing_tables = self.ctx.query_executor.show_tables() - if table_name in existing_tables: - overwrite = questionary.confirm( - f"Table '{table_name}' already exists. Overwrite?" - ).ask() - if not overwrite: - print("Load cancelled.") - return - # Drop existing table - self.ctx.query_executor.conn.execute(f"DROP TABLE IF EXISTS {table_name}") - - # Flatten nested JSON - flattened = [] - for item in data: - flat_item = {} - self._flatten_dict(item, flat_item) - flattened.append(flat_item) - - df = pd.DataFrame(flattened) - - # Register and create table - self.ctx.query_executor.conn.register(table_name, df) - self.ctx.query_executor.conn.execute(f"CREATE TABLE {table_name} AS SELECT * FROM {table_name}") - print(f"Created table '{table_name}' with {len(df)} rows and {len(df.columns)} columns") - - except Exception as e: - print(f"Error loading Cloud Guard data into DuckDB: {e}") - - def _flatten_dict(self, d, flat_dict, prefix=''): - """Recursively flatten nested dictionaries and handle lists""" - for k, v in d.items(): - key = f"{prefix}{k}" if prefix else k - key = key.replace(' ', '_').replace('-', '_').replace('.', '_') - - if isinstance(v, dict): - self._flatten_dict(v, flat_dict, f"{key}_") - elif isinstance(v, list): - flat_dict[key] = json.dumps(v) if v else None - else: - flat_dict[key] = v - - -class CloudGuardDeleteCommand(Command): - description = """Delete Cloud Guard JSON files and their corresponding DuckDB tables. - -USAGE: - cloudguard delete - -FUNCTIONALITY: - ✓ Shows interactive list of all Cloud Guard files in current snapshot - ✓ Displays file details: size, problem count, date range, creation time - ✓ Allows single or multiple file selection - ✓ Confirms deletion with detailed summary - ✓ Removes corresponding DuckDB tables if they exist - ✓ Shows cleanup summary with freed disk space - -EXAMPLE OUTPUT: - Select Cloud Guard files to delete: - - [✓] cloudguard_problems_15062025_7.json - → 67 problems, 145 KB, June 8-15 2025, Table: cloudguard_problems_15062025_7 - - [ ] cloudguard_problems_01012025_30.json - → 234 problems, 892 KB, Dec 2-Jan 1 2025, Table: cloudguard_problems_01012025_30 - -SAFETY FEATURES: - ✓ Confirmation prompt before deletion - ✓ Shows exactly what will be deleted - ✓ Option to cancel at any time - ✓ Graceful handling of missing tables""" - - def execute(self, args): - snapshot_dir = self.ctx.query_executor.current_snapshot_dir - if not snapshot_dir: - print("Error: No active tenancy snapshot. Use 'set tenancy' first.") - return - - pattern = os.path.join(snapshot_dir, "cloudguard_problems_*_*.json") - files = glob.glob(pattern) - - if not files: - print(f"No Cloud Guard JSON files found in {snapshot_dir}") - return - - # Analyze files and create choices - file_choices = [] - for file_path in sorted(files, key=os.path.getmtime, reverse=True): - filename = os.path.basename(file_path) - file_info = self._analyze_file(file_path) - - choice_text = f"{filename}\n" \ - f" → {file_info['problem_count']} problems, {file_info['file_size']}, " \ - f"{file_info['date_range']}\n" \ - f" → Table: {file_info['table_name']}, Created: {file_info['created']}" - - file_choices.append({ - 'name': choice_text, - 'value': { - 'path': file_path, - 'filename': filename, - 'table_name': file_info['table_name'], - 'size_bytes': file_info['size_bytes'] - } - }) - - print("\n" + "=" * 80) - print("DELETE CLOUD GUARD DATA") - print("=" * 80) - - # Multiple selection - selected_files = questionary.checkbox( - "Select Cloud Guard files to delete:", - choices=file_choices - ).ask() - - if not selected_files: - print("No files selected for deletion.") - return - - # Show deletion summary - total_size = sum(f['size_bytes'] for f in selected_files) - total_files = len(selected_files) - - print(f"\n{'='*60}") - print("DELETION SUMMARY") - print(f"{'='*60}") - print(f"Files to delete: {total_files}") - print(f"Total disk space to free: {self._format_file_size(total_size)}") - print("\nFiles and tables to be removed:") - - for file_info in selected_files: - print(f" 📄 {file_info['filename']}") - print(f" 🗃️ {file_info['table_name']} (if exists)") - - # Final confirmation - confirm = questionary.confirm( - f"\n❗ Are you sure you want to delete {total_files} file(s) and their tables?" - ).ask() - - if not confirm: - print("Deletion cancelled.") - return - - # Perform deletion - deleted_files = 0 - deleted_tables = 0 - freed_space = 0 - - existing_tables = self.ctx.query_executor.show_tables() - - for file_info in selected_files: - try: - # Delete JSON file - os.remove(file_info['path']) - deleted_files += 1 - freed_space += file_info['size_bytes'] - print(f"✓ Deleted file: {file_info['filename']}") - - # Delete DuckDB table if it exists - table_name = file_info['table_name'] - if table_name in existing_tables: - self.ctx.query_executor.conn.execute(f"DROP TABLE IF EXISTS {table_name}") - deleted_tables += 1 - print(f"✓ Deleted table: {table_name}") - - except Exception as e: - print(f"❌ Error deleting {file_info['filename']}: {e}") - - # Final summary - print(f"\n{'='*60}") - print("DELETION COMPLETE") - print(f"{'='*60}") - print(f"✓ Files deleted: {deleted_files}") - print(f"✓ Tables deleted: {deleted_tables}") - print(f"✓ Disk space freed: {self._format_file_size(freed_space)}") - - def _analyze_file(self, file_path): - """Analyze a Cloud Guard JSON file to extract metadata""" - filename = os.path.basename(file_path) - - # Get file stats - stat = os.stat(file_path) - file_size = self._format_file_size(stat.st_size) - created = datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M') - - # Extract date and window from filename - try: - parts = filename.replace('cloudguard_problems_', '').replace('.json', '').split('_') - date_part = parts[0] - days_part = parts[1] - - day = date_part[:2] - month = date_part[2:4] - year = date_part[4:8] - end_date = datetime.strptime(f"{day}-{month}-{year}", "%d-%m-%Y") - start_date = end_date - pd.Timedelta(days=int(days_part)) - - date_range = f"{start_date.strftime('%b %d')} - {end_date.strftime('%b %d %Y')}" - except: - date_range = "Unknown" - - # Count problems in JSON - try: - with open(file_path, 'r') as f: - data = json.load(f) - problem_count = len(data) if isinstance(data, list) else 0 - except: - problem_count = "Unknown" - - # Generate table name - table_name = filename.replace('cloudguard_problems_', '').replace('.json', '').replace('-', '_') - - return { - 'problem_count': problem_count, - 'file_size': file_size, - 'size_bytes': stat.st_size, - 'created': created, - 'date_range': date_range, - 'table_name': f"cloudguard_problems_{table_name}" - } - - def _format_file_size(self, size_bytes): - """Format file size in human readable format""" - if size_bytes == 0: - return "0 B" - size_names = ["B", "KB", "MB", "GB"] - import math - i = int(math.floor(math.log(size_bytes, 1024))) - p = math.pow(1024, i) - s = round(size_bytes / p, 1) - return f"{s} {size_names[i]}" \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/command_history.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/command_history.py deleted file mode 100644 index f3a2916ab..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/command_history.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -command_history.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import readline -import os -from typing import Optional, List -from .exceptions import ArgumentError - -class CommandHistory: - def __init__(self, history_file: str = ".sql_history"): - """Initialize command history manager""" - self.history_file = os.path.expanduser(history_file) - self.load_history() - - def load_history(self): - """Load command history from file""" - try: - readline.read_history_file(self.history_file) - except FileNotFoundError: - # Create history file if it doesn't exist - self.save_history() - - def save_history(self): - """Save command history to file""" - try: - readline.write_history_file(self.history_file) - except Exception as e: - print(f"Warning: Could not save command history: {e}") - - def add(self, command: str): - """Add a command to history""" - if command and command.strip(): # Only add non-empty commands - readline.add_history(command) - self.save_history() # Save after each command for persistence - - def get_history(self, limit: Optional[int] = None) -> List[str]: - """Get list of commands from history""" - history = [] - length = readline.get_current_history_length() - start = max(1, length - (limit or length)) - - for i in range(start, length + 1): - cmd = readline.get_history_item(i) - if cmd: # Only add non-None commands - history.append((i, cmd)) - return history - - def get_command(self, reference: str) -> str: - """ - Get a command from history using reference (e.g., !4 or !-1) - Returns the resolved command - """ - try: - # Remove the '!' from the reference - ref = reference.lstrip('!') - - # Handle negative indices - if ref.startswith('-'): - index = readline.get_current_history_length() + int(ref) - else: - index = int(ref) - - # Get the command - command = readline.get_history_item(index) - - if command is None: - raise ArgumentError(f"No command found at position {ref}") - - return command - - except ValueError: - raise ArgumentError(f"Invalid history reference: {reference}") - except Exception as e: - raise ArgumentError(f"Error accessing history: {e}") - - def show_history(self, limit: Optional[int] = None): - """Display command history""" - history = self.get_history(limit) - if not history: - print("No commands in history.") - return - - print("\nCommand History:") - for index, command in history: - print(f"{index}: {command}") \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/control_commands.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/control_commands.py deleted file mode 100644 index 809d08f81..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/control_commands.py +++ /dev/null @@ -1,213 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -control_commands.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -from .base_command import Command -from classes.file_selector import FileSelector -from classes.query_selector import QuerySelector -from classes.output_formatter import OutputFormatter -from classes.commands.filter_commands import AgeFilterCommand, CompartmentFilterCommand -import json -import pandas as pd -import os - -class SetQueriesCommand(Command): - """ - Usage: set queries [] - Launches an interactive YAML-file picker and loads the selected queries. - If the YAML file contains a snapshot_type, prompts for snapshot file selection. - """ - description = """Loads queries from a YAML file for batch execution. -Usage: set queries [directory] -- If directory is not specified, uses default query directory -- Opens an interactive file picker to select the YAML file -- If YAML contains snapshot_type, prompts to select a snapshot file -- Loads selected queries into the execution queue""" - - def execute(self, args: str): - # allow optional override of query-directory via args - directory = args or self.ctx.config_manager.get_setting("query_dir") or "query_files" - selector = FileSelector(directory) - yaml_path = selector.select_file() - if not yaml_path: - print("No YAML file selected.") - return - - qs = QuerySelector(yaml_path) - - # Check if snapshot file is needed - if qs.snapshot_type: - print(f"\nThis query file requires {qs.snapshot_type} snapshot data.") - - # Get current snapshot directory - snapshot_dir = self.ctx.query_executor.current_snapshot_dir - if not snapshot_dir: - print("Error: No active tenancy snapshot. Use 'set tenancy' first.") - return - - # Let user select snapshot file - snapshot_file = qs.select_snapshot_file(snapshot_dir) - if not snapshot_file: - print("No snapshot file selected. Query loading cancelled.") - return - - # Load the snapshot file into DuckDB - table_name = self._load_snapshot_to_duckdb(snapshot_file, qs.snapshot_type) - if table_name: - qs.set_snapshot_table(table_name) - print(f"✓ Loaded snapshot data into table: {table_name}") - else: - print("Failed to load snapshot data. Query loading cancelled.") - return - - # Select queries (with possible snapshot substitution) - qs.select_queries() - self.ctx.query_selector = qs - print(f"Loaded queries from '{yaml_path}' into queue.") - - if qs.snapshot_type: - print(f"Queries will use snapshot table: {qs.snapshot_table}") - - def _load_snapshot_to_duckdb(self, json_file, snapshot_type): - """Load JSON file into DuckDB and return the table name.""" - try: - # Generate table name based on filename - filename = os.path.basename(json_file) - if snapshot_type == "audit": - table_name = filename.replace('audit_events_', '').replace('.json', '').replace('-', '') - table_name = f"audit_events_{table_name}" - elif snapshot_type == "cloudguard": - table_name = filename.replace('cloudguard_problems_', '').replace('.json', '').replace('-', '_') - table_name = f"cloudguard_problems_{table_name}" - else: - table_name = filename.replace('.json', '').replace('-', '_') - - print(f"Loading {filename} into table {table_name}...") - - with open(json_file, 'r', encoding='utf-8') as f: - data = json.load(f) - - if not data: - print("Warning: JSON file contains no data") - return None - - # Check if table already exists - existing_tables = self.ctx.query_executor.show_tables() - if table_name in existing_tables: - print(f"Table '{table_name}' already exists, using existing table.") - return table_name - - # Flatten nested JSON - flattened = [] - for item in data: - flat_item = {} - self._flatten_dict(item, flat_item) - flattened.append(flat_item) - - df = pd.DataFrame(flattened) - - # Register and create table - self.ctx.query_executor.conn.register(table_name, df) - self.ctx.query_executor.conn.execute(f"CREATE TABLE {table_name} AS SELECT * FROM {table_name}") - print(f"Created table '{table_name}' with {len(df)} rows and {len(df.columns)} columns") - - return table_name - - except Exception as e: - print(f"Error loading snapshot into DuckDB: {e}") - return None - - def _flatten_dict(self, d, flat_dict, prefix=''): - """Recursively flatten nested dictionaries and handle lists""" - for k, v in d.items(): - key = f"{prefix}{k}" if prefix else k - key = key.replace(' ', '_').replace('-', '_').replace('.', '_') - - if isinstance(v, dict): - self._flatten_dict(v, flat_dict, f"{key}_") - elif isinstance(v, list): - flat_dict[key] = json.dumps(v) if v else None - else: - flat_dict[key] = v - -class SetTenancyCommand(Command): - """ - Usage: set tenancy - Re‑runs the tenancy‑selection & CSV loading flow, replacing the active QueryExecutor. - """ - description = """Changes the active tenancy and reloads CSV data. -Usage: set tenancy -- Prompts for tenancy selection -- Reloads CSV files for the selected tenancy -- Updates the query executor with new data""" - - def execute(self, args: str): - if not callable(self.ctx.reload_tenancy): - print("Error: tenancy reload not configured.") - return - new_executor = self.ctx.reload_tenancy() - - if new_executor: - self.ctx.query_executor = new_executor - self.ctx.last_result = None - print("Switched to new tenancy data.") - else: - print("Failed to change tenancy.") - -class RunQueriesCommand(Command): - """ - Usage: run queries - Executes all queries loaded by `set queries` in FIFO order. - """ - description = """Executes all queries that were loaded using 'set queries'. -Usage: run queries -- Executes queries in FIFO order -- Displays results after each query -- Can include both SQL queries and filter operations""" - - def execute(self, args: str): - qs = self.ctx.query_selector - if not qs or qs.query_queue.empty(): - print("No queries loaded (or queue is empty).") - return - - while True: - item = qs.dequeue_item() - if not item: - break - kind, val = item - - if kind == "Description": - print(f"\n== {val} ==") - - elif kind == "SQL": - print(f"Running SQL: {val}") - df = self.ctx.query_executor.execute_query(val) - if df is not None: - # store for potential filtering - self.ctx.last_result = df - fmt = self.ctx.config_manager.get_setting("output_format") or "dataframe" - # use the imported OutputFormatter - print(OutputFormatter.format_output(df, fmt)) - - elif kind == "Filter": - # val is something like "age api_keys 90" or "compartment tree_view" - parts = val.split() - filter_type = parts[0] # "age" or "compartment" - filter_args = " ".join(parts[1:]) # e.g. "api_keys 90" - - cmd_key = f"filter {filter_type}" - cmd_cls = self.ctx.registry.get(cmd_key) - if not cmd_cls: - print(f"Unknown filter command '{cmd_key}'") - continue - - # instantiate and run the filter command - cmd = cmd_cls(self.ctx) - cmd.execute(filter_args) diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/exceptions.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/exceptions.py deleted file mode 100644 index c197ea0f3..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/exceptions.py +++ /dev/null @@ -1,13 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -exceptions.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -class ArgumentError(Exception): - """Exception raised for errors in command arguments""" - pass \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/filter_commands.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/filter_commands.py deleted file mode 100644 index 217afe69b..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/filter_commands.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -filter_commands.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -from .base_command import Command -from classes.api_key_filter import ApiKeyFilter -from classes.compartment_structure import HCCompartmentStructure - -class AgeFilterCommand(Command): - description = """Filters results based on age in days for a specified column. -Usage: filter age - -Modes: -- older: Show only entries older than the specified days -- younger: Show only entries younger than or equal to the specified days - -The column specified by can contain dates in the following formats: -1. Direct date strings: 'YYYY-MM-DD HH:MM:SS' or 'YYYY-MM-DD HH:MM' -2. Comma-separated lists of dates -3. OCID entries with dates (separated by spaces or colons) - -Examples: -- filter age creation_date older 90 (shows entries older than 90 days) -- filter age api_keys younger 30 (shows entries 30 days old or newer) -- filter age last_modified older 60 (shows entries older than 60 days) - -The command will: -1. Parse all dates found in the specified column -2. For 'older' mode: Keep only rows where any date is older than the specified number of days -3. For 'younger' mode: Keep only rows where any date is younger than or equal to the specified number of days -4. Remove rows where no valid dates are found - -Note: -- The 'older' filter shows entries strictly older than -- The 'younger' filter shows entries equal to or newer than -- Rows where the date column is NULL/None or contains no valid dates will be excluded from the results -- If a row contains multiple dates, it will be included if ANY of its dates match the filter criteria""" - - def execute(self, args): - parts = args.split() - if len(parts) != 3: - print("Usage: filter age ") - return - - col, mode, days = parts - if mode.lower() not in ['older', 'younger']: - print("Mode must be either 'older' or 'younger'") - return - - if self.ctx.last_result is None: - print("No prior result to filter.") - return - - try: - days = int(days) - df = ApiKeyFilter(column_name=col, age_days=days, mode=mode.lower()).filter(self.ctx.last_result) - self.ctx.last_result = df - fmt = self.ctx.config_manager.get_setting("output_format") - print(__import__('classes.output_formatter').OutputFormatter.format_output(df, fmt)) - except ValueError: - print("Days must be an integer.") - -class CompartmentFilterCommand(Command): - description = """Filters and analyzes compartment structures. -Usage: filter compartment [arg] -Subcommands: -- root: Show root compartment -- depth: Show maximum depth -- tree_view: Display compartment tree -- path_to : Show path to specific compartment -- subs : Show sub-compartments -- comps_at_depth : Show compartments at specific depth""" - - def execute(self, args): - parts = args.split() - if not parts: - print("Usage: filter compartment [arg]") - return - sub = parts[0]; param = parts[1] if len(parts)>1 else None - if self.ctx.last_result is None or 'path' not in self.ctx.last_result.columns: - print("No 'path' column in last result.") - return - inst = HCCompartmentStructure(self.ctx.last_result['path'].tolist()) - method = { - 'root': inst.get_root_compartment, - 'depth': inst.get_depth, - 'tree_view': inst.get_comp_tree, - 'path_to': lambda: inst.get_path_to(param), - 'subs': lambda: inst.get_sub_compartments(param), - 'comps_at_depth': lambda: inst.get_compartments_by_depth(int(param)), - }.get(sub) - if not method: - print(f"Unknown subcommand '{sub}'.") - return - out = method() - print(out) diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/registry.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/registry.py deleted file mode 100644 index 2c125963d..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/registry.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -registry.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -class CommandRegistry: - def __init__(self): - # maps normalized command names to Command subclasses - self._commands = {} - - def register(self, name: str, command_cls): - """ - Register a Command subclass under a given name. - e.g. registry.register('show tables', ShowTablesCommand) - """ - self._commands[name.lower()] = command_cls - - def get(self, name: str): - """ - Look up a Command subclass by name; returns None if not found. - """ - return self._commands.get(name.lower()) - - def all_commands(self): - """ - Returns a sorted list of all registered command names. - """ - return sorted(self._commands.keys()) diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/standard_commands.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/standard_commands.py deleted file mode 100644 index 637e5b789..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/commands/standard_commands.py +++ /dev/null @@ -1,156 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -standard_commands.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -from .base_command import Command -from classes.output_formatter import OutputFormatter - - -class ShowTablesCommand(Command): - description = "Lists all available tables in the current database.\nUsage: show tables" - - def execute(self, args): - tables = self.ctx.query_executor.show_tables() - print("Available tables:") - for t in tables: - print(f" - {t}") - -class DescribeCommand(Command): - description = "Shows the structure of a table, including column names and types.\nUsage: describe " - - def execute(self, args): - if not args: - print("Usage: describe ") - return - info = self.ctx.query_executor.describe_table(args) - if not info: - print(f"Table '{args}' not found.") - else: - print(f"Columns in '{args}':") - for name, typ in info: - print(f" - {name}: {typ}") - -class ExecuteSqlCommand(Command): - description = "Executes a SQL query and displays the results.\nUsage: " - """Fallback for unrecognized commands: treat as SQL.""" - def execute(self, args): - sql = args.strip() - if not sql: - return - - # log directly (no print around it) - self.ctx.logger.log(f"Running SQL: {sql}", level="DEBUG") - - # execute the query - df = self.ctx.query_executor.execute_query(sql) - if df is not None: - self.ctx.last_result = df - - fmt = self.ctx.config_manager.get_setting("output_format") or "dataframe" - self.ctx.logger.log(f"Formatting output as {fmt}", level="DEBUG") - - # format and print the result - output = OutputFormatter.format_output(df, fmt) - print(output) - -class ExitCommand(Command): - description = "Exits the application.\nUsage: exit (or quit)" - - def execute(self, args): - print("Bye.") - raise SystemExit - -class HistoryCommand(Command): - description = """Shows command history. -Usage: history -- Shows all previously executed commands with their index numbers -- Use !n to re-run command number n from history -- Use !-n to re-run nth previous command""" - - def execute(self, args: str): - # Fetch the list of (index, command) tuples - history_items = self.ctx.history.get_history() - if not history_items: - print("No commands in history.") - return - - print("\nCommand History:") - for idx, cmd in history_items: - print(f"{idx}: {cmd}") -class HelpCommand(Command): - description = "Show help for available commands. Usage: help [command]" - - def execute(self, args): - if not args: - # Show all commands - print("Available commands:") - for name in self.ctx.registry.all_commands(): - cmd_cls = self.ctx.registry.get(name) - brief_desc = cmd_cls.description.split('\n')[0] # First line only - print(f" - {name:<20} - {brief_desc}") - print("\nType 'help ' for detailed help on a specific command.") - else: - # Show help for specific command - cmd_name = args.lower() - cmd_cls = self.ctx.registry.get(cmd_name) - if not cmd_cls: - print(f"Unknown command: {cmd_name}") - return - - print(f"\nHelp for '{cmd_name}':") - print(f"\n{cmd_cls.description}") - -class FilterCommand(Command): - def __init__(self, query_executor, original_result=None): - self.query_executor = query_executor - self.original_result = original_result - - def execute(self, args: str, **kwargs): - if self.original_result is None: - print("No results to filter. Run a query first.") - return - - try: - filter_parts = args.strip().lower().split() - if not filter_parts: - print("Invalid filter command. Usage: filter ") - return - - filter_type = filter_parts[0] - filter_args = filter_parts[1:] - - if filter_type == 'age': - return self._handle_age_filter(filter_args) - else: - print(f"Unknown filter type: {filter_type}") - - except Exception as e: - print(f"Error executing filter: {str(e)}") - - def _handle_age_filter(self, args): - if len(args) != 2: - print("Invalid age filter command. Usage: filter age ") - return - - column_name, age_days = args - try: - age_days = int(age_days) - from ..api_key_filter import ApiKeyFilter - - api_key_filter = ApiKeyFilter(column_name=column_name, age_days=age_days) - result = api_key_filter.filter(self.original_result.copy()) - - if result is not None and not result.empty: - print(OutputFormatter.format_output(result)) - else: - print("No records found after applying the filter.") - except ValueError: - print("Age must be a number") - except Exception as e: - print(f"Error applying age filter: {str(e)}") diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/compartment_structure.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/compartment_structure.py deleted file mode 100644 index ccdad9eb3..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/compartment_structure.py +++ /dev/null @@ -1,174 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -commpartment_structure.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -class HCCompartmentStructure: - def __init__(self, compartments): - self.compartments = [comp.strip() for comp in compartments] - - def get_root_compartment(self): - for comp in self.compartments: - if "(root)" in comp: - return comp - return None - - def get_depth(self): - root_depth = 1 - max_depth = max(len(comp.split('/')) for comp in self.compartments) - return max_depth + root_depth - - def get_sub_compartments_root(self): - return self.get_compartments_by_depth(1) - - def get_sub_compartments(self, target_compartment): - sub_compartments = set() # make unique - - for compartment in self.compartments: - if "(root)" in compartment: - continue - - parts = compartment.split(" / ") - if target_compartment in parts: - index = parts.index(target_compartment) - if index + 1 < len(parts): - sub_compartments.add(parts[index + 1]) - return list(sub_compartments) - - def get_compartments_by_depth(self, depth): - root_compartment = self.get_root_compartment() - compartments_at_depth = set() - - for compartment in self.compartments: - if root_compartment in compartment: - continue - parts = compartment.split(" / ") - - if len(parts) >= depth: - compartments_at_depth.add(parts[depth - 1]) - - return sorted(compartments_at_depth) - - def get_comp_tree(self): - tree = self.__build_tree(self.compartments) - return self.__print_tree(tree) - - def __build_tree(self, paths): - tree = {} - root = self.get_root_compartment() - tree[root] = {} - - for path in paths: - if path == root: - continue - parts = path.split('/') - current = tree[root] - for part in parts: - part = part.strip() - if part not in current: - current[part] = {} - current = current[part] - return tree - - def __print_tree(self, tree, prefix=''): - tree_str = "" - for idx, (key, value) in enumerate(sorted(tree.items())): - connector = "└── " if idx == len(tree) - 1 else "├── " - tree_str += f"{prefix}{connector}{key}\n" - if value: - extension = " " if idx == len(tree) - 1 else "│ " - tree_str += self.__print_tree(value, prefix + extension) - return tree_str - - def get_path_to(self, target_compartment): - """ - Return a list of all full paths from the root compartment - down to compartments whose name == `target_compartment`. - - Each path keeps the root compartment name, including '(root)', intact. - Example for 'acme-appdev-cmp': - ["/iosociiam (root)/acme-top-cmp/acme-appdev-cmp"] - """ - # 1) Build the tree from your existing compartments - tree = self.__build_tree(self.compartments) - - # 2) Identify the root compartment key (e.g. "/ iosociiam (root)") - root_key = self.get_root_compartment() - if root_key not in tree: - raise ValueError("Root compartment not found in the tree.") - - # Clean up leading/trailing spaces but **do not remove '(root)'**. - # For instance, if root_key is "/ (root)", - # `strip()` will remove extra leading/trailing whitespace but keep "(root)". - # If it starts with '/', we'll remove only that one slash so that - # the final path can start with a single slash. - cleaned_root = root_key.strip() - if cleaned_root.startswith("/"): - cleaned_root = cleaned_root[1:].strip() - - # Store any matching full paths in a list - results = [] - - def dfs(subtree, path_so_far): - """ - Depth-First Search through the compartment hierarchy. - subtree: the nested dictionary for the current node - path_so_far: list of compartment names from the root down to this node - """ - for child_name, child_subtree in subtree.items(): - # Clean the child but DO NOT remove '(root)' - child_clean = child_name.strip() - - new_path = path_so_far + [child_clean] - - # If this child matches target_compartment, record the full path - if child_clean == target_compartment: - # Build final path. Example: - # path_so_far = ["iosociiam (root)", "acme-top-cmp"] - # child_clean = "acme-appdev-cmp" - # => "/iosociiam (root)/acme-top-cmp/acme-appdev-cmp" - full_path = " / " + " / ".join(new_path) - results.append(full_path) - - # Recur into the child node - dfs(child_subtree, new_path) - - # 3) Start DFS from the root's subtree, using [cleaned_root] as the path - dfs(tree[root_key], [cleaned_root]) - - # 4) If no matches, raise an error - if not results: - raise ValueError(f"Compartment '{target_compartment}' not found.") - - return results - - - """ - This is to handle the different subcommands from the CLI filter compartment command. - """ - def handle_request(self, request, *args): - if request == "get_root_compartment": - return self.get_root_compartment() - elif request == "get_max_depth": - return self.get_depth() - elif request == "get_sub_compartments_root": - return self.get_sub_compartments_root() - elif request == "get_tree_view": - return self.get_comp_tree() - elif request == "get_sub_compartments": - if args: - return self.get_sub_compartments(args[0]) - else: - raise ValueError("Compartment name required for 'get_sub_compartments' request.") - elif request == "get_compartments_at_depth": - if args: - return self.get_compartments_by_depth(int(args[0])) - else: - raise ValueError("Depth value required for 'get_compartments_at_depth' request.") - else: - raise ValueError("Invalid request.") diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/config_manager.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/config_manager.py deleted file mode 100644 index 52fb8ef69..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/config_manager.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -config_manager.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import yaml -import argparse -import sys - -class ConfigManager: - def __init__(self): - - if len(sys.argv) == 1: - sys.argv.extend(['--config-file=config.yaml', '--interactive']) - - # Parse arguments - self.args = self.parse_arguments() - - # Load YAML configuration if specified - if self.args.config_file: - self.config = self.load_yaml_config(self.args.config_file) - else: - self.config = {} - - def load_yaml_config(self, config_file): - with open(config_file, 'r') as file: - return yaml.safe_load(file) - - def parse_arguments(self): - parser = argparse.ArgumentParser(description="OCI Query Tool") - parser.add_argument("--config-file", type=str, help="Path to YAML config file") - parser.add_argument("--csv-dir", type=str, help="Directory with CSV files") - parser.add_argument("--prefix", type=str, help="File prefix for filtering CSV files") - parser.add_argument("--output-format", type=str, help="Output format (DataFrame, JSON, YAML)") - parser.add_argument("--query-file", type=str, help="Path to YAML query file") - parser.add_argument("--delimiter", type=str, help="CSV delimiter") - parser.add_argument("--case-insensitive-headers", action="store_true", help="Convert headers to lowercase") - parser.add_argument("--output-dir", type=str, help="Directory to save query results") - parser.add_argument("--interactive", action="store_true", help="Enable interactive mode") - parser.add_argument("--log-level", type=str, help="Set log level") - parser.add_argument("--debug", action="store_true", help="Enable debug mode") - - parser.add_argument("--train-model", type=str, help="Path to JSON file for training the username classifier") - # New argument for testing the model - parser.add_argument("--test-model", type=str, help="Username to test with the classifier") - return parser.parse_args() - - def get_setting(self, key): - # Return CLI argument if available, otherwise fallback to config file - return getattr(self.args, key.replace('-', '_'), None) or self.config.get(key, None) diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/csv_loader_duckdb.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/csv_loader_duckdb.py deleted file mode 100644 index 80fd235ab..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/csv_loader_duckdb.py +++ /dev/null @@ -1,50 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -csv_loader_duckdb.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import duckdb -import pandas as pd -import os -import re - -class CSVLoaderDuckDB: - def __init__(self, csv_dir, prefix="csve", delimiter=',', case_insensitive_headers=False): - self.csv_dir = csv_dir - self.prefix = prefix # No auto-underscore to allow flexibility - self.delimiter = delimiter - self.case_insensitive_headers = case_insensitive_headers - self.conn = duckdb.connect(database=':memory:') # In-memory DuckDB connection - - def load_csv_files(self): - for filename in os.listdir(self.csv_dir): - if filename.endswith(".csv") and filename.startswith(self.prefix): # Ensure prefix check - # Remove only the prefix from the beginning, keeping the rest intact - table_name = filename[len(self.prefix):].removeprefix("_").removesuffix(".csv") - - # Ensure valid DuckDB table name - table_name = table_name.replace("-", "_").replace(" ", "_") - table_name = f'"{table_name}"' # Quote it to allow special characters - - print(f"Loading CSV file into DuckDB: {filename} as {table_name}") - - # Read CSV into pandas DataFrame - df = pd.read_csv(os.path.join(self.csv_dir, filename), delimiter=self.delimiter) - - # Replace dots in headers with underscores - df.columns = [re.sub(r'[.-]', '_', col) for col in df.columns] - - # Optionally convert headers to lowercase - if self.case_insensitive_headers: - df.columns = [col.lower() for col in df.columns] - - # Register DataFrame in DuckDB - self.conn.execute(f"CREATE TABLE {table_name} AS SELECT * FROM df") - - def query(self, sql): - return self.conn.execute(sql).fetchdf() # Fetch result as a pandas DataFrame diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/data_validator.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/data_validator.py deleted file mode 100644 index 39f4e47d6..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/data_validator.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -data_validator.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -class DataValidator: - @staticmethod - def validate_dataframe(df, required_columns): - missing_columns = [col for col in required_columns if col not in df.columns] - if missing_columns: - print(f"Warning: Missing columns {missing_columns} in DataFrame") diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/directory_selector.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/directory_selector.py deleted file mode 100644 index 071efa1e9..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/directory_selector.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -directory_selector.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import os -import questionary - -class DirectorySelector: - def __init__(self, parent_dir): - """ - Initialize with the parent directory which contains subdirectories. - :param parent_dir: Path to the parent directory. - """ - if not os.path.isdir(parent_dir): - raise ValueError(f"Provided path '{parent_dir}' is not a directory.") - self.parent_dir = os.path.abspath(parent_dir) - self.new_snapshot = "Create new snapshot of tenancy" - - - def list_subdirectories(self): - """ - List all subdirectories in the parent directory sorted by creation time (newest first). - :return: A list of subdirectory names. - """ - subdirs = [ - name for name in os.listdir(self.parent_dir) - if os.path.isdir(os.path.join(self.parent_dir, name)) - ] - - # Sort by creation time, newest first - subdirs.sort(key=lambda name: os.path.getctime(os.path.join(self.parent_dir, name)), reverse=True) - return subdirs - - def select_directory(self): - """ - Prompts the user to select a subdirectory using questionary. - :return: The full path to the selected subdirectory or None if no selection is made. - """ - subdirs = self.list_subdirectories() - if not subdirs: - print(f"No subdirectories found in {self.parent_dir}") - return None - - # Prompt the user to select one of the subdirectories. - subdirs.append(self.new_snapshot) - selected = questionary.select( - "Select a directory or create a new snapshot from the tenancy using showoci:", - choices=subdirs - ).ask() - - if selected is None: - # User cancelled the selection. - return None - - if selected == self.new_snapshot: - return selected - - # Return the full directory path. - return os.path.join(self.parent_dir, selected) - - def get_new_snapshot(self): - return self.new_snapshot \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/file_selector.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/file_selector.py deleted file mode 100644 index d39d206a4..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/file_selector.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -file_selector.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import os -import questionary - -class FileSelector: - def __init__(self, directory): - """Initialize FileSelector with the given directory.""" - self.directory = directory - - def get_yaml_files(self): - """Retrieve a list of YAML files from the specified directory.""" - if not os.path.isdir(self.directory): - print(f"Error: The directory '{self.directory}' does not exist.") - return [] - - # List only .yaml or .yml files - return [f for f in os.listdir(self.directory) if f.endswith((".yaml", ".yml"))] - - def select_file(self): - """Allows the user to select a YAML file interactively.""" - yaml_files = self.get_yaml_files() - - if not yaml_files: - print("No YAML files found in the directory.") - return None - - # Use questionary to allow the user to select a file - selected_file = questionary.select( - "Select a YAML file:", choices=yaml_files - ).ask() - - if selected_file: - return os.path.join(self.directory, selected_file) - return None diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/logger.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/logger.py deleted file mode 100644 index 6d7396b23..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/logger.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -logger.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import logging - -class Logger: - def __init__(self, level='INFO'): - self.logger = logging.getLogger(__name__) - self.logger.setLevel(getattr(logging, level.upper(), logging.INFO)) - handler = logging.StreamHandler() - handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) - self.logger.addHandler(handler) - - def log(self, message, level='INFO'): - if hasattr(self.logger, level.lower()): - getattr(self.logger, level.lower())(message) diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/oci_config_selector.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/oci_config_selector.py deleted file mode 100644 index ee594d621..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/oci_config_selector.py +++ /dev/null @@ -1,219 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -oci_config_selector.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import configparser -import os -import questionary -import tempfile - -class OCIConfigSelector: - def __init__(self, oci_config_file, tqt_config_file, csv_dir): - """ - Initializes the OCIConfigSelector with paths to both config files. - The paths support '~' to denote the user's home directory. - :param oci_config_file: Path to the main OCI config file (DEFAULT domain). - :param tqt_config_file: Path to the TQT config file (additional tenancies). - :param csv_dir: Base directory for CSV files. - """ - # Expand the user home directory (e.g., '~/.oci/config') - self.oci_config_file = os.path.expanduser(oci_config_file) - self.tqt_config_file = os.path.expanduser(tqt_config_file) - self.csv_dir = csv_dir - self.config = configparser.ConfigParser() - self.combined_config_content = None - self.read_and_combine_configs() - - def read_and_combine_configs(self): - """ - Reads both config files, concatenates their content, and loads the combined config. - """ - combined_content = [] - - # Read the main OCI config file (DEFAULT domain) - if os.path.exists(self.oci_config_file): - try: - with open(self.oci_config_file, 'r') as f: - oci_content = f.read().strip() - if oci_content: - combined_content.append(oci_content) - print(f"Loaded DEFAULT domain from: {self.oci_config_file}") - except Exception as e: - print(f"Warning: Could not read OCI config file {self.oci_config_file}: {e}") - else: - print(f"Warning: OCI config file not found: {self.oci_config_file}") - - # Read the TQT config file (additional tenancies) - if os.path.exists(self.tqt_config_file): - try: - with open(self.tqt_config_file, 'r') as f: - tqt_content = f.read().strip() - if tqt_content: - combined_content.append(tqt_content) - print(f"Loaded additional tenancies from: {self.tqt_config_file}") - except Exception as e: - print(f"Warning: Could not read TQT config file {self.tqt_config_file}: {e}") - else: - print(f"Warning: TQT config file not found: {self.tqt_config_file}") - - # Combine the content - if combined_content: - self.combined_config_content = '\n\n'.join(combined_content) - - # Create a temporary file to load the combined configuration - with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.config') as temp_file: - temp_file.write(self.combined_config_content) - temp_config_path = temp_file.name - - try: - # Read the combined configuration - read_files = self.config.read(temp_config_path) - if not read_files: - raise FileNotFoundError(f"Unable to read combined config from temporary file") - print(f"Successfully combined and loaded configuration from both files") - finally: - # Clean up the temporary file - try: - os.unlink(temp_config_path) - except: - pass - else: - raise FileNotFoundError("No valid configuration content found in either config file") - - def get_combined_config_content(self): - """ - Returns the combined configuration content as a string. - Useful for debugging or logging purposes. - """ - if self.combined_config_content is None: - raise ValueError("No combined configuration content available. Check if config files were read successfully.") - return self.combined_config_content - - def list_sections(self): - """ - Returns a list of sections available in the combined config. - Note: The DEFAULT section is not included in this list because configparser - treats it as a special section containing default values. - """ - return self.config.sections() - - def select_section(self): - """ - Uses questionary to prompt the user to select one of the available sections, - select the DEFAULT section, or create a new section. - Returns a tuple: (section_name, prefix) where prefix is the value of the - prefix attribute under the section if it exists, otherwise None. - """ - sections = self.list_sections() - # Add "Create New Section" as an option - choices = ["DEFAULT"] + sections + ["Create New Section"] - - answer = questionary.select( - "Select a section (use arrow keys and press ENTER):", - choices=choices, - default="DEFAULT" - ).ask() - - if answer == "Create New Section": - answer = self.create_new_section() - else: - print(f"You selected: {answer}") - - # Check for the 'prefix' attribute in the selected section. - if answer == "DEFAULT": - # For DEFAULT, check the defaults() dictionary. - prefix = self.config.defaults().get("prefix", None) - else: - prefix = self.config.get(answer, "prefix") if self.config.has_option(answer, "prefix") else None - - return answer, prefix - - def create_new_section(self): - """ - Creates a new section in the TQT config file. - Asks the user whether they have CSV files or connection details. - For CSV files: asks for the prefix and shows the path where files must be pasted. - For connection details: prompts for necessary details and adds them to the new section. - """ - section_name = questionary.text("Enter the name for the new section:").ask() - # Check if the section already exists - if section_name in self.config.sections(): - print(f"Section '{section_name}' already exists. Please choose a different name.") - return self.select_section()[0] # Re-prompt for selection and return the section name - - option = questionary.select( - "Do you have CSV files or connection details?", - choices=["CSV files", "Connection Details"] - ).ask() - - if option == "CSV files": - prefix = questionary.text("Provide the prefix for the CSV files:").ask() - # Determine the path where the CSV files should be pasted. - csv_path = os.path.join(self.csv_dir, section_name, section_name + '__') - print(f"Please paste your CSV files with prefix '{prefix}' into the following directory:\n{csv_path}") - # Optionally, create the directory if it does not exist. - if not os.path.exists(csv_path): - os.makedirs(csv_path) - print(f"Created directory: {csv_path}") - - # Save the CSV prefix in the TQT config file for future reference. - self._add_section_to_tqt_config(section_name, {"prefix": prefix}) - - # Return the new section name. - print(f"New section '{section_name}' added to TQT config file. Restart to select and load your data.") - exit() - - elif option == "Connection Details": - oci_user = questionary.text("Enter OCI user:").ask() - fingerprint = questionary.text("Enter fingerprint:").ask() - tenancy = questionary.text("Enter tenancy:").ask() - region = questionary.text("Enter region:").ask() - key_file = questionary.text("Enter key file path:").ask() - - # Create new section with connection details in TQT config. - config_data = { - "user": oci_user, - "fingerprint": fingerprint, - "tenancy": tenancy, - "region": region, - "key_file": key_file - } - self._add_section_to_tqt_config(section_name, config_data) - print(f"New section '{section_name}' added to TQT config file.") - return section_name - - else: - print("Invalid option selected.") - return self.create_new_section() # Recurse until a valid option is provided. - - def _add_section_to_tqt_config(self, section_name, config_data): - """ - Adds a new section to the TQT config file. - :param section_name: Name of the section to add - :param config_data: Dictionary of key-value pairs for the section - """ - # Create the TQT config file if it doesn't exist - os.makedirs(os.path.dirname(self.tqt_config_file), exist_ok=True) - - # Read existing TQT config if it exists - tqt_config = configparser.ConfigParser() - if os.path.exists(self.tqt_config_file): - tqt_config.read(self.tqt_config_file) - - # Add the new section - tqt_config.add_section(section_name) - for key, value in config_data.items(): - tqt_config.set(section_name, key, value) - - # Write back to the TQT config file - with open(self.tqt_config_file, "w") as configfile: - tqt_config.write(configfile) - - # Refresh the combined configuration - self.read_and_combine_configs() \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/output_formatter.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/output_formatter.py deleted file mode 100644 index 655159167..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/output_formatter.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -output_formatter.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import pandas as pd -import json - -class OutputFormatter: - @staticmethod - def format_output(data, output_format="DataFrame"): - if data is None: - return "No data to display" - - try: - if isinstance(data, pd.DataFrame): - with pd.option_context('display.max_rows', None, - 'display.max_columns', None, - 'display.width', 1000): - return str(data) - elif isinstance(data, (list, tuple)): - return "\n".join(map(str, data)) - elif isinstance(data, dict): - return "\n".join(f"{k}: {v}" for k, v in data.items()) - else: - return str(data) - except Exception as e: - return f"Error formatting output: {str(e)}" \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/query_executor_duckdb.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/query_executor_duckdb.py deleted file mode 100644 index 0a26a17d6..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/query_executor_duckdb.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -query_executor_duckdb.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import duckdb - -class QueryExecutorDuckDB: - def __init__(self, conn): - self.conn = conn # DuckDB connection - - def execute_query(self, query): - try: - result = self.conn.execute(query).fetchdf() - return result - except Exception as e: - print(f"Error executing query: {e}") - return None - - def show_tables(self): - """Use DuckDB's SHOW TABLES command to list all tables.""" - result = self.conn.execute("SHOW TABLES").fetchall() - return [row[0] for row in result] - - def describe_table(self, table_name): - """Use DuckDB's DESCRIBE command to get column names and types.""" - try: - result = self.conn.execute(f"DESCRIBE {table_name}").fetchall() - return [(row[0], row[1]) for row in result] - except Exception as e: - print(f"Error describing table '{table_name}': {e}") - return None diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/query_selector.py b/security/security-design/shared-assets/oci-security-health-check-forensics/classes/query_selector.py deleted file mode 100644 index 119bd6fc8..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/classes/query_selector.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -query_selector.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import yaml -import questionary -import queue -import os -import glob - -class QuerySelector: - def __init__(self, yaml_file=None): - """Initialize QuerySelector with an optional YAML file path and a FIFO queue.""" - self.yaml_file = yaml_file - self.query_queue = queue.Queue() # Always initialize an empty FIFO queue - self.snapshot_type = None - self.snapshot_table = None - - if yaml_file: - self.queries = self.load_queries() - else: - print("No YAML file provided. Initializing an empty queue.") - self.queries = [] # Empty query list if no file is provided - - def load_queries(self): - """Load queries from a YAML file and check for snapshot_type.""" - try: - with open(self.yaml_file, "r") as file: - data = yaml.safe_load(file) - # Check for snapshot_type parameter - self.snapshot_type = data.get("snapshot_type", None) - return data.get("queries", []) - except Exception as e: - print(f"Error loading YAML file: {e}") - return [] - - def select_snapshot_file(self, snapshot_dir): - """Select a snapshot file based on the snapshot_type.""" - if not self.snapshot_type: - return None - - # Determine file pattern based on snapshot type - if self.snapshot_type == "audit": - pattern = os.path.join(snapshot_dir, "audit_events_*_*.json") - elif self.snapshot_type == "cloudguard": - pattern = os.path.join(snapshot_dir, "cloudguard_problems_*_*.json") - else: - print(f"Unknown snapshot type: {self.snapshot_type}") - return None - - # Find matching files - files = glob.glob(pattern) - - if not files: - print(f"No {self.snapshot_type} snapshot files found in {snapshot_dir}") - return None - - # Prepare choices with metadata - file_choices = [] - for file_path in sorted(files, key=os.path.getmtime, reverse=True): - filename = os.path.basename(file_path) - stat = os.stat(file_path) - file_size = self._format_file_size(stat.st_size) - - choice_text = f"{filename} ({file_size})" - file_choices.append({ - 'name': choice_text, - 'value': file_path - }) - - # Let user select - selected = questionary.select( - f"Select a {self.snapshot_type} snapshot file for queries:", - choices=[{'name': c['name'], 'value': c['value']} for c in file_choices] - ).ask() - - return selected - - def _format_file_size(self, size_bytes): - """Format file size in human readable format.""" - if size_bytes == 0: - return "0 B" - size_names = ["B", "KB", "MB", "GB"] - import math - i = int(math.floor(math.log(size_bytes, 1024))) - p = math.pow(1024, i) - s = round(size_bytes / p, 1) - return f"{s} {size_names[i]}" - - def set_snapshot_table(self, table_name): - """Set the snapshot table name for query substitution.""" - self.snapshot_table = table_name - - def select_queries(self): - """Displays a list of query descriptions, allowing multiple selections, and pushes each item separately onto FIFO queue.""" - if not self.queries: - print("No queries available.") - return [] - - # Prepare choices: Show description only - choices = [query["description"] for query in self.queries] - - # Use questionary to allow multiple selections - selected_descriptions = questionary.checkbox( - "Select one or more queries:", choices=choices - ).ask() - - for choice in selected_descriptions: - for query in self.queries: - if query["description"] == choice: - self.query_queue.put(("Description", query["description"])) - - # Substitute snapshot_table in SQL if needed - sql = query["sql"] - if self.snapshot_table and "{snapshot_data}" in sql: - sql = sql.replace("{snapshot_data}", self.snapshot_table) - - self.query_queue.put(("SQL", sql)) - if query.get("filter") != None: - self.query_queue.put(("Filter", query.get("filter", "None"))) - break # Stop after adding matching query - - def dequeue_item(self): - """Dequeues and returns the next item from the FIFO queue.""" - if not self.query_queue.empty(): - return self.query_queue.get() - else: - print("Queue is empty.") - return None \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/config.yaml b/security/security-design/shared-assets/oci-security-health-check-forensics/config.yaml deleted file mode 100644 index 48d2085f3..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/config.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Directory to store CSV files -csv_dir: "data" - -oci_config_file: "~/.oci/config" # OCI config file location (default is ~/.oci/config) -tqt_config_file: "qt_config" # Tenancy Query Tool config file location qt_config - -# Prefix for CSV files -prefix: "oci" - -# Resource argument for showoci (a: all, i: identity, n: network, c: compute, etc.) -resource_argument: "a" - -# Output format (DataFrame, json, etc.) -output_format: "DataFrame" - -# Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) -log_level: "INFO" - -# CSV file settings -delimiter: "," -case_insensitive_headers: true - -# Interactive mode -interactive: true - -# Audit fetch settings -audit_worker_count: 10 -audit_worker_window: 1 # hours \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/healthcheck_forensic_tool.py b/security/security-design/shared-assets/oci-security-health-check-forensics/healthcheck_forensic_tool.py deleted file mode 100644 index 2ab34185b..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/healthcheck_forensic_tool.py +++ /dev/null @@ -1,374 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -healthcheck_forensic_tool.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import sys -import os -import glob -import shutil -import requests -import readline -import atexit -import datetime -import tempfile -from pathlib import Path - -from classes.config_manager import ConfigManager -from classes.logger import Logger -from classes.csv_loader_duckdb import CSVLoaderDuckDB as CSVLoader -from classes.query_executor_duckdb import QueryExecutorDuckDB as QueryExecutor -from classes.oci_config_selector import OCIConfigSelector -from classes.directory_selector import DirectorySelector -from classes.command_parser import CommandParser -from classes.commands.registry import CommandRegistry -from classes.commands.base_command import ShellContext -from classes.commands.command_history import CommandHistory -from classes.commands.cloudguard_commands import CloudGuardFetchCommand, CloudGuardDeleteCommand -import classes.commands.standard_commands as std -import classes.commands.filter_commands as filt -import classes.commands.control_commands as ctl -import classes.commands.audit_commands as audit - -# Pandas display options (if you ever import pandas here) -try: - import pandas as pd - pd.set_option("display.max_rows", None) - pd.set_option("display.max_columns", None) - pd.set_option("display.width", 1000) - pd.set_option("display.max_colwidth", None) -except ImportError: - pass - -# Global variable to store the combined config file path -_combined_config_file = None - -# ----------------------------------------------------------------------------- -def is_repo_accessible(url: str) -> bool: - try: - r = requests.get(url, timeout=5) - return r.status_code == 200 - except requests.RequestException: - return False - -def setup_showoci() -> bool: - repo_url = "https://github.com/oracle/oci-python-sdk" - repo_path = "oci-python-sdk" - showoci_dir = "showoci" - backup_zip = "showoci_zip/showoci.zip" - - # First, try to clone/update from GitHub - if is_repo_accessible(repo_url): - print("✓ Internet connection detected. Attempting to clone/update OCI SDK...") - try: - # Clone or pull - if not os.path.isdir(repo_path): - print("Cloning OCI SDK from GitHub...") - import git - git.Repo.clone_from(repo_url, repo_path) - print("✓ Successfully cloned OCI SDK repository") - else: - print("Updating existing OCI SDK repository...") - import git - repo = git.Repo(repo_path) - repo.remotes.origin.pull() - print("✓ Successfully updated OCI SDK repository") - - # Create symlink and copy files - link_target = os.path.join(repo_path, "examples", "showoci") - if not os.path.exists(showoci_dir): - os.symlink(link_target, showoci_dir) - - # Copy the .py files into the CWD - for src in glob.glob(os.path.join(showoci_dir, "*.py")): - shutil.copy(src, ".") - - print("✓ ShowOCI setup completed using GitHub repository") - return True - - except Exception as e: - print(f"⚠️ Failed to clone/update from GitHub: {e}") - print("📦 Falling back to local backup...") - # Fall through to backup method - - else: - print("❌ No internet connection detected or GitHub is not accessible") - print("📦 Using local backup archive...") - - # Fallback: Use local backup zip file - if not os.path.exists(backup_zip): - print(f"❌ Error: Backup file '{backup_zip}' not found!") - print(" Please ensure you have either:") - print(" 1. Internet connection to download from GitHub, OR") - print(" 2. The backup file 'showoci_zip/showoci.zip' in your project directory") - return False - - try: - print(f"📦 Extracting ShowOCI from backup archive: {backup_zip}") - import zipfile - - # Extract zip to current directory - with zipfile.ZipFile(backup_zip, 'r') as zip_ref: - zip_ref.extractall(".") - - print("✓ Successfully extracted ShowOCI from backup archive") - print("📋 Note: Using offline backup - some features may be outdated") - return True - - except Exception as e: - print(f"❌ Failed to extract from backup archive: {e}") - print(" Please check that 'showoci_zip/showoci.zip' is a valid archive") - return False - -def create_combined_config_file(oci_config_selector): - """ - Creates a temporary combined config file that showoci can use. - Returns the path to the temporary file. - """ - global _combined_config_file - - # Clean up any existing combined config file - cleanup_combined_config_file() - - # Create a new temporary file that won't be automatically deleted - temp_fd, temp_path = tempfile.mkstemp(suffix='.config', prefix='combined_oci_') - - try: - with os.fdopen(temp_fd, 'w') as temp_file: - temp_file.write(oci_config_selector.get_combined_config_content()) - - _combined_config_file = temp_path - print(f"Created temporary combined config file: {temp_path}") - return temp_path - except Exception as e: - # Clean up on error - try: - os.unlink(temp_path) - except: - pass - raise e - -def cleanup_combined_config_file(): - """ - Cleans up the temporary combined config file. - """ - global _combined_config_file - - if _combined_config_file and os.path.exists(_combined_config_file): - try: - os.unlink(_combined_config_file) - print(f"Cleaned up temporary config file: {_combined_config_file}") - except Exception as e: - print(f"Warning: Could not clean up temporary config file {_combined_config_file}: {e}") - finally: - _combined_config_file = None - -def call_showoci(combined_conf_file, profile, tenancy, out_dir, prefix, arg): - """ - Updated to use the combined config file instead of the original one. - """ - sys.argv = [ - "main.py", - "-cf", combined_conf_file, # Use the combined config file - "-t", tenancy, - f"-{arg}", - "-csv", os.path.join(out_dir, prefix), - "-jf", os.path.join(out_dir, "showoci.json") - ] - from showoci import execute_extract - execute_extract() - -def new_snapshot(tenancy, base, prefix, combined_conf_file, arg): - """ - Updated to use the combined config file. - """ - ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - target = os.path.join(base, f"{tenancy}_{ts}") - os.makedirs(target, exist_ok=True) - call_showoci(combined_conf_file, tenancy, tenancy, target, prefix, arg) - return target - -def set_tenancy_data(logger, cfg_mgr): - csv_dir = cfg_mgr.get_setting("csv_dir") - oci_conf = cfg_mgr.get_setting("oci_config_file") - tqt_conf = cfg_mgr.get_setting("tqt_config_file") - prefix = cfg_mgr.get_setting("prefix") - resource_arg= cfg_mgr.get_setting("resource_argument") - - print(f"\nConfig → csv_dir={csv_dir}, oci_config_file={oci_conf}, tqt_config_file={tqt_conf}, prefix={prefix}\n") - - # Create the OCI config selector with both config files - sel = OCIConfigSelector(oci_conf, tqt_conf, csv_dir) - tenancy, override_prefix = sel.select_section() - prefix = override_prefix or prefix - - # Create a temporary combined config file for showoci to use - combined_conf_file = create_combined_config_file(sel) - - tenancy_dir = os.path.join(csv_dir, tenancy) - if os.path.isdir(tenancy_dir) and os.listdir(tenancy_dir): - ds = DirectorySelector(tenancy_dir) - choice = ds.select_directory() - if choice == ds.get_new_snapshot(): - choice = new_snapshot(tenancy, tenancy_dir, prefix, combined_conf_file, resource_arg) - else: - choice = new_snapshot(tenancy, tenancy_dir, prefix, combined_conf_file, resource_arg) - - print(f"Loading CSVs from → {choice}") - loader = CSVLoader( - csv_dir=choice, - prefix=prefix, - delimiter=cfg_mgr.get_setting("delimiter"), - case_insensitive_headers=cfg_mgr.get_setting("case_insensitive_headers") or False - ) - loader.load_csv_files() - logger.log("CSV files loaded.") - - tables = loader.conn.execute("SHOW TABLES").fetchall() - tables = [t[0] for t in tables] - logger.log(f"Tables: {tables}") - - executor = QueryExecutor(loader.conn) - executor.current_snapshot_dir = choice - return executor - -def show_startup_help(): - """Display helpful information when the tool starts""" - print("=" * 80) - print("OCI QUERY TOOL - Interactive Mode") - print("=" * 80) - print("Available commands:") - print(" show tables - List all loaded CSV tables") - print(" describe
- Show table structure") - print(" SELECT * FROM
- Run SQL queries on your data") - print(" history - Show command history") - print(" help [command] - Get detailed help") - print() - print("Data Fetching Commands:") - print(" audit_events fetch DD-MM-YYYY - Fetch audit events") - print(" audit_events fetch - Load existing audit data") - print(" audit_events delete - Delete audit files") - print(" cloudguard fetch DD-MM-YYYY - Fetch Cloud Guard problems") - print(" cloudguard fetch - Load existing Cloud Guard data") - print(" cloudguard delete - Delete Cloud Guard files") - print(" Example: audit_events fetch 15-06-2025 7") - print(" (Fetches 7 days of data ending on June 15, 2025)") - print() - print("Filtering & Analysis:") - print(" filter age - Filter by date") - print(" filter compartment - Analyze compartments") - print() - print("Batch Operations:") - print(" set queries - Load queries from YAML file") - print(" run queries - Execute loaded queries") - print(" set tenancy - Switch to different tenancy") - print() - print("Type 'help ' for detailed usage or 'exit' to quit.") - print("=" * 80) - -# Register cleanup function to run at exit -def cleanup_at_exit(): - cleanup_combined_config_file() - -# ----------------------------------------------------------------------------- -def main(): - # Register cleanup function - atexit.register(cleanup_at_exit) - - try: - # 1) load config & logger - cfg = ConfigManager() - log = Logger(level=cfg.get_setting("log_level") or "INFO") - - # 2) initial setup & CLI history - setup_showoci() - cmd_history = CommandHistory(".sql_history") - - # 3) build context - executor = set_tenancy_data(log, cfg) - ctx = ShellContext( - query_executor=executor, - config_manager=cfg, - logger=log, - history=cmd_history, - query_selector=None, - reload_tenancy_fn=lambda: set_tenancy_data(log, cfg) - ) - - # 4) command registry & parser - registry = CommandRegistry() - parser = CommandParser(registry) - ctx.registry = registry - - # register commands - registry.register('show tables', std.ShowTablesCommand) - registry.register('describe', std.DescribeCommand) - registry.register('exit', std.ExitCommand) - registry.register('quit', std.ExitCommand) - registry.register('history', std.HistoryCommand) - registry.register('help', std.HelpCommand) - registry.register('filter age', filt.AgeFilterCommand) - registry.register('filter compartment', filt.CompartmentFilterCommand) - registry.register('set queries', ctl.SetQueriesCommand) - registry.register('run queries', ctl.RunQueriesCommand) - registry.register('set tenancy', ctl.SetTenancyCommand) - registry.register('audit_events fetch', audit.AuditEventsFetchCommand) - registry.register('audit_events delete', audit.AuditEventsDeleteCommand) - registry.register('cloudguard fetch', CloudGuardFetchCommand) - registry.register('cloudguard delete', CloudGuardDeleteCommand) - registry.register('', std.ExecuteSqlCommand) - - # Show startup help - show_startup_help() - - # 5) REPL - while True: - try: - user_input = input("CMD> ").strip() - if not user_input: - continue - - low = user_input.lower() - if low in ('exit','quit'): - cmd_history.save_history() - break - if low == 'history': - cmd_history.show_history() - continue - if user_input.startswith('!'): - user_input = cmd_history.get_command(user_input) - - # save it (unless it was a bang-exec) - if not user_input.startswith('!'): - cmd_history.add(user_input) - - cmd_name, args = parser.parse(user_input) - cmd_cls = registry.get(cmd_name) - if not cmd_cls: - print(f"Unknown command: {cmd_name}") - continue - - cmd = cmd_cls(ctx) - cmd.execute(args) - - except EOFError: - cmd_history.save_history() - break - except KeyboardInterrupt: - print("\nCancelled.") - except Exception as e: - log.log(f"Error: {e}", level="ERROR") - - except Exception as e: - print(f"Fatal error: {e}") - finally: - # Ensure cleanup happens - cleanup_combined_config_file() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/json_file_analyzer_showoci.py b/security/security-design/shared-assets/oci-security-health-check-forensics/json_file_analyzer_showoci.py deleted file mode 100644 index 88165a1c3..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/json_file_analyzer_showoci.py +++ /dev/null @@ -1,397 +0,0 @@ -""" -Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. -This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license. - -json_file_analyzer_showoci.py -@author base: Jacco Steur -Supports Python 3 and above - -coding: utf-8 -""" -import json -import argparse -from collections import OrderedDict, defaultdict - -def get_type_name(value): - """Returns a string representation of the value's type.""" - if isinstance(value, str): - return "string" - elif isinstance(value, bool): - return "boolean" - elif isinstance(value, int): - return "integer" - elif isinstance(value, float): - return "float" - elif value is None: - return "null" - elif isinstance(value, list): - return "array" - elif isinstance(value, dict): - return "object" - else: - return type(value).__name__ - -def discover_json_structure_recursive(data, max_depth=5, current_depth=0): - """ - Recursively discovers the structure of JSON data with depth limiting. - """ - if isinstance(data, dict): - if current_depth >= max_depth: - return f"object (max depth {max_depth} reached - {len(data)} keys)" - - structure = OrderedDict() - for key, value in data.items(): - structure[key] = discover_json_structure_recursive(value, max_depth, current_depth + 1) - return structure - elif isinstance(data, list): - if not data: - return "array (empty)" - else: - # Always try to show the structure of array elements - first_element = data[0] - - # If it's an array of simple types, handle that - if not isinstance(first_element, (dict, list)): - element_type = get_type_name(first_element) - # Check if all elements are the same simple type - if all(get_type_name(item) == element_type for item in data[:min(5, len(data))]): - return f"array of {element_type}" - else: - return "array of mixed simple types" - - # For complex types (objects/arrays), analyze structure - element_structure = discover_json_structure_recursive(first_element, max_depth, current_depth + 1) - - # Check if other elements have similar structure (sample a few) - sample_size = min(5, len(data)) - similar_structure = True - - if len(data) > 1: - for i in range(1, sample_size): - other_structure = discover_json_structure_recursive(data[i], max_depth, current_depth + 1) - if not structures_are_similar(element_structure, other_structure): - similar_structure = False - break - - if similar_structure: - return { - "_array_info": f"array of {len(data)} items", - "_element_structure": element_structure - } - else: - return { - "_array_info": f"array of {len(data)} items (mixed structures)", - "_example_element": element_structure - } - else: - return get_type_name(data) - -def structures_are_similar(struct1, struct2): - """Check if two structures are similar enough to be considered the same type.""" - if type(struct1) != type(struct2): - return False - - if isinstance(struct1, dict) and isinstance(struct2, dict): - # Consider similar if they have mostly the same keys - keys1 = set(struct1.keys()) - keys2 = set(struct2.keys()) - common_keys = keys1 & keys2 - total_keys = keys1 | keys2 - - # Similar if at least 70% of keys are common - similarity = len(common_keys) / len(total_keys) if total_keys else 1 - return similarity >= 0.7 - - return struct1 == struct2 - -def merge_structures(struct1, struct2): - """ - Merge two structure representations, handling cases where the same field - might have different types across different records of the same type. - """ - if struct1 == struct2: - return struct1 - - if isinstance(struct1, dict) and isinstance(struct2, dict): - merged = OrderedDict() - all_keys = set(struct1.keys()) | set(struct2.keys()) - - for key in all_keys: - if key in struct1 and key in struct2: - merged[key] = merge_structures(struct1[key], struct2[key]) - elif key in struct1: - merged[key] = f"{struct1[key]} (optional)" - else: - merged[key] = f"{struct2[key]} (optional)" - - return merged - else: - # If structures are different and not both dicts, show both possibilities - return f"{struct1} | {struct2}" - -def analyze_json_by_type(data, max_depth=5): - """ - Analyze JSON data grouped by 'type' field. - - Args: - data: List of dictionaries, each containing 'type' and 'data' fields - max_depth: Maximum depth for structure analysis - - Returns: - Dictionary mapping type names to their data structures - """ - if not isinstance(data, list): - raise ValueError("Expected JSON data to be a list of objects") - - type_structures = {} - type_counts = defaultdict(int) - - for item in data: - if not isinstance(item, dict): - print(f"Warning: Found non-dict item: {type(item)}") - continue - - if 'type' not in item: - print(f"Warning: Found item without 'type' field: {list(item.keys())}") - continue - - if 'data' not in item: - print(f"Warning: Found item without 'data' field for type '{item['type']}'") - continue - - item_type = item['type'] - item_data = item['data'] - type_counts[item_type] += 1 - - # Discover structure of this item's data - current_structure = discover_json_structure_recursive(item_data, max_depth) - - # If we've seen this type before, merge structures - if item_type in type_structures: - type_structures[item_type] = merge_structures( - type_structures[item_type], - current_structure - ) - else: - type_structures[item_type] = current_structure - - return type_structures, dict(type_counts) - -def print_dict_structure(struct, indent=0, max_line_length=120): - """Print dictionary structure with proper indentation and special handling for arrays.""" - spaces = " " * indent - if isinstance(struct, dict): - # Special handling for array structures - if "_array_info" in struct: - print(f"{spaces}{struct['_array_info']}") - if "_element_structure" in struct: - print(f"{spaces}Each element has structure:") - print_dict_structure(struct["_element_structure"], indent + 2, max_line_length) - elif "_example_element" in struct: - print(f"{spaces}Example element structure:") - print_dict_structure(struct["_example_element"], indent + 2, max_line_length) - return - - # Normal object structure - print(f"{spaces}{{") - for i, (key, value) in enumerate(struct.items()): - comma = "," if i < len(struct) - 1 else "" - if isinstance(value, dict): - if "_array_info" in value: - # Special compact display for arrays - print(f"{spaces} \"{key}\": {value['_array_info']}") - if "_element_structure" in value: - print(f"{spaces} Each element:") - print_dict_structure(value["_element_structure"], indent + 6, max_line_length) - elif "_example_element" in value: - print(f"{spaces} Example element:") - print_dict_structure(value["_example_element"], indent + 6, max_line_length) - if comma: - print(f"{spaces} ,") - else: - print(f"{spaces} \"{key}\": {{") - print_dict_structure(value, indent + 4, max_line_length) - print(f"{spaces} }}{comma}") - else: - # Handle simple values and other types - value_str = format_value_string(value, max_line_length - indent - len(key) - 6) - print(f"{spaces} \"{key}\": {value_str}{comma}") - print(f"{spaces}}}") - else: - formatted_value = format_value_string(struct, max_line_length - indent) - print(f"{spaces}{formatted_value}") - -def format_value_string(value, max_length=80): - """Format a value string with appropriate truncation and cleaning.""" - value_str = str(value) - - # Clean up common patterns - value_str = value_str.replace("OrderedDict(", "").replace("})", "}") - - # For array descriptions, make them more readable - if value_str.startswith("array of ") or value_str.startswith("array ("): - # Keep array descriptions intact but clean them up - if len(value_str) > max_length: - # Find a good break point - if "," in value_str and len(value_str) > max_length: - parts = value_str.split(",") - truncated = parts[0] - if len(truncated) < max_length - 10: - truncated += ", ..." - value_str = truncated - else: - value_str = value_str[:max_length-3] + "..." - else: - # For other long strings, truncate normally - if len(value_str) > max_length: - value_str = value_str[:max_length-3] + "..." - - return value_str - -def print_type_analysis(type_structures, type_counts, filter_types=None): - """Print the analysis results in a readable format.""" - print("=" * 80) - print("JSON STRUCTURE ANALYSIS BY TYPE") - print("=" * 80) - - # Filter if requested - if filter_types: - filter_set = set(filter_types) - type_structures = {k: v for k, v in type_structures.items() if k in filter_set} - type_counts = {k: v for k, v in type_counts.items() if k in filter_set} - - print(f"\nFound {len(type_structures)} different types:") - for type_name in sorted(type_counts.keys()): - print(f" - {type_name}: {type_counts[type_name]} record(s)") - - if not filter_types: - print("\n" + "=" * 80) - print("TIP: Use --type-filter to focus on specific types for detailed analysis") - print(" Example: --type-filter \"identity,showoci\"") - - print("\n" + "=" * 80) - - for type_name in sorted(type_structures.keys()): - structure = type_structures[type_name] - print(f"\nTYPE: {type_name}") - print(f"Records: {type_counts[type_name]}") - print("-" * 60) - print("Data structure:") - - # Pretty print with better formatting - if isinstance(structure, dict): - print_dict_structure(structure, indent=2) - else: - print(f" {structure}") - - # Show field count for complex structures - if isinstance(structure, dict): - print(f" → {len(structure)} top-level fields") - print() - -def show_sample_data(data, sample_type, max_items=1): - """Show sample data for a specific type.""" - print("=" * 80) - print(f"SAMPLE DATA FOR TYPE: {sample_type}") - print("=" * 80) - - count = 0 - for item in data: - if isinstance(item, dict) and item.get('type') == sample_type: - print(f"\nSample {count + 1}:") - print("-" * 40) - sample_data = json.dumps(item['data'], indent=2) - if len(sample_data) > 2000: - lines = sample_data.split('\n') - truncated = '\n'.join(lines[:50]) - print(f"{truncated}\n... (truncated - showing first 50 lines)") - else: - print(sample_data) - - count += 1 - if count >= max_items: - break - - if count == 0: - print(f"No records found for type '{sample_type}'") - -def main(): - """ - Main function to parse arguments, read JSON file, analyze by type, - and print the results. - """ - parser = argparse.ArgumentParser( - description="Analyze JSON file structure grouped by 'type' field." - ) - parser.add_argument("json_file", help="Path to the JSON file to analyze.") - parser.add_argument( - "--max-depth", - type=int, - default=4, - help="Maximum depth to analyze nested structures (default: 4)" - ) - parser.add_argument( - "--type-filter", - help="Only analyze specific type(s), comma-separated" - ) - parser.add_argument( - "--list-types", - action="store_true", - help="Just list all available types and exit" - ) - parser.add_argument( - "--sample", - help="Show sample data for a specific type" - ) - - args = parser.parse_args() - - try: - with open(args.json_file, 'r', encoding='utf-8') as f: - try: - data = json.load(f, object_pairs_hook=OrderedDict) - except json.JSONDecodeError as e: - print(f"Error: Invalid JSON file. {e}") - return - - print(f"Analyzing file: {args.json_file}") - - type_structures, type_counts = analyze_json_by_type(data, args.max_depth) - - # List types mode - if args.list_types: - print("\nAvailable types/sections:") - for type_name in sorted(type_counts.keys()): - print(f" - {type_name} ({type_counts[type_name]} records)") - return - - # Sample data mode - if args.sample: - show_sample_data(data, args.sample) - return - - # Filter by type if specified - filter_types = None - if args.type_filter: - filter_types = [t.strip() for t in args.type_filter.split(',')] - print(f"Filtering to types: {', '.join(filter_types)}") - - print_type_analysis(type_structures, type_counts, filter_types=filter_types) - - # Additional analysis info - print("=" * 80) - print("USAGE TIPS:") - print(f"- Use --list-types to see all available types") - print(f"- Use --type-filter \"type1,type2\" to focus on specific types") - print(f"- Use --sample \"type_name\" to see actual sample data") - print(f"- Use --max-depth N to control analysis depth (current: {args.max_depth})") - - except FileNotFoundError: - print(f"Error: File not found at {args.json_file}") - except ValueError as e: - print(f"Error: {e}") - except Exception as e: - print(f"An unexpected error occurred: {e}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/query_files/CIS_3.0.0_OCI_Foundations_Benchmark_Identity_And_Access_Management.yaml b/security/security-design/shared-assets/oci-security-health-check-forensics/query_files/CIS_3.0.0_OCI_Foundations_Benchmark_Identity_And_Access_Management.yaml deleted file mode 100644 index 76c93dbb8..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/query_files/CIS_3.0.0_OCI_Foundations_Benchmark_Identity_And_Access_Management.yaml +++ /dev/null @@ -1,295 +0,0 @@ -queries: - # Identity and Access Management - - description: "[CIS 3.0.0]:1.1 Ensure service level admins are created to manage resources of particular service (Manual)" - sql: > - SELECT DISTINCT - ic.name as compartment_name, - ic.path as compartment_path, - ip.statement, - ip.policy_name - FROM identity_policy ip - JOIN identity_compartments ic ON ip.compartment_id = ic.id - WHERE LOWER(ip.statement) LIKE '%allow group%' - AND LOWER(ip.statement) LIKE '%to manage all-resources%' - AND LOWER(ip.policy_name) != 'tenant admin policy' - ORDER BY ip.policy_name; - - - description: "[CIS 3.0.0]:1.2 Ensure permissions on all resources are given only to the tenancy administrator group (Automated)" - sql: > - SELECT DISTINCT - ic.name as compartment_name, - ic.path as compartment_path, - ip.statement, - ip.policy_name - FROM identity_compartments ic - JOIN identity_policy ip ON ic.id = ip.compartment_id - WHERE LOWER(ip.statement) LIKE '%allow group%' - AND LOWER(ip.statement) LIKE '%to manage all-resources in tenancy%' - AND LOWER(ip.policy_name) != 'tenant admin policy' - ORDER BY ip.policy_name; - - - description: "[CIS 3.0.0]:1.3 Ensure IAM administrators cannot update tenancy Administrators group" - sql: > - SELECT DISTINCT - ic.name as compartment_name, - ic.path as compartment_path, - ip.statement, - ip.policy_name - FROM identity_policy ip - JOIN identity_compartments ic ON ip.compartment_id = ic.id - WHERE LOWER(ip.policy_name) NOT IN ('tenant admin policy', 'psm-root-policy') - AND LOWER(ip.statement) LIKE '%allow group%' - AND LOWER(ip.statement) LIKE '%tenancy%' - AND (LOWER(ip.statement) LIKE '%to manage%' OR LOWER(ip.statement) LIKE '%to use%') - AND (LOWER(ip.statement) LIKE '%all-resources%' OR (LOWER(ip.statement) LIKE '%groups%' AND LOWER(ip.statement) LIKE '%users%')); - ORDER BY ip.policy_name; - - - description: "[CIS 3.0.0]:1.4 Ensure IAM password policy requires minimum length of 14 or greater (Automated). Ensure that 1 or more is selected for Numeric (minimum) OR Special (minimum)" - sql: > - SELECT DISTINCT - ic.name as compartment_name, - ic.path as compartment_path, - ip.domain_name, - ip.name, - ip.min_length, - ip.min_numerals, - ip.min_special_chars - FROM identity_domains_pwd_policies ip - JOIN identity_compartments ic ON ip.compartment_ocid = ic.id - WHERE ip.name = 'defaultPasswordPolicy' - AND min_length < 14 - AND (ip.min_numerals IS NULL OR ip.min_numerals < 1 OR ip.min_special_chars IS NULL OR ip.min_special_chars < 1) - ORDER BY LOWER(ip.domain_name) - - - description: "[CIS 3.0.0]:1.5 Ensure IAM password policy expires passwords within 365 days (Manual)" - sql: > - SELECT DISTINCT - ic.name as compartment_name, - ic.path as compartment_path, - ip.domain_name, - ip.name, - ip.password_expires_after - FROM identity_domains_pwd_policies ip - JOIN identity_compartments ic ON ip.compartment_ocid = ic.id - WHERE ip.name = 'defaultPasswordPolicy' - AND (ip.password_expires_after IS NULL OR ip.password_expires_after > 365) - ORDER BY LOWER(ip.domain_name) - - - description: "[CIS 3.0.0]:1.6 Ensure IAM password policy prevents password reuse (Manual)" - sql: > - SELECT DISTINCT - ic.name as compartment_name, - ic.path as compartment_path, - ip.domain_name, - ip.name, - ip.num_passwords_in_history - FROM identity_domains_pwd_policies ip - JOIN identity_compartments ic ON ip.compartment_ocid = ic.id - WHERE ip.name = 'defaultPasswordPolicy' - AND (ip.num_passwords_in_history IS NULL OR ip.num_passwords_in_history < 24) - ORDER BY LOWER(ip.domain_name) - - - description: "[CIS 3.0.0]:1.7 Ensure MFA is enabled for all users with a console password (Automated)" - sql: > - SELECT DISTINCT - domain_name, - display_name, - mfa_status, - is_federated_user, - can_use_console_password - FROM identity_domains_users - WHERE active = 'True' - AND is_federated_user IS NULL - AND mfa_status IS NULL - AND can_use_console_password = 'True' - ORDER BY LOWER(domain_name) - - - description: "[CIS 3.0.0]:1.8 Ensure user API keys rotate within 90 days (Automated)" - sql: > - SELECT DISTINCT - domain_name, - display_name, - can_use_api_keys, - api_keys - FROM identity_domains_users - WHERE can_use_api_keys = 'True' - AND api_keys IS NOT NULL - filter : "age api_keys older 90" - - - description: "[CIS 3.0.0]:1.9 Ensure user customer secret keys rotate within 90 days (Automated)" - sql: > - SELECT DISTINCT - domain_name, - display_name, - can_use_customer_secret_keys, - customer_secret_keys - FROM identity_domains_users - WHERE can_use_customer_secret_keys = 'True' - AND customer_secret_keys IS NOT NULL - filter : "age customer_secret_keys older 90" - - - description: "[CIS 3.0.0]:1.10 Ensure user auth tokens rotate within 90 days or less (Automated)" - sql: > - SELECT DISTINCT - domain_name, - display_name, - can_use_auth_tokens, - auth_tokens - FROM identity_domains_users - WHERE can_use_auth_tokens = 'True' - AND auth_tokens IS NOT NULL - filter : "age auth_tokens older 90" - - - description: "[CIS 3.0.0]:1.11 Ensure user IAM Database Passwords rotate within 90 days (Manual)" - sql: > - SELECT DISTINCT - domain_name, - display_name, - can_use_db_credentials, - db_credentials - FROM identity_domains_users - WHERE can_use_db_credentials = 'True' - AND db_credentials IS NOT NULL - filter : "age db_credentials older 90" - - - description: "[CIS 3.0.0]:1.12 Ensure API keys are not created for tenancy administrator users (Automated)" - sql: > - SELECT DISTINCT - domain_name, - display_name, - can_use_api_keys, - api_keys, - groups - FROM identity_domains_users - WHERE api_keys IS NOT NULL - AND can_use_api_keys = True - AND domain_name = 'Default' - AND groups LIKE '%Administrators%' - - - description: "[CIS 3.0.0]:1.13 Ensure all OCI IAM user accounts have a valid and current email address (Manual) ⚠️ Assuming account_recovery_required is true when email is not verified." - sql: > - SELECT DISTINCT - domain_name, - display_name, - external_id, - active, - status, - account_recovery_required - FROM identity_domains_users - WHERE account_recovery_required is true - AND active is true - AND external_id is null - - - description: "[CIS 3.0.0]:1.14 Ensure Instance Principal authentication is used for OCI instances, OCI Cloud Databases and OCI Functions to access OCI resources (Manual)" - sql: > - SELECT DISTINCT - c.path AS compartment_path, - p.policy_name, - p.statement - FROM identity_policy p - JOIN identity_compartments c ON p.compartment_id = c.id - WHERE LOWER(p.statement) LIKE '%request.principal%' - - - description: "[CIS 3.0.0]:1.15 Ensure storage service-level admins cannot delete resources they manage (Manual)" - sql: > - WITH storage_policies AS ( - SELECT DISTINCT - tenant_name, - policy_name, - statement, - LOWER(statement) as statement_lower, - CASE - WHEN LOWER(statement) LIKE '%where%' THEN - REPLACE(REPLACE(LOWER(SPLIT_PART(statement, 'WHERE', 2)), ' ', ''), '''', '') - ELSE '' - END as clean_where_clause - FROM identity_policy - WHERE LOWER(policy_name) NOT IN ('tenant admin policy', 'psm-root-policy') - AND LOWER(statement) LIKE '%allow group%' - AND LOWER(statement) LIKE '%to manage%' - AND ( - LOWER(statement) LIKE '%object-family%' OR - LOWER(statement) LIKE '%file-family%' OR - LOWER(statement) LIKE '%volume-family%' OR - LOWER(statement) LIKE '%buckets%' OR - LOWER(statement) LIKE '%objects%' OR - LOWER(statement) LIKE '%file-systems%' OR - LOWER(statement) LIKE '%volumes%' OR - LOWER(statement) LIKE '%mount-targets%' OR - LOWER(statement) LIKE '%volume-backups%' OR - LOWER(statement) LIKE '%boot-volume-backups%' - ) - ), - non_compliant_policies AS ( - SELECT * - FROM storage_policies - WHERE - -- Exclude storage admin policies (they are allowed to have = permissions) - NOT (clean_where_clause LIKE '%request.permission=bucket_delete%' OR - clean_where_clause LIKE '%request.permission=object_delete%' OR - clean_where_clause LIKE '%request.permission=file_system_delete%' OR - clean_where_clause LIKE '%request.permission=mount_target_delete%' OR - clean_where_clause LIKE '%request.permission=export_set_delete%' OR - clean_where_clause LIKE '%request.permission=volume_delete%' OR - clean_where_clause LIKE '%request.permission=volume_backup_delete%' OR - clean_where_clause LIKE '%request.permission=boot_volume_backup_delete%') - AND ( - -- No WHERE clause (unrestricted access) - (clean_where_clause = '') OR - -- WHERE clause exists but doesn't properly restrict delete permissions based on resource type - (clean_where_clause != '' AND NOT ( - -- Object-family restrictions - (statement_lower LIKE '%object-family%' AND - clean_where_clause LIKE '%request.permission!=bucket_delete%' AND - clean_where_clause LIKE '%request.permission!=object_delete%') OR - -- File-family restrictions - (statement_lower LIKE '%file-family%' AND - clean_where_clause LIKE '%request.permission!=export_set_delete%' AND - clean_where_clause LIKE '%request.permission!=mount_target_delete%' AND - clean_where_clause LIKE '%request.permission!=file_system_delete%' AND - clean_where_clause LIKE '%request.permission!=file_system_delete_snapshot%') OR - -- Volume-family restrictions - (statement_lower LIKE '%volume-family%' AND - clean_where_clause LIKE '%request.permission!=volume_backup_delete%' AND - clean_where_clause LIKE '%request.permission!=volume_delete%' AND - clean_where_clause LIKE '%request.permission!=boot_volume_backup_delete%') OR - -- Individual resource restrictions - (statement_lower LIKE '%buckets%' AND clean_where_clause LIKE '%request.permission!=bucket_delete%') OR - (statement_lower LIKE '%objects%' AND clean_where_clause LIKE '%request.permission!=object_delete%') OR - (statement_lower LIKE '%file-systems%' AND - clean_where_clause LIKE '%request.permission!=file_system_delete%' AND - clean_where_clause LIKE '%request.permission!=file_system_delete_snapshot%') OR - (statement_lower LIKE '%mount-targets%' AND clean_where_clause LIKE '%request.permission!=mount_target_delete%') OR - (statement_lower LIKE '%volumes%' AND clean_where_clause LIKE '%request.permission!=volume_delete%') OR - (statement_lower LIKE '%volume-backups%' AND clean_where_clause LIKE '%request.permission!=volume_backup_delete%') OR - (statement_lower LIKE '%boot-volume-backups%' AND clean_where_clause LIKE '%request.permission!=boot_volume_backup_delete%') - )) - ) - ) - SELECT - tenant_name, - policy_name, - statement, - FROM non_compliant_policies - ORDER BY tenant_name, policy_name - - - description: "[CIS 3.0.0]:1.16 Ensure OCI IAM credentials unused for 45 days or more are disabled (Automated)" - sql: > - SELECT DISTINCT - domain_name, - user_name, - password_last_successful_login_date - FROM identity_domains_users - filter : "age password_last_successful_login_date older 45" - - - description: "[CIS 3.0.0]:1.17 Ensure there is only one active API Key for any single OCI IAM user (Automated)" - sql: > - SELECT DISTINCT - domain_name, - display_name, - can_use_api_keys, - api_keys - FROM identity_domains_users - WHERE can_use_api_keys = 'True' - AND api_keys IS NOT NULL - AND CONTAINS(api_keys, ',') diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/query_files/FORENSIC_Audit.yaml b/security/security-design/shared-assets/oci-security-health-check-forensics/query_files/FORENSIC_Audit.yaml deleted file mode 100644 index 5da142f42..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/query_files/FORENSIC_Audit.yaml +++ /dev/null @@ -1,26 +0,0 @@ -snapshot_type: audit - -queries: - - description: "[FORENSIC]: Fetch distict set of eventtypes from the fetched audit logs window." - sql: "SELECT DISTINCT event_type, source, data_event_name, data_compartment_name, data_identity_principal_name FROM {snapshot_data}" - - - description: "[FORENSIC] Get all the event_types etc and order them by priciple_name for IdentityControlPlane" - sql: "SELECT data_identity_principal_name, data_identity_ip_address, event_type, source, data_compartment_name, data_event_name FROM {snapshot_data} where source = 'IdentityControlPlane' GROUP BY data_identity_principal_name, data_identity_ip_address, event_type, source, data_compartment_name, data_event_name ORDER BY data_identity_principal_name" - - - description: "[FORENSIC] Get all the event_types etc and order them by priciple_name for ConsoleSignIn" - sql: "SELECT data_identity_principal_name, data_identity_ip_address, event_type, source, data_compartment_name, data_event_name FROM {snapshot_data} where source = 'IdentitySignOn' GROUP BY data_identity_principal_name, data_identity_ip_address, event_type, source, data_compartment_name, data_event_name ORDER BY data_identity_principal_name" - - - description: "[FORENSIC] Find all administrative actions in the last period" - sql: "SELECT event_time, data_event_name, data_identity_principal_name, data_resource_name FROM {snapshot_data} WHERE data_event_name LIKE '%Admin%' OR data_event_name LIKE '%Create%' OR data_event_name LIKE '%Delete%' OR data_event_name LIKE '%Update%' ORDER BY event_time DESC" - - - description: "[FORENSIC] Show all unique users who performed actions" - sql: "SELECT DISTINCT data_identity_principal_name, COUNT(*) as action_count FROM {snapshot_data} GROUP BY data_identity_principal_name ORDER BY action_count DESC" - - - description: "[FORENSIC] Find all failed authentication attempts" - sql: "SELECT event_time, data_identity_principal_name, data_event_name, data_response_response_time FROM {snapshot_data} WHERE data_event_name LIKE '%Failed%' OR data_response_status != 'SUCCEEDED' ORDER BY event_time DESC" - - - description: "[FORENSIC] Show resource deletions" - sql: "SELECT event_time, data_user_name, data_resource_name, data_event_name FROM {snapshot_data} WHERE data_event_name LIKE '%Delete%' ORDER BY event_time DESC" - - - description: "[FORENSIC] Find policy changes" - sql: "SELECT event_time, data_user_name, data_resource_name, data_event_name FROM {snapshot_data} WHERE event_type = 'Policy' OR event_type LIKE '%Policy%' ORDER BY event_time DESC" \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/query_files/FORENSIC_Cloudguard.yaml b/security/security-design/shared-assets/oci-security-health-check-forensics/query_files/FORENSIC_Cloudguard.yaml deleted file mode 100644 index 45a16774c..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/query_files/FORENSIC_Cloudguard.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# queries/FORENSIC_CloudGuard.yaml -snapshot_type: cloudguard - -queries: - - description: "[FORENSIC] Get all the CG problems sorted by resource_name" - sql: > - SELECT resource_name, detector_rule_id, risk_level, labels, time_first_detected, time_last_detected, lifecycle_state, lifecycle_detail, detector_id - FROM {snapshot_data} - ORDER BY resource_name" - - - description: "[FORENSIC] Show all high-risk Cloud Guard problems" - sql: > - SELECT resource_name, detector_rule_id, risk_level, labels, time_first_detected, time_last_detected, lifecycle_state - FROM {snapshot_data} - WHERE risk_level = 'HIGH' - ORDER BY time_last_detected DESC - - - description: "[FORENSIC] Find problems by detector type" - sql: > - SELECT detector_id, COUNT(*) as problem_count - FROM {snapshot_data} - GROUP BY detector_id - ORDER BY problem_count DESC - - - description: "[FORENSIC] Show active problems (not resolved)" - sql: > - SELECT resource_name, detector_rule_id, risk_level, lifecycle_state, lifecycle_detail - FROM {snapshot_data} - WHERE lifecycle_state != 'RESOLVED' - ORDER BY time_last_detected DESC - - - description: "[FORENSIC] Find problems in specific compartments" - sql: > - SELECT ic.name as compartment_name, ic.path as compartment_path, COUNT(*) as problem_count - FROM {snapshot_data} cp - LEFT JOIN identity_compartments ic - ON cp.compartment_id = ic.id - GROUP BY cp.compartment_id, ic.name, ic.path - ORDER BY problem_count DESC - diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/requirements.txt b/security/security-design/shared-assets/oci-security-health-check-forensics/requirements.txt deleted file mode 100644 index e8e3ed687..000000000 --- a/security/security-design/shared-assets/oci-security-health-check-forensics/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -pandas -pyyaml -duckdb -oci -questionary -tqdm -gitpython -requests \ No newline at end of file diff --git a/security/security-design/shared-assets/oci-security-health-check-forensics/showoci_zip/showoci.zip b/security/security-design/shared-assets/oci-security-health-check-forensics/showoci_zip/showoci.zip deleted file mode 100644 index af15e5a18bc0aef36b329a0ab1cdfbc90483c91c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 207505 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�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%0Dw@M~lzBx3(YQ9(1n`KGsD^$Vi|%wNi_ZKE9Nj@V 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

XJ%H7E=?`mVH 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

r-ts7NjYER}|1X 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