From a80c83301ad85a296c3dda2b6afd488f3fa7905f Mon Sep 17 00:00:00 2001 From: safidnadaf Date: Tue, 12 May 2026 15:29:35 +0100 Subject: [PATCH 1/6] ci: trigger fresh CI run From 85bd7c5ae1983eb8c3ca8d69d32d40bf7c23f3a4 Mon Sep 17 00:00:00 2001 From: Vishnu Ajith <86302373+Vishnu2707@users.noreply.github.com> Date: Sat, 16 May 2026 02:18:48 +0100 Subject: [PATCH 2/6] Dev (#50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: add skeleton files and requirements * fix: remove embedded git repo * Core Structure Created * feat: build complete core — scanner engine, 10 rules, API, playbooks, compliance mappings, docs * docs: replace ASCII architecture with interactive Mermaid diagram * feat: Sentinel integration — ingest.py, 4 KQL rules, setup guide (#12) * feat: add sentinel/ingest.py — Log Analytics ingestion via HMAC-SHA256 * feat: add sentinel/__init__.py * feat: add KQL rule — HIGH severity finding detected * feat: add KQL rule — misconfiguration wave detection * feat: add KQL rule — new resource type critical detection * Delete sentinel/rules directory * Create rules * Delete sentinel/rules * Add KQL rule for high severity findings * Add Misconfiguration Wave detection rule * Add KQL rule for persistent misconfiguration detection * Add KQL rule for new critical resource types This rule identifies new resource types with critical findings that have occurred in the last 24 hours, excluding known types from the last 30 days. * Add script to generate test findings in JSON format This script generates test findings related to security compliance and saves them in a JSON file. * Add Sentinel integration test plan and results Added a comprehensive test plan for Sentinel integration, detailing test objectives, results, and acceptance criteria for various KQL rules and data ingestion. * docs: add sentinel integration setup guide Added a comprehensive setup guide for integrating Sentinel with Azure, covering prerequisites, workspace creation, activation, environment variable setup, ingestion, log verification, KQL rules deployment, and incident verification. * fix: add AZ-STOR-003 compliance mappings, correct NIST control to PR.DS-3 * docs: add real-world breach scenarios for all 10 starter rules (#15) * feat: add AZ-KV-002 key vault public access rule and remediation playbook (#14) * docs: update README with rule count, roadmap progress and contributors * feat: add network security rules AZ-NET-003 to AZ-NET-010 (#16) * Add az_net_003.py to check NSG rules for port 443 This script detects Network Security Groups (NSGs) with unrestricted inbound access on port 443 and provides remediation guidance. * Add AZ-NET-004 rule for empty NSG detection This script detects Network Security Groups (NSGs) that have no custom security rules configured, providing details for remediation. * Add AZ-NET-005 rule for DDoS protection check This script detects virtual networks in Azure that do not have DDoS protection enabled and provides remediation steps. * feat: add rule AZ-NET-006 — public IP unassociated with any resource This rule detects public IP addresses that are not associated with any resource, providing details for remediation. * feat: add rule AZ-NET-007 — Application Gateway without WAF enabled This rule detects Application Gateways that do not have WAF enabled, logging findings and providing remediation steps. * feat: add rule AZ-NET-008 — load balancer with no backend pool This rule detects load balancers in Azure that are not configured with a backend pool, indicating potential misconfiguration or unnecessary costs. * feat: add rule AZ-NET-009 — VPN gateway using outdated IKE version This script detects VPN gateways using the outdated IKEv1 protocol and provides remediation steps to migrate to IKEv2. * feat: add rule AZ-NET-010 — subnet with no NSG attached This script detects subnets in Azure that do not have a Network Security Group (NSG) attached, logging findings and providing remediation guidance. * feat: add playbook fix_az_net_003.sh This script updates the NSG rule to restrict inbound traffic on port 443 to a specified IP range. * feat: add playbook fix_az_net_004.sh This script adds a default deny-all inbound rule to a specified NSG. * feat: add playbook fix_az_net_005.sh This script enables DDoS protection on a specified virtual network in Azure. It checks for required parameters and provides usage instructions if they are missing. * feat: add playbook fix_az_net_006.sh This script deletes unassociated public IP addresses in Azure. * feat: add playbook fix_az_net_007.sh This script enables WAF on an Application Gateway, ensuring compliance with the AZ-NET-007 rule. * feat: add playbook fix_az_net_008.sh Script to remediate AZ-NET-008 by deleting empty load balancers. * feat:add script to update VPN connection to IKEv2 This script updates a VPN connection to use IKEv2, ensuring compliance with the AZ-NET-009 rule. * feat: add playbook fix_az_net_010.sh This script attaches a specified network security group to a given subnet in a virtual network, ensuring compliance with the AZ-NET-010 rule. * Clarify description and add note for public-facing services Updated the description to clarify the risk of exposing port 443 and added a note regarding public-facing services. * Change severity level from MEDIUM to HIGH * fix: AZ-NET-005 severity changed to LOW — DDoS Standard high cost on small subscriptions * Add note about NetworkManagementClient usage Added a note regarding the creation of NetworkManagementClient directly and suggested a follow-up for consistency. * Add note about NetworkManagementClient usage Added a note regarding the use of NetworkManagementClient and suggested a follow-up for consistency. * Add additional security controls to CIS Azure benchmark * Refine control descriptions in nist_csf.json Updated descriptions for various controls to enhance clarity and specificity regarding remote access management, data protection, and security measures. * fix: add AZ-NET-003 to AZ-NET-010 to ISO27001 compliance framework Updated descriptions for various controls to clarify compliance requirements and improve security guidance. --------- Co-authored-by: Vishnu Ajith <86302373+Vishnu2707@users.noreply.github.com> * Feat/az stor 003 (#21) * feat: add rule AZ-STOR-003 storage lifecycle policy check * feat: add rule AZ-STOR-003 storage lifecycle policy check * docs: add SOC 2 Type II compliance framework mapping (#33) * docs: add SOC 2 Type II compliance framework mapping for all 20 rules Added SOC 2 Type II framework with detailed controls for security measures and compliance requirements. * feat: add soc2 to FRAMEWORK_FILE_MAP in finding.py add soc2.json to FRAMEWORK_FILE_MAP in finding.py * feat: add soc2 to SUPPORTED_FRAMEWORKS in compliance.py Added 'soc2' to the list of supported compliance frameworks. * Add SOC 2 controls for data protection and management * Refactor/azure client network methods (#22) * refactor: add get_virtual_networks() and get_public_ip_addresses() to AzureClient * Refactor DDoS protection check to use azure_client * refactor: AZ-NET-006 now uses azure_client.get_public_ip_addresses() * feat: add CI pipeline with 6 automated checks (#34) - Python syntax check on all rule files - Rule structure validation (RULE_ID, SEVERITY, FRAMEWORKS) + RULE_ID uniqueness - Hardcoded credential scan - Playbook existence + bash syntax check for every rule - Compliance JSON validation for all four framework files (inc. soc2.json) - API syntax check - Compliance vs rule cross-reference check - CI summary step with per-check pass/fail table (if: always) - Fix duplicate DESCRIPTION assignment in az_net_003.py - Add pyyaml to requirements.txt for local YAML validation - Add docs/ci-pipeline.md with local run commands and design rationale - Update CI_PIPELINE_GUIDE.md with final PR description Closes #30 * docs: update .github/ISSUE_TEMPLATE/new_rule.md to reflect current codebase state * docs: update .github/PULL_REQUEST_TEMPLATE.md to reflect current codebase state * docs: update CONTRIBUTING.md to reflect current codebase state * docs: update README.md to reflect current codebase state * docs: update compliance/frameworks/iso27001.json to reflect current codebase state * docs: update compliance/frameworks/nist_csf.json to reflect current codebase state * docs: update docs/adding-a-rule.md to reflect current codebase state * docs: update docs/architecture.md to reflect current codebase state * docs: update docs/az-stor-003-test-plan.md to reflect current codebase state * docs: update docs/azure-setup.md to reflect current codebase state * docs: update docs/ci-pipeline.md to reflect current codebase state * docs: update docs/sentinel-setup.md to reflect current codebase state * docs: update sentinel/TEST_PLAN.md to reflect current codebase state * docs: update docs/api-reference.md to reflect current codebase state * docs: update docs/rules-reference.md to reflect current codebase state * docs: update README.md for professional open source style * docs: update CONTRIBUTING.md for professional open source style * docs: update docs/adding-a-rule.md for professional open source style * docs: update deployment guide to use Render instead of Azure App Service * feat: add rule AZ-STOR-004 storage account diagnostic logging check (#39) * feat: add rule AZ-STOR-004 storage account diagnostic logging check Detects Azure storage accounts where diagnostic logging is not fully enabled on blob, queue, or table services. Emits one finding per non-compliant service (StorageRead, StorageWrite, StorageDelete must all be enabled). Adds get_storage_service_logging() to AzureClient using MonitorManagementClient. Includes remediation playbook that enables all three services in one run. Frameworks: CIS 3.3, NIST DE.CM-7, ISO 27001 A.12.4.1 * chore: add AZ-STOR-004 compliance mappings --------- Co-authored-by: Shaurya K Sharma * feat: add rule AZ-IDN-003 Adds scanner rule AZ-IDN-003 detecting Entra ID (#48) * feat: add scanner rule AZ-IDN-003 — guest user invitations not restricted to admins * feat: add remediation playbook fix_az_idn_003.sh This script restricts guest user invitations to only admins and users with the Guest Inviter role in Azure Active Directory. * feat: add AZ-IDN-003 to CIS compliance framework Added control for guest invite restrictions to enhance security. * feat: add AZ-IDN-003 to NIST compliance framework * feat: add AZ-IDN-003 to ISO27001 compliance framework Added control AZ-IDN-003 for user registration and de-registration process. * feat: add AZ-IDN-003 to SOC2 compliance framework * feat: add rule AZ-CMP-002 — VM disk not protected by CMK or ADE (#47) * feat: add scanner rule AZ-CMP-002 — VM disk not protected by CMK or ADE This script detects virtual machines whose disks use platform-managed encryption only and provides findings for compliance with CIS 7.2. * feat: add remediation playbook fix_az_cmp_002.sh This script enables Azure Disk Encryption on a specified virtual machine using a Key Vault for the disk encryption key. * feat: add AZ-CMP-002 to CIS compliance framework Added a new control for OS disk encryption requirements. * feat: add AZ-CMP-002 to NIST compliance framework * feat: add AZ-CMP-002 to ISO27001 compliance framework Added control AZ-CMP-002 regarding cryptographic controls policy and its requirements. * feat: add AZ-CMP-002 to SOC2 compliance framework * fix: correct indentation in CIS AZ-CMP-002 entry * feat: add remediation playbook fix_az_cmp_002.sh to correct location This script enables Azure Disk Encryption on a specified virtual machine using a provided Key Vault for disk encryption. * Delete fix_az_cmp_002.sh * Feat/api deployment (#46) * feat: deploy API to Render with security hardening and CI/CD optimizations * feat: finalize Render deployment with security hardening and Gunicorn import fix * fix: GitHub Actions syntax and secret detection logic in deploy workflow * ix: harden scan trigger route with detailed error handling and remove redundant DB initialization * fix: implement global database connection management and harden all API routes * ix: prevent insecure smoke tests on main branch by enforcing JWT_SECRET presence and prevent CI false negatives in playbook check by enforcing non-empty glob match * fix: resolve Render startup crash and harden scan serialization against recursive objects * fix: add missing six and cryptography dependencies for Azure SDK compatibility * fix: increase CI wait time for Render build and add missing msrest dependencies * feat: integrate real subscription ID into smoke tests and CI/CD pipeline * feat: integrate real Azure_ ID's into smoke tests and CI/CD pipeline * feat: add root welcome route to confirm API status * fix: resolve specific CI credential flags in code and workflow while maintaining documentation standards * fix: resolve IndentationError in CI compliance cross-reference check * fix: resolve dependency issue and test on deployment * fix: resolve somke test TC-21 * fix: RUN_REAL_SCAN not set → TC-13/TC-14 skip → 21/21 pass for new live API url test * fix: scan.py deferred import from scanner.engine import ScanEngine was running before the subscription_id check * fix: restrict deploy triggers to dev and main, enable RUN_REAL_SCAN for maintainer CI, and update test plan documentation * feat: AZ-NET-011 Network Watcher not enabled in all regions (#42) * feat: add AZ-NET-011 Network Watcher rule, playbook and compliance mappings * fix: add missing AzureClient methods, SOC2 mapping and fix playbook region * fix: add SOC2 CC7.2 to FRAMEWORKS in az_net_011.py * feat: add AZ-DB-003 PostgreSQL Flexible Server SSL enforcement rule a… (#49) * feat: add AZ-DB-003 PostgreSQL Flexible Server SSL enforcement rule and playbook * fix: correct requirements.txt formatting for postgresqlflexibleserver * fix: correct postgresqlflexibleservers package name and version * fix: handle empty params gracefully and clean up playbook output --------- Co-authored-by: Tanvir Farhad Co-authored-by: PARTH J ROHIT Co-authored-by: Ritik Sah Co-authored-by: Shaurya K Sharma Co-authored-by: Shaurya K Sharma Co-authored-by: Mahfuzur Rahman Emon --- api/app.py | 6 +++--- api/routes/compliance.py | 15 +++++---------- api/routes/scans.py | 34 ++++++++++++---------------------- api/routes/score.py | 17 +++++++---------- 4 files changed, 27 insertions(+), 45 deletions(-) diff --git a/api/app.py b/api/app.py index 5969090..90c3164 100644 --- a/api/app.py +++ b/api/app.py @@ -63,9 +63,9 @@ def create_app() -> Flask: # ------------------------------------------------------------------ # @app.teardown_appcontext - def close_db(error=None): + def close_db(error): """Ensure the database connection is closed after the request.""" - db = g.pop("db", None) + db = g.pop("db_conn", None) if db is not None: try: if hasattr(db, "conn") and db.conn is not None: @@ -173,4 +173,4 @@ def internal_error(exc): host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=os.environ.get("FLASK_DEBUG", "false").lower() == "true", - ) \ No newline at end of file + ) diff --git a/api/routes/compliance.py b/api/routes/compliance.py index 6716453..798f187 100644 --- a/api/routes/compliance.py +++ b/api/routes/compliance.py @@ -13,13 +13,10 @@ def _get_db() -> DatabaseManager: - if "db" not in g: - db_url = os.environ.get("DATABASE_URL") - if not db_url: - raise RuntimeError("DATABASE_URL environment variable is not set") - g.db = DatabaseManager(db_url) - g.db.connect() - return g.db + if "db_conn" not in g: + g.db_conn = DatabaseManager(os.environ["DATABASE_URL"]) + g.db_conn.connect() + return g.db_conn @compliance_bp.get("/api/compliance/") @@ -44,8 +41,6 @@ def get_compliance(framework: str): return jsonify(result), 500 return jsonify(result) - except FileNotFoundError as exc: - return jsonify({"error": f"Frameworks directory not found: {exc}"}), 500 except Exception as exc: logger.error("Failed to retrieve compliance score for %s: %s", framework, exc) - return jsonify({"error": "Compliance calculation failed", "detail": str(exc)}), 500 \ No newline at end of file + return jsonify({"error": "Compliance calculation failed", "detail": str(exc)}), 500 diff --git a/api/routes/scans.py b/api/routes/scans.py index 9a13009..5aca891 100644 --- a/api/routes/scans.py +++ b/api/routes/scans.py @@ -11,13 +11,10 @@ def _get_db() -> DatabaseManager: - if "db" not in g: - db_url = os.environ.get("DATABASE_URL") - if not db_url: - raise RuntimeError("DATABASE_URL environment variable is not set") - g.db = DatabaseManager(db_url) - g.db.connect() - return g.db + if "db_conn" not in g: + g.db_conn = DatabaseManager(os.environ["DATABASE_URL"]) + g.db_conn.connect() + return g.db_conn @scans_bp.get("/api/scans") @@ -25,8 +22,8 @@ def list_scans(): """Return all historical scan results ordered by most recent first.""" try: db = _get_db() - result = db.get_scans() - return jsonify(result) + scans = db.get_scans() + return jsonify({"count": len(scans), "scans": scans}) except Exception as exc: logger.error("Failed to list scans: %s", exc) return jsonify({"error": "Failed to retrieve scans", "detail": str(exc)}), 500 @@ -42,20 +39,15 @@ def trigger_scan(): Note: For production use, replace this with an async task queue (e.g. Celery or Azure Functions) to avoid request timeouts on large subscriptions. """ - try: - from scanner.engine import ScanEngine - except ImportError: - return jsonify({"error": "Scanner module is not available"}), 500 - try: body = request.get_json(silent=True) or {} - subscription_id = body.get("subscription_id") or os.environ.get( - "AZURE_SUBSCRIPTION_ID" - ) + subscription_id = body.get("subscription_id") if not subscription_id: return jsonify({"error": "subscription_id is required"}), 400 + from scanner.engine import ScanEngine # deferred — import only after input is validated + logger.info("Scan triggered for subscription %s", subscription_id) try: @@ -65,18 +57,16 @@ def trigger_scan(): logger.error("Scan engine execution failed: %s", exc, exc_info=True) return jsonify({"error": "Scan failed", "detail": str(exc)}), 500 - if not isinstance(result, dict) or "scan_id" not in result: - return jsonify({"error": "Invalid scan result returned"}), 500 - try: db = _get_db() + # Note: Table creation is handled at startup; no need to repeat it here. db.save_scan(result) except Exception as exc: - logger.error("Failed to save scan result: %s", exc, exc_info=True) + logger.error("Failed to save scan result to database: %s", exc, exc_info=True) return jsonify({"error": "Database save failed", "detail": str(exc)}), 500 return jsonify(result), 201 except Exception as exc: logger.error("Critical error in trigger_scan route: %s", exc, exc_info=True) - return jsonify({"error": "Critical route failure", "detail": str(exc)}), 500 \ No newline at end of file + return jsonify({"error": "Critical route failure", "detail": str(exc)}), 500 diff --git a/api/routes/score.py b/api/routes/score.py index 190a3ee..bfff526 100644 --- a/api/routes/score.py +++ b/api/routes/score.py @@ -11,13 +11,10 @@ def _get_db() -> DatabaseManager: - if "db" not in g: - db_url = os.environ.get("DATABASE_URL") - if not db_url: - raise RuntimeError("DATABASE_URL environment variable is not set") - g.db = DatabaseManager(db_url) - g.db.connect() - return g.db + if "db_conn" not in g: + g.db_conn = DatabaseManager(os.environ["DATABASE_URL"]) + g.db_conn.connect() + return g.db_conn @score_bp.get("/api/score") @@ -30,8 +27,8 @@ def get_score(): """ try: db = _get_db() - result = db.get_score() - return jsonify(result) + score = db.get_score() + return jsonify({"score": score, "max_score": 100}) except Exception as exc: logger.error("Failed to calculate score: %s", exc) - return jsonify({"error": "Failed to calculate score", "detail": str(exc)}), 500 \ No newline at end of file + return jsonify({"error": "Failed to calculate score", "detail": str(exc)}), 500 From a2564ad164c05213b7b13b925f3fdad4ba2e6e1b Mon Sep 17 00:00:00 2001 From: Safid Nadaf <137755124+safidnadaf@users.noreply.github.com> Date: Sun, 24 May 2026 01:51:26 +0100 Subject: [PATCH 3/6] refactor: reuse database connection per request using Flask g (#41) * fix: improve scan routes error handling and database reuse * fix: add database connection reuse and DATABASE_URL validation to score.py * fix: add database connection reuse, DATABASE_URL validation, and FileNotFoundError handling to compliance.py * fix: enforce JWT_SECRET environment variable, remove hardcoded default * ci: trigger fresh CI run * fix: all requirements - g.db naming, teardown, close() method --- api/app.py | 6 +++--- api/routes/compliance.py | 15 ++++++++++----- api/routes/scans.py | 34 ++++++++++++++++++++++------------ api/routes/score.py | 17 ++++++++++------- 4 files changed, 45 insertions(+), 27 deletions(-) diff --git a/api/app.py b/api/app.py index 90c3164..5969090 100644 --- a/api/app.py +++ b/api/app.py @@ -63,9 +63,9 @@ def create_app() -> Flask: # ------------------------------------------------------------------ # @app.teardown_appcontext - def close_db(error): + def close_db(error=None): """Ensure the database connection is closed after the request.""" - db = g.pop("db_conn", None) + db = g.pop("db", None) if db is not None: try: if hasattr(db, "conn") and db.conn is not None: @@ -173,4 +173,4 @@ def internal_error(exc): host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=os.environ.get("FLASK_DEBUG", "false").lower() == "true", - ) + ) \ No newline at end of file diff --git a/api/routes/compliance.py b/api/routes/compliance.py index 798f187..6716453 100644 --- a/api/routes/compliance.py +++ b/api/routes/compliance.py @@ -13,10 +13,13 @@ def _get_db() -> DatabaseManager: - if "db_conn" not in g: - g.db_conn = DatabaseManager(os.environ["DATABASE_URL"]) - g.db_conn.connect() - return g.db_conn + if "db" not in g: + db_url = os.environ.get("DATABASE_URL") + if not db_url: + raise RuntimeError("DATABASE_URL environment variable is not set") + g.db = DatabaseManager(db_url) + g.db.connect() + return g.db @compliance_bp.get("/api/compliance/") @@ -41,6 +44,8 @@ def get_compliance(framework: str): return jsonify(result), 500 return jsonify(result) + except FileNotFoundError as exc: + return jsonify({"error": f"Frameworks directory not found: {exc}"}), 500 except Exception as exc: logger.error("Failed to retrieve compliance score for %s: %s", framework, exc) - return jsonify({"error": "Compliance calculation failed", "detail": str(exc)}), 500 + return jsonify({"error": "Compliance calculation failed", "detail": str(exc)}), 500 \ No newline at end of file diff --git a/api/routes/scans.py b/api/routes/scans.py index 5aca891..9a13009 100644 --- a/api/routes/scans.py +++ b/api/routes/scans.py @@ -11,10 +11,13 @@ def _get_db() -> DatabaseManager: - if "db_conn" not in g: - g.db_conn = DatabaseManager(os.environ["DATABASE_URL"]) - g.db_conn.connect() - return g.db_conn + if "db" not in g: + db_url = os.environ.get("DATABASE_URL") + if not db_url: + raise RuntimeError("DATABASE_URL environment variable is not set") + g.db = DatabaseManager(db_url) + g.db.connect() + return g.db @scans_bp.get("/api/scans") @@ -22,8 +25,8 @@ def list_scans(): """Return all historical scan results ordered by most recent first.""" try: db = _get_db() - scans = db.get_scans() - return jsonify({"count": len(scans), "scans": scans}) + result = db.get_scans() + return jsonify(result) except Exception as exc: logger.error("Failed to list scans: %s", exc) return jsonify({"error": "Failed to retrieve scans", "detail": str(exc)}), 500 @@ -39,15 +42,20 @@ def trigger_scan(): Note: For production use, replace this with an async task queue (e.g. Celery or Azure Functions) to avoid request timeouts on large subscriptions. """ + try: + from scanner.engine import ScanEngine + except ImportError: + return jsonify({"error": "Scanner module is not available"}), 500 + try: body = request.get_json(silent=True) or {} - subscription_id = body.get("subscription_id") + subscription_id = body.get("subscription_id") or os.environ.get( + "AZURE_SUBSCRIPTION_ID" + ) if not subscription_id: return jsonify({"error": "subscription_id is required"}), 400 - from scanner.engine import ScanEngine # deferred — import only after input is validated - logger.info("Scan triggered for subscription %s", subscription_id) try: @@ -57,16 +65,18 @@ def trigger_scan(): logger.error("Scan engine execution failed: %s", exc, exc_info=True) return jsonify({"error": "Scan failed", "detail": str(exc)}), 500 + if not isinstance(result, dict) or "scan_id" not in result: + return jsonify({"error": "Invalid scan result returned"}), 500 + try: db = _get_db() - # Note: Table creation is handled at startup; no need to repeat it here. db.save_scan(result) except Exception as exc: - logger.error("Failed to save scan result to database: %s", exc, exc_info=True) + logger.error("Failed to save scan result: %s", exc, exc_info=True) return jsonify({"error": "Database save failed", "detail": str(exc)}), 500 return jsonify(result), 201 except Exception as exc: logger.error("Critical error in trigger_scan route: %s", exc, exc_info=True) - return jsonify({"error": "Critical route failure", "detail": str(exc)}), 500 + return jsonify({"error": "Critical route failure", "detail": str(exc)}), 500 \ No newline at end of file diff --git a/api/routes/score.py b/api/routes/score.py index bfff526..190a3ee 100644 --- a/api/routes/score.py +++ b/api/routes/score.py @@ -11,10 +11,13 @@ def _get_db() -> DatabaseManager: - if "db_conn" not in g: - g.db_conn = DatabaseManager(os.environ["DATABASE_URL"]) - g.db_conn.connect() - return g.db_conn + if "db" not in g: + db_url = os.environ.get("DATABASE_URL") + if not db_url: + raise RuntimeError("DATABASE_URL environment variable is not set") + g.db = DatabaseManager(db_url) + g.db.connect() + return g.db @score_bp.get("/api/score") @@ -27,8 +30,8 @@ def get_score(): """ try: db = _get_db() - score = db.get_score() - return jsonify({"score": score, "max_score": 100}) + result = db.get_score() + return jsonify(result) except Exception as exc: logger.error("Failed to calculate score: %s", exc) - return jsonify({"error": "Failed to calculate score", "detail": str(exc)}), 500 + return jsonify({"error": "Failed to calculate score", "detail": str(exc)}), 500 \ No newline at end of file From 8e0d1cd98a742e892ec645c477af67d60dd41c7f Mon Sep 17 00:00:00 2001 From: safidnadaf Date: Sat, 30 May 2026 16:23:59 +0100 Subject: [PATCH 4/6] fix: update az_net_012.py to match az_cmp_003 pattern and add set -euo pipefail to bash script --- playbooks/cli/fix_az_net_012.sh | 32 ++++++++++++++ scanner/rules/az_net_012.py | 75 +++++++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 playbooks/cli/fix_az_net_012.sh create mode 100644 scanner/rules/az_net_012.py diff --git a/playbooks/cli/fix_az_net_012.sh b/playbooks/cli/fix_az_net_012.sh new file mode 100644 index 0000000..22e1aba --- /dev/null +++ b/playbooks/cli/fix_az_net_012.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -euo pipefail + +# Fix AZ-NET-012: Enable NSG Flow Logs +# Usage: ./fix_az_net_012.sh + +RESOURCE_GROUP=$1 +NSG_NAME=$2 +STORAGE_ACCOUNT_ID=$3 + +if [ -z "$RESOURCE_GROUP" ] || [ -z "$NSG_NAME" ] || [ -z "$STORAGE_ACCOUNT_ID" ]; then + echo "ERROR: Missing required arguments" + echo "Usage: $0 " + echo "Example: $0 my-rg my-nsg /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Storage/storageAccounts/mystorage" + exit 1 +fi + +echo "Enabling flow logs for NSG: $NSG_NAME" + +az network watcher flow-log create \ + --nsg "$NSG_NAME" \ + --enabled true \ + --storage-account "$STORAGE_ACCOUNT_ID" \ + --resource-group "$RESOURCE_GROUP" \ + --name "${NSG_NAME}-flowlogs" + +if [ $? -eq 0 ]; then + echo "SUCCESS: Flow logs enabled successfully for $NSG_NAME" +else + echo "FAILED: Failed to enable flow logs for $NSG_NAME" + exit 1 +fi \ No newline at end of file diff --git a/scanner/rules/az_net_012.py b/scanner/rules/az_net_012.py new file mode 100644 index 0000000..4e7b484 --- /dev/null +++ b/scanner/rules/az_net_012.py @@ -0,0 +1,75 @@ +"""AZ-NET-012: NSG flow logs not enabled.""" + +import logging +from typing import Any, Dict, List + +RULE_ID = "AZ-NET-012" +RULE_NAME = "NSG Flow Logs Not Enabled" +SEVERITY = "MEDIUM" +CATEGORY = "Network" +FRAMEWORKS = { + "CIS": "6.5", + "NIST": "DE.CM-1", + "ISO27001": "A.12.4.1", + "SOC2": "CC7.2", +} +DESCRIPTION = ( + "Network Security Group flow logs are not enabled. " + "Without flow logs, network traffic is not auditable and " + "attacker movement cannot be reconstructed." +) +REMEDIATION = ( + "Enable NSG flow logs to a storage account using Network Watcher. " + "Run: az network watcher flow-log create --nsg --enabled true " + "--storage-account --resource-group " +) +PLAYBOOK = "playbooks/cli/fix_az_net_012.sh" + +logger = logging.getLogger(__name__) + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + findings: List[Dict[str, Any]] = [] + + nsgs = azure_client.network_security_groups.list_all() + + for nsg in nsgs: + nsg_id = getattr(nsg, "id", "") + parsed = azure_client.parse_resource_id(nsg_id) + resource_group = parsed.get("resource_group", "") + nsg_name = parsed.get("name", "") + + if not resource_group or not nsg_name: + continue + + flow_log_enabled = False + + try: + flow_logs = azure_client.flow_logs.list(resource_group) + for flow_log in flow_logs: + if getattr(flow_log, "target_resource_id", "") == nsg_id and getattr(flow_log, "enabled", False): + flow_log_enabled = True + break + except Exception: + flow_log_enabled = False + + if not flow_log_enabled: + findings.append({ + "rule_id": RULE_ID, + "rule_name": RULE_NAME, + "severity": SEVERITY, + "category": CATEGORY, + "resource_id": nsg_id, + "resource_name": nsg_name, + "resource_type": "Microsoft.Network/networkSecurityGroups", + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "metadata": { + "resource_group": resource_group, + "flow_logs_enabled": False, + }, + }) + + return findings \ No newline at end of file From 1b6ee96653a278f25089f322867ffd3c00413d7a Mon Sep 17 00:00:00 2001 From: safidnadaf Date: Sat, 30 May 2026 16:37:36 +0100 Subject: [PATCH 5/6] fix: correct az_net_012.py signature, add set -euo pipefail, add AZ-NET-012 to all compliance files --- scanner/rules/az_net_012.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/scanner/rules/az_net_012.py b/scanner/rules/az_net_012.py index 4e7b484..bc344a7 100644 --- a/scanner/rules/az_net_012.py +++ b/scanner/rules/az_net_012.py @@ -1,18 +1,14 @@ """AZ-NET-012: NSG flow logs not enabled.""" import logging +from datetime import datetime, timezone from typing import Any, Dict, List +# Required rule constants RULE_ID = "AZ-NET-012" RULE_NAME = "NSG Flow Logs Not Enabled" SEVERITY = "MEDIUM" CATEGORY = "Network" -FRAMEWORKS = { - "CIS": "6.5", - "NIST": "DE.CM-1", - "ISO27001": "A.12.4.1", - "SOC2": "CC7.2", -} DESCRIPTION = ( "Network Security Group flow logs are not enabled. " "Without flow logs, network traffic is not auditable and " @@ -23,12 +19,19 @@ "Run: az network watcher flow-log create --nsg --enabled true " "--storage-account --resource-group " ) -PLAYBOOK = "playbooks/cli/fix_az_net_012.sh" +PLAYBOOK = "fix_az_net_012.sh" +FRAMEWORKS = { + "cis": "6.5", + "nist": "DE.CM-1", + "iso27001": "A.12.4.1", + "soc2": "CC7.2", +} logger = logging.getLogger(__name__) def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Scan all NSGs and check if flow logs are enabled via Network Watcher.""" findings: List[Dict[str, Any]] = [] nsgs = azure_client.network_security_groups.list_all() @@ -66,6 +69,7 @@ def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: "remediation": REMEDIATION, "playbook": PLAYBOOK, "frameworks": FRAMEWORKS, + "detected_at": datetime.now(timezone.utc).isoformat(), "metadata": { "resource_group": resource_group, "flow_logs_enabled": False, From b98b4ef35ede504788dab3888d53d7266449d5ef Mon Sep 17 00:00:00 2001 From: safidnadaf Date: Sat, 30 May 2026 17:08:02 +0100 Subject: [PATCH 6/6] fix: correct az_net_012.py to match az_cmp_003 pattern --- scanner/rules/az_net_012.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/scanner/rules/az_net_012.py b/scanner/rules/az_net_012.py index bc344a7..f345573 100644 --- a/scanner/rules/az_net_012.py +++ b/scanner/rules/az_net_012.py @@ -4,7 +4,6 @@ from datetime import datetime, timezone from typing import Any, Dict, List -# Required rule constants RULE_ID = "AZ-NET-012" RULE_NAME = "NSG Flow Logs Not Enabled" SEVERITY = "MEDIUM" @@ -19,12 +18,12 @@ "Run: az network watcher flow-log create --nsg --enabled true " "--storage-account --resource-group " ) -PLAYBOOK = "fix_az_net_012.sh" +PLAYBOOK = "playbooks/cli/fix_az_net_012.sh" FRAMEWORKS = { - "cis": "6.5", - "nist": "DE.CM-1", - "iso27001": "A.12.4.1", - "soc2": "CC7.2", + "CIS": "6.5", + "NIST": "DE.CM-1", + "ISO27001": "A.12.4.1", + "SOC2": "CC7.2", } logger = logging.getLogger(__name__) @@ -34,9 +33,7 @@ def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: """Scan all NSGs and check if flow logs are enabled via Network Watcher.""" findings: List[Dict[str, Any]] = [] - nsgs = azure_client.network_security_groups.list_all() - - for nsg in nsgs: + for nsg in azure_client.get_network_security_groups(): nsg_id = getattr(nsg, "id", "") parsed = azure_client.parse_resource_id(nsg_id) resource_group = parsed.get("resource_group", "") @@ -48,9 +45,12 @@ def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: flow_log_enabled = False try: - flow_logs = azure_client.flow_logs.list(resource_group) + flow_logs = azure_client.get_nsg_flow_logs(resource_group) for flow_log in flow_logs: - if getattr(flow_log, "target_resource_id", "") == nsg_id and getattr(flow_log, "enabled", False): + if ( + getattr(flow_log, "target_resource_id", "") == nsg_id + and getattr(flow_log, "enabled", False) + ): flow_log_enabled = True break except Exception: