diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..627de2f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,69 @@ +name: Feature Request +about: Suggest a new rule, compliance mapping, playbook, or capability for OpenShield +title: "feat: " +labels: enhancement +assignees: '' +--- + +## Summary + +A clear one-sentence description of the feature you are proposing. + +## Problem It Solves + +What is the current limitation or gap? Why does this matter for Azure cloud security posture? +Link any related issues or discussions if relevant. + +## Proposed Solution + +Describe what you want to happen. Be specific. + +--- + +**If proposing a new scanner rule, fill in all fields below:** + +- Azure resource type: +- Misconfiguration it detects: +- Suggested RULE_ID (format: `AZ--`, e.g. `AZ-KV-003`): +- Severity: (CRITICAL / HIGH / MEDIUM / LOW) +- Compliance frameworks it maps to: + - CIS Azure Benchmark control: + - NIST CSF control: + - ISO 27001 control: +- Does a matching remediation playbook need to be created? (Yes / No) + +--- + +**If proposing a compliance mapping:** + +- Framework name and version: +- Control ID(s): +- Which existing rules does it apply to: +- Source documentation link: + +--- + +**If proposing an API or CLI change:** + +- Endpoint or command affected: +- Current behaviour: +- Proposed behaviour: +- Example request/response or command: + +--- + +## Alternatives Considered + +What other approaches did you consider, and why did you rule them out? + +## Additional Context + +Add any Azure documentation links, CVE references, CIS Benchmark pages, screenshots, or reference implementations here. + +## Contribution + +Are you willing to implement this yourself? + +- [ ] Yes, I plan to open a PR for this +- [ ] I can help review a PR but cannot implement it myself +- [ ] I am not able to contribute code for this \ No newline at end of file diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..e8e98d2 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,82 @@ +# Security Policy + +## Reporting a Vulnerability + +If you discover a security vulnerability in OpenShield, please **do not open a public GitHub issue**. +Opening a public issue exposes the vulnerability to bad actors before a fix is available. + + +We will acknowledge your report within 48 hours and work with you to coordinate a fix and responsible disclosure timeline. + +### What to include in your report + +To help us triage quickly, please include: + +- A description of the vulnerability and its potential impact +- The affected component (scanner engine, REST API, auth logic, playbooks) +- Steps to reproduce the issue +- Any relevant logs, proof-of-concept code, or screenshots +- The version of OpenShield you were testing (check `git log --oneline -1`) + +The more detail you provide, the faster we can respond. + +--- + +## Supported Versions + +| Version | Supported | +|---------|-----------| +| 0.1.x | Yes | + +Older versions are not patched. If you are running a version below 0.1.x, upgrade to the latest release before filing a report. + +--- + +## Disclosure Process + +We follow a coordinated disclosure model: + +1. **Report received** -- you email the vulnerability privately +2. **Acknowledgement** -- we respond within 48 hours to confirm receipt +3. **Investigation** -- we reproduce and assess the impact +4. **Fix developed** -- we write and test a patch +5. **Coordinated release** -- we agree a disclosure date with you (typically 7-14 days after fix) +6. **Public advisory** -- we publish a GitHub Security Advisory and release the fix + +We ask that you do not publicly disclose the vulnerability until step 6 is complete. + +--- + +## Scope + +### In scope + +- Scanner engine (`scanner/`) -- rule logic, Azure SDK calls, output handling +- REST API (`api/`) -- authentication, authorisation, input validation, JWT handling +- Compliance framework mappings (`compliance/`) -- data integrity +- Sentinel integration (`sentinel/`) -- HMAC signing, data upload logic +- Hardcoded secrets or credentials anywhere in the codebase + +### Out of scope + +- Vulnerabilities in third-party dependencies -- report those to the upstream maintainer +- Security issues in infrastructure you deploy OpenShield to (your Azure environment, your PostgreSQL instance) +- Social engineering attacks +- Physical security + +--- + +## Recognition + +We value responsible disclosure. Researchers who report valid vulnerabilities will be: + +- Acknowledged by name (or pseudonym if preferred) in the release notes for the fix +- Listed in a `SECURITY_ACKNOWLEDGEMENTS.md` file we maintain in this repository + +We do not currently offer a bug bounty programme, but we are grateful for every report. + +--- + +## Contact + +**Email: vishnu.ajith@owasp.org** \ No newline at end of file diff --git a/README.md b/README.md index d4dca8b..d75eb2e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,13 @@ # OpenShield -> **Open source Cloud Security Posture Management (CSPM) for Azure — built by the community, for the community.** - -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +> **Open source Cloud Security Posture Management (CSPM) for Azure - built by the community, for the community.** + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![Python 3.11](https://img.shields.io/badge/python-3.11-blue.svg)](https://www.python.org/downloads/release/python-3110/) +[![CI](https://github.com/openshield-org/openshield/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/openshield-org/openshield/actions/workflows/ci.yml) +[![Deploy](https://github.com/openshield-org/openshield/actions/workflows/deploy.yml/badge.svg?branch=dev)](https://github.com/openshield-org/openshield/actions/workflows/deploy.yml) +[![Security Policy](https://img.shields.io/badge/security-policy-green.svg)](.github/SECURITY.md) +[![OWASP](https://img.shields.io/badge/OWASP-listing%20review-orange.svg)](https://owasp.org) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![Good First Issues](https://img.shields.io/github/issues/openshield-org/openshield/good-first-issue)](https://github.com/openshield-org/openshield/issues?q=is%3Aissue+label%3Agood-first-issue) [![Discord](https://img.shields.io/badge/Discord-Join%20Us-7289da)](https://discord.gg/openshield) @@ -113,6 +118,7 @@ openshield/ --- + ## Quick Start ```bash @@ -185,4 +191,20 @@ MIT — free to use, modify, and distribute. --- +> Built with ❤️ by security engineers and students who believe cloud security tooling should be accessible to everyone. + +--- + +## Learn OpenShield + +Explore the OpenShield learning portal to understand: + +- Azure CSPM fundamentals +- OpenShield architecture +- Compliance mappings +- Remediation workflows +- Contributor onboarding +- Documentation navigation + +👉 [OpenShield Learn](docs/learn/index.html) > Built by security engineers and students who believe cloud security tooling should be accessible to everyone. diff --git a/api/app.py b/api/app.py index 691bfe4..21ccb24 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: @@ -171,4 +171,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/models/finding.py b/api/models/finding.py index 7b2eda7..6f03068 100644 --- a/api/models/finding.py +++ b/api/models/finding.py @@ -96,6 +96,13 @@ def _get_conn(self) -> Any: self.connect() return self.conn + def close(self) -> None: + """Close the database connection.""" + if self.conn and not self.conn.closed: + self.conn.close() + self.conn = None + logger.debug("Database connection closed") + # ------------------------------------------------------------------ # # Schema # # ------------------------------------------------------------------ # 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 diff --git a/compliance/frameworks/cis_azure_benchmark.json b/compliance/frameworks/cis_azure_benchmark.json index f5c1989..68ec4e6 100644 --- a/compliance/frameworks/cis_azure_benchmark.json +++ b/compliance/frameworks/cis_azure_benchmark.json @@ -98,6 +98,11 @@ "control_name": "Ensure that 'OS disk' are encrypted", "description": "Virtual machine OS and data disks are using platform-managed encryption only (EncryptionAtRestWithPlatformKey). CIS 7.2 requires disks to be protected using customer-managed keys or Azure Disk Encryption. Platform-managed encryption does not give the organisation control over the encryption keys and does not satisfy this control." }, + "AZ-CMP-003": { + "control_id": "8.2", + "control_name": "Ensure that 'Endpoint protection solution' is installed on VMs", + "description": "The virtual machine does not have a recognised endpoint protection extension installed. CIS 8.2 requires that an approved endpoint protection solution is installed and running on all virtual machines. Without endpoint protection, malware and ransomware can execute without detection." + }, "AZ-KV-001": { "control_id": "8.5", "control_name": "Ensure the Key Vault is Recoverable", @@ -127,6 +132,11 @@ "control_id": "4.3.6", "control_name": "Ensure SSL connection is enabled for PostgreSQL Flexible Server", "description": "SSL enforcement should be enabled on PostgreSQL Flexible Server to ensure data in transit is encrypted. Without SSL, database connections transmit data in plaintext, exposing it to interception." + }, + "AZ-KV-004": { + "control_id": "8.6", + "control_name": "Ensure that Azure Key Vault Purge Protection is Enabled", + "description": "Azure Key Vaults without purge protection enabled allow permanent deletion of vaults and their secrets, keys, and certificates during the soft-delete retention period. Even with soft delete enabled, a malicious insider or privileged account can purge vault objects before the retention period expires. Enabling purge protection prevents this by blocking purge operations for the full retention period." } } } \ No newline at end of file diff --git a/compliance/frameworks/iso27001.json b/compliance/frameworks/iso27001.json index 697052e..71cd134 100644 --- a/compliance/frameworks/iso27001.json +++ b/compliance/frameworks/iso27001.json @@ -98,6 +98,11 @@ "control_name": "Policy on the use of cryptographic controls", "description": "Virtual machine OS and data disks are using platform-managed encryption only (EncryptionAtRestWithPlatformKey). A.10.1.1 requires that a policy on the use of cryptographic controls is developed and implemented. Platform-managed encryption does not give the organisation control over the encryption keys. Customer-managed keys or Azure Disk Encryption are required to satisfy this control." }, + "AZ-CMP-003": { + "control_id": "A.12.2.1", + "control_name": "Controls against malware", + "description": "The virtual machine does not have a recognised endpoint protection extension installed. A.12.2.1 requires that detection, prevention and recovery controls are implemented to protect against malware. Without endpoint protection, malware executing on the VM will not be detected or prevented." + }, "AZ-KV-001": { "control_id": "A.17.2.1", "control_name": "Availability of information processing facilities", @@ -127,6 +132,11 @@ "control_id": "A.10.1.1", "control_name": "Policy on the use of cryptographic controls", "description": "SSL enforcement on PostgreSQL Flexible Server applies cryptographic controls to data in transit. A policy on the use of cryptographic controls for protection of information should be developed and implemented." + }, + "AZ-KV-004": { + "control_id": "A.17.2.1", + "control_name": "Availability of information processing facilities", + "description": "Purge protection prevents permanent deletion of Azure Key Vault secrets, keys, and certificates during the soft-delete retention period. Without it, cryptographic material can be irrecoverably destroyed, threatening the availability of information processing facilities that depend on those keys and secrets." } } } \ No newline at end of file diff --git a/compliance/frameworks/nist_csf.json b/compliance/frameworks/nist_csf.json index ad41cc2..18d6376 100644 --- a/compliance/frameworks/nist_csf.json +++ b/compliance/frameworks/nist_csf.json @@ -98,6 +98,11 @@ "control_name": "Data-at-rest is protected", "description": "Virtual machine OS and data disks are using platform-managed encryption only (EncryptionAtRestWithPlatformKey). PR.DS-1 requires that data at rest is protected using appropriate controls. Platform-managed encryption does not give the organisation control over the encryption keys. Customer-managed keys or Azure Disk Encryption are required to satisfy this control." }, + "AZ-CMP-003": { + "control_id": "DE.CM-4", + "control_name": "Malicious code is detected", + "description": "The virtual machine does not have a recognised endpoint protection extension installed. DE.CM-4 requires that malicious code is detected on organisational systems. Without endpoint protection, malware and ransomware executing on the VM will not be detected or blocked." + }, "AZ-KV-001": { "control_id": "PR.IP-4", "control_name": "Backups of information are conducted, maintained, and tested", @@ -127,6 +132,11 @@ "control_id": "PR.DS-2", "control_name": "Data-in-transit is protected", "description": "SSL enforcement on PostgreSQL Flexible Server ensures data in transit between applications and the database is encrypted. Disabling SSL exposes database traffic to interception and tampering." + }, + "AZ-KV-004": { + "control_id": "PR.IP-4", + "control_name": "Backups of information are conducted, maintained, and tested", + "description": "Purge protection ensures that deleted Key Vault objects can be recovered within the retention period and cannot be permanently destroyed before it expires. Without purge protection, backups of cryptographic material may be rendered unrecoverable if an insider or compromised account issues a purge operation during the soft-delete window." } } } \ No newline at end of file diff --git a/compliance/frameworks/soc2.json b/compliance/frameworks/soc2.json index a793241..10eb673 100644 --- a/compliance/frameworks/soc2.json +++ b/compliance/frameworks/soc2.json @@ -103,6 +103,11 @@ "control_name": "Protects Data in Transit and At Rest", "description": "Virtual machine OS and data disks are using platform-managed encryption only (EncryptionAtRestWithPlatformKey). CC6.7 requires that data is protected using encryption. Platform-managed encryption does not give the organisation control over the encryption keys. Customer-managed keys or Azure Disk Encryption are required to satisfy this control." }, + "AZ-CMP-003": { + "control_id": "CC6.8", + "control_name": "Prevents or Detects Unauthorized or Malicious Software", + "description": "The virtual machine does not have a recognised endpoint protection extension installed. CC6.8 requires that controls are implemented to prevent or detect and act upon the introduction of unauthorized or malicious software. Without endpoint protection, malicious code executing on the VM will not be detected or blocked." + }, "AZ-KV-001": { "control_id": "A1.2", "control_name": "Environmental Threats and Recovery", @@ -122,6 +127,11 @@ "control_id": "CC6.1", "control_name": "Logical and physical access controls", "description": "SSL enforcement ensures database connections are encrypted, protecting data in transit from unauthorized access. Disabling SSL undermines logical access controls by exposing database traffic in plaintext." + }, + "AZ-KV-004": { + "control_id": "CC9.1", + "control_name": "Risk Mitigation", + "description": "Azure Key Vaults without purge protection enabled allow permanent deletion of secrets, keys, and certificates during the soft-delete retention period. CC9.1 requires that identified risks are mitigated through controls that reduce the likelihood or impact of risk events. Enabling purge protection mitigates the risk of irrecoverable loss of cryptographic material by preventing purge operations from executing before the retention period expires." } } } \ No newline at end of file diff --git a/docs/learn/index.html b/docs/learn/index.html new file mode 100644 index 0000000..93c5164 --- /dev/null +++ b/docs/learn/index.html @@ -0,0 +1,479 @@ + + + + + + OpenShield Learn + + + +
+
Open Source Azure CSPM Platform
+

OpenShield Learn

+

+ A practical learning hub for understanding OpenShield, Azure cloud security posture management, + misconfiguration detection, compliance mapping, drift detection, and remediation workflows. +

+ +
+ +
+
+

What is OpenShield?

+

+ OpenShield is an open-source Azure CSPM platform designed to identify cloud misconfigurations, + map findings to compliance frameworks, monitor posture drift, and provide remediation guidance. It helps users understand + what is insecure, why it matters, and how to fix it. +

+
+
+

Misconfiguration Scanning

+

Checks Azure resources for risky settings that can expose data, weaken access control, or reduce security visibility.

+
+
+

Compliance Mapping

+

Connects security findings to frameworks such as CIS, NIST, and ISO so issues can be understood in a governance context.

+
+
+

Remediation Guidance

+

Provides practical fix guidance using Azure CLI, ARM templates, Terraform, and validation checks where applicable.

+
+
+

Drift Detection

+

Tracks changes in cloud security posture so teams can identify when previously safe configurations become risky.

+
+
+
+ +
+

How OpenShield Works

+

+ OpenShield follows a simple scanning pipeline: collect Azure resource configuration, evaluate rules, + generate findings, map them to controls, and expose results through the platform. +

+
+
Azure Subscription
+
Scanner Engine
+
Rule Evaluation
+
Findings
+
Compliance Mapping
+
Drift Detection
+
Dashboard & Reporting
+
+
+ +
+

Core Components

+

+ OpenShield is built with a simple MVP-friendly architecture: Python scanner, Flask API, + PostgreSQL storage, React frontend, compliance mapping, Sentinel integration, and supporting remediation playbooks. +

+
+
+

Scanner Engine

+

Python-based scanner that uses Azure SDK clients to inspect Azure resource configuration and evaluate security rules.

+ PythonAzure SDK +
+
+

Flask API

+

Backend API layer responsible for exposing scan results, findings, metadata, and platform data to the frontend.

+ FlaskREST API +
+
+

PostgreSQL

+

Stores scan findings, rule metadata, compliance mappings, and remediation-related information.

+ DatabasePersistence +
+
+

React Dashboard

+

Frontend dashboard for viewing findings, severity, affected resources, and security posture information.

+ ReactDashboard +
+
+

Playbooks

+

Remediation documents that explain how to fix detected issues using CLI, ARM templates, Terraform, and validation steps.

+ Azure CLIARMTerraform +
+
+

Sentinel

+

Supports security monitoring and SIEM-focused documentation where OpenShield findings connect with detection workflows.

+ SIEMDetection +
+
+
+ +
+

CSPM Basics

+

+ Cloud Security Posture Management focuses on continuously identifying insecure cloud configurations. + In Azure, common examples include public storage exposure, weak network rules, missing logging, + overly permissive identities, and disabled security protections. +

+
+
+

Why It Matters

+

Cloud breaches often happen because resources are misconfigured, not because the cloud provider itself failed.

+
+
+

Example Issues

+
    +
  • Public blob access
  • +
  • Weak network security groups
  • +
  • Missing monitoring or logging
  • +
  • Over-permissive access policies
  • +
+
+
+

OpenShield Role

+

OpenShield helps surface these issues, explain their impact, and guide users toward safer Azure configurations.

+
+
+
+ +
+

Compliance Mapping

+

+ A single security finding can map to multiple compliance controls. OpenShield uses mappings to connect + technical misconfigurations with security frameworks such as CIS Benchmarks, NIST CSF, ISO 27001, and SOC 2. +

+
+

CIS

Maps findings to cloud security benchmarks and configuration recommendations.

+

NIST

Connects findings to broader cybersecurity controls and risk management practices.

+

ISO 27001

Supports governance, information security controls, and audit-oriented reporting context.

+

SOC 2

Connects relevant findings to trust-service control areas such as security, availability, and confidentiality.

+
+
+ +
+

Remediation Philosophy

+

+ Detection alone is not enough. A useful CSPM tool should explain the risk, provide fix guidance, + and help validate whether the issue has actually been resolved. +

+
+

Detect

Identify insecure Azure configuration accurately with minimal false positives.

+

Explain

Show why the finding matters, what resource is affected, and what the risk is.

+

Fix

Provide Azure CLI, ARM template, or Terraform-based remediation steps that users can apply safely.

+

Validate

Re-run checks or confirm settings to verify the misconfiguration is resolved.

+
+
+ +
+

Contributor Learning Path

+

+ New contributors should understand the security problem first, then the OpenShield architecture, + then the rule and remediation workflow. +

+
+
+

Suggested Path

+
    +
  1. Understand CSPM fundamentals
  2. +
  3. Review the OpenShield architecture
  4. +
  5. Explore existing documentation and rules
  6. +
  7. Understand findings, mappings, and remediation playbooks
  8. +
  9. Add or improve rules and playbooks
  10. +
  11. Test changes against Azure safely
  12. +
+
+
+

Contribution Focus

+

Good contributions improve detection accuracy, remediation quality, documentation clarity, or platform reliability.

+
+
+
+ +
+

Documentation Links

+

+ Use these links as the starting point for understanding and contributing to OpenShield. +

+
+
+
ArchitectureSystem design, platform components, and scanning workflow.
+ Open +
+
+
API ReferenceBackend API documentation for working with OpenShield data.
+ Open +
+
+
Azure SetupRequired Azure setup and configuration before running scans.
+ Open +
+
+
Rules ReferenceRule documentation and expected structure for security checks.
+ Open +
+
+
Adding a RuleContributor guide for creating and testing new scan rules.
+ Open +
+
+
+ +
+

Open Source Goals

+

+ OpenShield aims to make Azure security posture management easier to understand, easier to test, + and easier to improve through community contribution. +

+
+

Security Research

Encourage practical Azure misconfiguration research and rule development.

+

Education

Help learners understand CSPM, cloud controls, and secure Azure configuration.

+

Community

Build a contributor-friendly platform where improvements are clear and reviewable.

+
+
+ +
+

Future Scope

+

+ OpenShield can grow over time with richer dashboards, stronger compliance reports, + automated remediation workflows, and eventually broader cloud coverage. +

+
+ +
+ Note: This page is a static documentation hub. Do not add fake file upload buttons here. + Real uploads require backend storage, authentication, authorization, file validation, and access control. +
+
+ +
+ OpenShield — Open Source Azure CSPM Platform | Learn, Contribute, Improve Azure Security +
+ + diff --git a/playbooks/cli/fix_az_cmp_003.sh b/playbooks/cli/fix_az_cmp_003.sh new file mode 100644 index 0000000..f2c83f1 --- /dev/null +++ b/playbooks/cli/fix_az_cmp_003.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# OpenShield Remediation Playbook +# Rule: AZ-CMP-003 — VM without endpoint protection installed +# Usage: ./fix_az_cmp_003.sh [windows|linux] +# Severity: HIGH + +set -e + +RG=$1 +VM=$2 +OS=${3:-windows} + +if [ -z "$RG" ] || [ -z "$VM" ]; then + echo "Usage: $0 [windows|linux]" + exit 1 +fi + +if [ "${OS,,}" = "linux" ]; then + echo "Installing MDE.Linux on $VM..." + az vm extension set \ + --resource-group "$RG" \ + --vm-name "$VM" \ + --name "MDE.Linux" \ + --publisher "Microsoft.Azure.AzureDefenderForServers" \ + --version "1.0" \ + --auto-upgrade-minor-version true + echo "Done. Finish onboarding in the Defender portal." +else + echo "Enabling IaaSAntimalware on $VM..." + SETTINGS='{ + "AntimalwareEnabled": true, + "RealtimeProtectionEnabled": true, + "ScheduledScanSettings": { + "isEnabled": true, + "day": "1", + "time": "120", + "scanType": "Quick" + } + }' + az vm extension set \ + --resource-group "$RG" \ + --vm-name "$VM" \ + --name "IaaSAntimalware" \ + --publisher "Microsoft.Azure.Security" \ + --version "1.3" \ + --auto-upgrade-minor-version true \ + --settings "$SETTINGS" + echo "IaaSAntimalware enabled on $VM." +fi diff --git a/playbooks/cli/fix_az_kv_004.sh b/playbooks/cli/fix_az_kv_004.sh new file mode 100644 index 0000000..d4d193c --- /dev/null +++ b/playbooks/cli/fix_az_kv_004.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -euo pipefail +# AZ-KV-004: Enable purge protection on an Azure Key Vault +# Usage: ./fix_az_kv_004.sh +RESOURCE_GROUP=$1 +VAULT_NAME=$2 +if [ -z "$RESOURCE_GROUP" ] || [ -z "$VAULT_NAME" ]; then + echo "Usage: $0 " + exit 1 +fi +echo "Enabling purge protection on Key Vault: $VAULT_NAME..." +az keyvault update \ + --resource-group "$RESOURCE_GROUP" \ + --name "$VAULT_NAME" \ + --enable-purge-protection true +echo "Purge protection enabled for Key Vault: $VAULT_NAME" +echo "Note: Purge protection cannot be disabled once enabled." \ No newline at end of file diff --git a/scanner/azure_client.py b/scanner/azure_client.py index e65f567..5dc9bd0 100644 --- a/scanner/azure_client.py +++ b/scanner/azure_client.py @@ -240,6 +240,7 @@ def get_virtual_networks(self) -> List[Any]: logger.error("get_virtual_networks failed: %s", exc) return [] + def get_public_ip_addresses(self) -> List[Any]: """List all public IP addresses in the subscription.""" try: @@ -262,6 +263,16 @@ def get_virtual_machines(self) -> List[Any]: logger.error("get_virtual_machines failed: %s", exc) return [] + + + def get_vm_extensions(self, resource_group: str, vm_name: str) -> Optional[List[Any]]: + try: + result = ComputeManagementClient(self.credential, self.subscription_id).virtual_machine_extensions.list(resource_group, vm_name) + return list(getattr(result, "value", []) or []) + except Exception as exc: + logger.error("get_vm_extensions failed for %s/%s: %s", resource_group, vm_name, exc) + return None + # ------------------------------------------------------------------ # # Databases # # ------------------------------------------------------------------ # diff --git a/scanner/rules/az_cmp_003.py b/scanner/rules/az_cmp_003.py new file mode 100644 index 0000000..96c88a0 --- /dev/null +++ b/scanner/rules/az_cmp_003.py @@ -0,0 +1,80 @@ +"""AZ-CMP-003: VM without endpoint protection installed.""" + +import logging +from typing import Any, Dict, List + +RULE_ID = "AZ-CMP-003" +RULE_NAME = "VM Without Endpoint Protection Installed" +SEVERITY = "HIGH" +CATEGORY = "Compute" +FRAMEWORKS = { + "CIS": "8.2", + "NIST": "DE.CM-4", + "ISO27001": "A.12.2.1", + "SOC2": "CC6.8", +} +DESCRIPTION = ( + "VM has no recognised endpoint protection extension installed. " + "Without it malware and ransomware can run undetected. " + "CIS 8.2 requires an approved AV/EDR solution on all VMs." +) +REMEDIATION = ( + "Install IaaSAntimalware or onboard to MDE (MDE.Windows / MDE.Linux) " + "depending on the OS." +) +PLAYBOOK = "playbooks/cli/fix_az_cmp_003.sh" + +KNOWN_EP_EXTENSIONS = { + "microsoftmonitoringagent", + "mde.linux", + "mde.windows", + "iaasantimalware", +} + +logger = logging.getLogger(__name__) + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + findings: List[Dict[str, Any]] = [] + + for vm in azure_client.get_virtual_machines(): + parsed = azure_client.parse_resource_id(getattr(vm, "id", "")) + rg = parsed.get("resource_group", "") + vm_name = parsed.get("name", "") + if not rg or not vm_name: + continue + + exts = azure_client.get_vm_extensions(rg, vm_name) + if exts is None: + continue + + installed = set() + for e in exts: + t = ( + getattr(e, "type_properties_type", None) + or getattr(e, "virtual_machine_extension_type", None) + or getattr(e, "type", "") + ) + if t: + installed.add(t.lower()) + + if not installed.intersection(KNOWN_EP_EXTENSIONS): + findings.append({ + "rule_id": RULE_ID, + "rule_name": RULE_NAME, + "severity": SEVERITY, + "category": CATEGORY, + "resource_id": vm.id, + "resource_name": vm_name, + "resource_type": "Microsoft.Compute/virtualMachines", + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "metadata": { + "resource_group": rg, + "installed_extensions": sorted(installed), + }, + }) + + return findings diff --git a/scanner/rules/az_kv_004.py b/scanner/rules/az_kv_004.py new file mode 100644 index 0000000..d281976 --- /dev/null +++ b/scanner/rules/az_kv_004.py @@ -0,0 +1,58 @@ +"""AZ-KV-004: Key Vault purge protection disabled.""" + +from typing import Any, Dict, List + +RULE_ID = "AZ-KV-004" +RULE_NAME = "Key Vault Purge Protection Disabled" +SEVERITY = "MEDIUM" +CATEGORY = "Key Vault" +FRAMEWORKS = { + "CIS": "8.6", + "NIST": "PR.IP-4", + "ISO27001": "A.17.2.1", + "SOC2": "CC9.1" +} +DESCRIPTION = ( + "Azure Key Vaults without purge protection enabled allow permanent " + "deletion of vaults and their secrets, keys, and certificates during " + "the soft-delete retention period. Without purge protection, a " + "malicious insider or accidental deletion can result in irrecoverable " + "loss of cryptographic material." +) +REMEDIATION = ( + "Enable purge protection on the Key Vault. Note: once enabled, " + "purge protection cannot be disabled. Ensure soft delete is also " + "enabled as purge protection requires it." +) +PLAYBOOK = "playbooks/cli/fix_az_kv_004.sh" + + +def scan(azure_client: Any, subscription_id: str) -> List[Dict[str, Any]]: + """Return a list of findings. Return [] if no issues are found.""" + findings: List[Dict[str, Any]] = [] + + for vault in azure_client.get_key_vaults(): + parsed = azure_client.parse_resource_id(vault.id) + resource_group = parsed["resource_group"] + vault_name = parsed["name"] + + properties = getattr(vault, "properties", None) + purge_protection = getattr(properties, "enable_purge_protection", False) + + if not purge_protection: + findings.append({ + "rule_id": RULE_ID, + "rule_name": RULE_NAME, + "severity": SEVERITY, + "category": CATEGORY, + "resource_id": vault.id, + "resource_name": vault_name, + "resource_type": "Microsoft.KeyVault/vaults", + "description": DESCRIPTION, + "remediation": REMEDIATION, + "playbook": PLAYBOOK, + "frameworks": FRAMEWORKS, + "metadata": {"resource_group": resource_group} + }) + + return findings \ No newline at end of file