From 702e0f37227467ddb0ea0761c647ccccf0ef5b92 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 01:46:51 +0100 Subject: [PATCH 01/16] Create __init__.py --- ai/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 ai/__init__.py diff --git a/ai/__init__.py b/ai/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/ai/__init__.py @@ -0,0 +1 @@ + From 040af13dd0e7c0083995cebedbddc0877f495c84 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 01:50:28 +0100 Subject: [PATCH 02/16] Implement OpenShield vector store builder This script builds the OpenShield knowledge base vector store for RAG AI insights by collecting rules and compliance documents, then embedding them into a vector store. --- ai/embed.py | 109 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 ai/embed.py diff --git a/ai/embed.py b/ai/embed.py new file mode 100644 index 00000000..0b04702b --- /dev/null +++ b/ai/embed.py @@ -0,0 +1,109 @@ +"""Build the OpenShield knowledge base vector store for RAG AI insights.""" + +import importlib.util +import json +import logging +from pathlib import Path + +import chromadb + +logger = logging.getLogger(__name__) + +REPO_ROOT = Path(__file__).resolve().parent.parent +RULES_DIR = REPO_ROOT / "scanner" / "rules" +FRAMEWORKS_DIR = REPO_ROOT / "compliance" / "frameworks" +VECTORSTORE_DIR = REPO_ROOT / "ai" / "vectorstore" +COLLECTION_NAME = "openshield" + + +def _load_rule_module(path): + spec = importlib.util.spec_from_file_location(path.stem, path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _collect_rule_documents(): + documents = [] + for path in sorted(RULES_DIR.glob("az_*.py")): + try: + module = _load_rule_module(path) + except Exception as exc: + logger.warning("Skipping %s: %s", path.name, exc) + continue + rule_id = getattr(module, "RULE_ID", None) + if not rule_id: + continue + text = ( + f"OpenShield rule {rule_id}: {getattr(module, 'RULE_NAME', '')}\n" + f"Category: {getattr(module, 'CATEGORY', '')}\n" + f"Severity: {getattr(module, 'SEVERITY', '')}\n" + f"Description: {getattr(module, 'DESCRIPTION', '')}\n" + f"Remediation: {getattr(module, 'REMEDIATION', '')}" + ) + documents.append({ + "id": f"rule-{rule_id}", + "text": text, + "source": rule_id, + "type": "rule", + }) + return documents + + +def _collect_compliance_documents(): + documents = [] + for path in sorted(FRAMEWORKS_DIR.glob("*.json")): + framework = path.stem + try: + data = json.loads(path.read_text(encoding="utf-8")) + except Exception as exc: + logger.warning("Skipping %s: %s", path.name, exc) + continue + for control_id, control in data.get("controls", {}).items(): + description = control.get("description", "") + if not description: + continue + text = ( + f"{framework} control {control_id}: " + f"{control.get('control_name', '')}\n{description}" + ) + documents.append({ + "id": f"{framework}-{control_id}", + "text": text, + "source": f"{framework} {control_id}", + "type": "control", + }) + return documents + + +def build_vectorstore(): + VECTORSTORE_DIR.mkdir(parents=True, exist_ok=True) + client = chromadb.PersistentClient(path=str(VECTORSTORE_DIR)) + + # Start clean so a refresh never leaves stale entries behind. + try: + client.delete_collection(COLLECTION_NAME) + except Exception: + pass + collection = client.create_collection(COLLECTION_NAME) + + documents = _collect_rule_documents() + _collect_compliance_documents() + if not documents: + raise RuntimeError("No documents found to embed. Check repo paths.") + + collection.add( + ids=[d["id"] for d in documents], + documents=[d["text"] for d in documents], + metadatas=[ + {"source": d["source"], "type": d["type"]} for d in documents + ], + ) + logger.info( + "Embedded %d documents into '%s'.", len(documents), COLLECTION_NAME + ) + return len(documents) + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + count = build_vectorstore() + print(f"Done. Vector store built with {count} documents at {VECTORSTORE_DIR}") From 9f8b2ca204d759860b6769a8085abcae35af22c9 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 01:51:25 +0100 Subject: [PATCH 03/16] Add AI insights routes for summary, prioritisation, and Q&A --- api/routes/ai.py | 172 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 api/routes/ai.py diff --git a/api/routes/ai.py b/api/routes/ai.py new file mode 100644 index 00000000..f3cc7b8d --- /dev/null +++ b/api/routes/ai.py @@ -0,0 +1,172 @@ +"""AI insights routes: RAG grounded summary, prioritisation and Q&A.""" + +import json +import logging + +from flask import Blueprint, jsonify, request + +from api.services.ai_provider import get_completion +from ai.retriever import retrieve, VectorStoreNotBuilt + +ai_bp = Blueprint("ai", __name__) +logger = logging.getLogger(__name__) + +SEVERITY_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3} + + +def _findings_to_text(findings): + ordered = sorted( + findings, + key=lambda f: SEVERITY_ORDER.get(str(f.get("severity", "")).upper(), 4), + ) + lines = [] + for i, f in enumerate(ordered, 1): + lines.append( + f"{i}. [{f.get('severity', 'UNKNOWN')}] " + f"{f.get('rule_name', 'Unknown')} on " + f"{f.get('resource_name', 'unknown resource')}: " + f"{f.get('description', '')}" + ) + return "\n".join(lines) if lines else "No findings." + + +def _context_for(query): + chunks = retrieve(query, n_results=5) + context = "\n".join(f"- ({c['source']}) {c['text']}" for c in chunks) + sources = [c["source"] for c in chunks if c["source"]] + return context, sources + + +def _read_request(): + body = request.get_json(silent=True) + if not body: + return None, (jsonify({"error": "Request body must be JSON"}), 400) + if not body.get("provider"): + return None, (jsonify({"error": "provider is required"}), 400) + if not body.get("api_key"): + return None, (jsonify({"error": "api_key is required"}), 400) + return body, None + + +@ai_bp.post("/api/ai/summary") +def ai_summary(): + body, error = _read_request() + if error: + return error + findings = body.get("findings", []) + if not isinstance(findings, list): + return jsonify({"error": "findings must be a list"}), 400 + + findings_text = _findings_to_text(findings) + try: + context, sources = _context_for(findings_text) + except VectorStoreNotBuilt as exc: + return jsonify({"error": str(exc)}), 503 + + prompt = ( + "You are a cloud security advisor. Using ONLY the grounded knowledge " + "below, write a plain English executive summary of the security " + "posture for a non technical reader. Keep it under 120 words.\n\n" + f"GROUNDED KNOWLEDGE:\n{context}\n\nFINDINGS:\n{findings_text}" + ) + try: + answer = get_completion( + body["provider"], body["api_key"], prompt, model=body.get("model") + ) + except ValueError as exc: + return jsonify({"error": str(exc)}), 400 + except RuntimeError as exc: + return jsonify({"error": str(exc)}), 502 + + return jsonify({ + "summary": answer, + "sources": sources, + "provider": body["provider"], + "model": body.get("model"), + }) + + +@ai_bp.post("/api/ai/prioritise") +def ai_prioritise(): + body, error = _read_request() + if error: + return error + findings = body.get("findings", []) + if not isinstance(findings, list): + return jsonify({"error": "findings must be a list"}), 400 + + findings_text = _findings_to_text(findings) + try: + context, sources = _context_for(findings_text) + except VectorStoreNotBuilt as exc: + return jsonify({"error": str(exc)}), 503 + + prompt = ( + "You are a cloud security advisor. Using ONLY the grounded knowledge " + "below, rank these findings by real world exploitability and business " + "risk, not just the severity label. Respond with valid JSON only, no " + "markdown, as a list of objects with fields: priority, rule_name, " + "resource_name, severity, reason.\n\n" + f"GROUNDED KNOWLEDGE:\n{context}\n\nFINDINGS:\n{findings_text}" + ) + try: + raw = get_completion( + body["provider"], body["api_key"], prompt, model=body.get("model") + ) + except ValueError as exc: + return jsonify({"error": str(exc)}), 400 + except RuntimeError as exc: + return jsonify({"error": str(exc)}), 502 + + try: + prioritised = json.loads(raw) + except (json.JSONDecodeError, TypeError): + prioritised = raw + + return jsonify({ + "prioritised_findings": prioritised, + "sources": sources, + "provider": body["provider"], + "model": body.get("model"), + }) + + +@ai_bp.post("/api/ai/ask") +def ai_ask(): + body, error = _read_request() + if error: + return error + question = body.get("question", "") + if not question or not question.strip(): + return jsonify({"error": "question is required"}), 400 + + try: + context, sources = _context_for(question) + except VectorStoreNotBuilt as exc: + return jsonify({"error": str(exc)}), 503 + + findings = body.get("findings", []) + findings_text = _findings_to_text(findings) if findings else "Not provided." + + prompt = ( + "You are a cloud security advisor. Answer the question using ONLY the " + "grounded knowledge below. If the answer is not in the knowledge, say " + "so honestly. Reference specific rule IDs or controls where relevant." + f"\n\nGROUNDED KNOWLEDGE:\n{context}\n\n" + f"CURRENT FINDINGS:\n{findings_text}\n\nQUESTION: {question}" + ) + try: + answer = get_completion( + body["provider"], body["api_key"], prompt, model=body.get("model") + ) + except ValueError as exc: + return jsonify({"error": str(exc)}), 400 + except RuntimeError as exc: + return jsonify({"error": str(exc)}), 502 + + return jsonify({ + "answer": answer, + "sources": sources, + "provider": body["provider"], + "model": body.get("model"), + }) From fd822fb1a83bd0e0f1ba28937f62d349367811cd Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 01:54:48 +0100 Subject: [PATCH 04/16] Implement knowledge retrieval from vector store This module retrieves relevant knowledge from the OpenShield vector store for retrieval-augmented generation (RAG). It includes error handling for missing vector stores and the chromadb dependency. --- ai/retriever.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 ai/retriever.py diff --git a/ai/retriever.py b/ai/retriever.py new file mode 100644 index 00000000..5e97363e --- /dev/null +++ b/ai/retriever.py @@ -0,0 +1,52 @@ +"""Retrieve relevant OpenShield knowledge from the vector store for RAG.""" + +import logging +from pathlib import Path + +try: + import chromadb +except ImportError: + chromadb = None + +logger = logging.getLogger(__name__) + +REPO_ROOT = Path(__file__).resolve().parent.parent +VECTORSTORE_DIR = REPO_ROOT / "ai" / "vectorstore" +COLLECTION_NAME = "openshield" + + +class VectorStoreNotBuilt(RuntimeError): + """Raised when the vector store is missing or chromadb is unavailable.""" + + +def _get_collection(): + if chromadb is None: + raise VectorStoreNotBuilt( + "chromadb is not installed. Install it with 'pip install chromadb'." + ) + if not VECTORSTORE_DIR.exists(): + raise VectorStoreNotBuilt( + "Vector store not found. Run 'python ai/embed.py' first." + ) + client = chromadb.PersistentClient(path=str(VECTORSTORE_DIR)) + try: + return client.get_collection(COLLECTION_NAME) + except Exception as exc: + raise VectorStoreNotBuilt( + "Vector store collection missing. Run 'python ai/embed.py' first." + ) from exc + + +def retrieve(query, n_results=5): + """Return the most relevant knowledge chunks for a query. + + Each result is a dict with 'text' and 'source'. + """ + collection = _get_collection() + results = collection.query(query_texts=[query], n_results=n_results) + documents = results.get("documents", [[]])[0] + metadatas = results.get("metadatas", [[]])[0] + chunks = [] + for text, meta in zip(documents, metadatas): + chunks.append({"text": text, "source": (meta or {}).get("source", "")}) + return chunks From f7ebff1dbcf2875e7d5420c51e94adee3de5e6c9 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 01:57:23 +0100 Subject: [PATCH 05/16] Register AI blueprint in the Flask app --- api/app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/app.py b/api/app.py index 21ccb247..f11a1b95 100644 --- a/api/app.py +++ b/api/app.py @@ -9,6 +9,7 @@ from flask_cors import CORS from api.models.finding import DatabaseManager +from api.routes.ai import ai_bp load_dotenv() @@ -117,6 +118,7 @@ def verify_jwt() -> None: app.register_blueprint(scans_bp) app.register_blueprint(score_bp) app.register_blueprint(compliance_bp) + app.register_blueprint(ai_bp) # ------------------------------------------------------------------ # # Routes (public) # @@ -171,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 + ) From ddec6a4a3bbf19e4c2fbdf1c2e753511609a9637 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 01:57:54 +0100 Subject: [PATCH 06/16] Add chromadb version 0.4.24 to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 0e34c95f..f643453b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,3 +21,4 @@ cryptography==42.0.5 msrest==0.7.1 azure-mgmt-postgresqlflexibleservers==1.0.0b1 azure-keyvault-certificates==4.8.0 +chromadb==0.4.24 From 155d3eef9d9858ad2652154c5ec16b659f3b7423 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 02:32:47 +0100 Subject: [PATCH 07/16] feat: add Azure security skills into knowledge base for RAG embedding --- ai/knowledge/LICENSE | 201 +++++ .../SKILL.md | 80 ++ .../SKILL.md | 70 ++ .../SKILL.md | 268 +++++++ .../SKILL.md | 264 +++++++ .../SKILL.md | 317 ++++++++ .../SKILL.md | 232 ++++++ .../SKILL.md | 691 ++++++++++++++++++ 8 files changed, 2123 insertions(+) create mode 100644 ai/knowledge/LICENSE create mode 100644 ai/knowledge/skills/analyzing-azure-activity-logs-for-threats/SKILL.md create mode 100644 ai/knowledge/skills/analyzing-cloud-storage-access-patterns/SKILL.md create mode 100644 ai/knowledge/skills/auditing-azure-active-directory-configuration/SKILL.md create mode 100644 ai/knowledge/skills/auditing-cloud-with-cis-benchmarks/SKILL.md create mode 100644 ai/knowledge/skills/building-cloud-siem-with-sentinel/SKILL.md create mode 100644 ai/knowledge/skills/building-identity-federation-with-saml-azure-ad/SKILL.md create mode 100644 ai/knowledge/skills/building-identity-governance-lifecycle-process/SKILL.md diff --git a/ai/knowledge/LICENSE b/ai/knowledge/LICENSE new file mode 100644 index 00000000..d8851182 --- /dev/null +++ b/ai/knowledge/LICENSE @@ -0,0 +1,201 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please do not remove or change + the license header comment from a contributed file except when + necessary. + + Copyright 2026 mukul975 + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ai/knowledge/skills/analyzing-azure-activity-logs-for-threats/SKILL.md b/ai/knowledge/skills/analyzing-azure-activity-logs-for-threats/SKILL.md new file mode 100644 index 00000000..10e795be --- /dev/null +++ b/ai/knowledge/skills/analyzing-azure-activity-logs-for-threats/SKILL.md @@ -0,0 +1,80 @@ +--- +name: analyzing-azure-activity-logs-for-threats +description: 'Queries Azure Monitor activity logs and sign-in logs via azure-monitor-query to detect suspicious administrative + operations, impossible travel, privilege escalation, and resource modifications. Builds KQL queries for threat hunting in + Azure environments. Use when investigating suspicious Azure tenant activity or building cloud SIEM detections. + + ' +domain: cybersecurity +subdomain: security-operations +tags: +- azure +- cloud-security +- azure-monitor +- kql +- threat-hunting +- activity-logs +version: '1.0' +author: mahipal +license: Apache-2.0 +nist_csf: +- DE.CM-01 +- RS.MA-01 +- GV.OV-01 +- DE.AE-02 +--- + +# Analyzing Azure Activity Logs for Threats + + +## When to Use + +- When investigating security incidents that require analyzing azure activity logs for threats +- When building detection rules or threat hunting queries for this domain +- When SOC analysts need structured procedures for this analysis type +- When validating security monitoring coverage for related attack techniques + +## Prerequisites + +- Familiarity with security operations concepts and tools +- Access to a test or lab environment for safe execution +- Python 3.8+ with required dependencies installed +- Appropriate authorization for any testing activities + +## Instructions + +Use azure-monitor-query to execute KQL queries against Azure Log Analytics workspaces, +detecting suspicious admin operations and sign-in anomalies. + +```python +from azure.identity import DefaultAzureCredential +from azure.monitor.query import LogsQueryClient +from datetime import timedelta + +credential = DefaultAzureCredential() +client = LogsQueryClient(credential) + +response = client.query_workspace( + workspace_id="WORKSPACE_ID", + query="AzureActivity | where OperationNameValue has 'MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE' | take 10", + timespan=timedelta(hours=24), +) +``` + +Key detection queries: +1. Role assignment changes (privilege escalation) +2. Resource group and subscription modifications +3. Key vault secret access from new IPs +4. Network security group rule changes +5. Conditional access policy modifications + +## Examples + +```python +# Detect new Global Admin role assignments +query = ''' +AuditLogs +| where OperationName == "Add member to role" +| where TargetResources[0].modifiedProperties[0].newValue has "Global Administrator" +''' +``` diff --git a/ai/knowledge/skills/analyzing-cloud-storage-access-patterns/SKILL.md b/ai/knowledge/skills/analyzing-cloud-storage-access-patterns/SKILL.md new file mode 100644 index 00000000..a6149876 --- /dev/null +++ b/ai/knowledge/skills/analyzing-cloud-storage-access-patterns/SKILL.md @@ -0,0 +1,70 @@ +--- +name: analyzing-cloud-storage-access-patterns +description: Detect abnormal access patterns in AWS S3, GCS, and Azure Blob Storage by analyzing CloudTrail Data Events, GCS + audit logs, and Azure Storage Analytics. Identifies after-hours bulk downloads, access from new IP addresses, unusual API + calls (GetObject spikes), and potential data exfiltration using statistical baselines and time-series anomaly detection. +domain: cybersecurity +subdomain: cloud-security +tags: +- analyzing +- cloud +- storage +- access +version: '1.0' +author: mahipal +license: Apache-2.0 +atlas_techniques: +- AML.T0024 +- AML.T0056 +nist_ai_rmf: +- MEASURE-2.7 +- MAP-5.1 +- MANAGE-2.4 +nist_csf: +- PR.IR-01 +- ID.AM-08 +- GV.SC-06 +- DE.CM-01 +--- + + +# Analyzing Cloud Storage Access Patterns + + +## When to Use + +- When investigating security incidents that require analyzing cloud storage access patterns +- When building detection rules or threat hunting queries for this domain +- When SOC analysts need structured procedures for this analysis type +- When validating security monitoring coverage for related attack techniques + +## Prerequisites + +- Familiarity with cloud security concepts and tools +- Access to a test or lab environment for safe execution +- Python 3.8+ with required dependencies installed +- Appropriate authorization for any testing activities + +## Instructions + +1. Install dependencies: `pip install boto3 requests` +2. Query CloudTrail for S3 Data Events using AWS CLI or boto3. +3. Build access baselines: hourly request volume, per-user object counts, source IP history. +4. Detect anomalies: + - After-hours access (outside 8am-6pm local time) + - Bulk downloads: >100 GetObject calls from single principal in 1 hour + - New source IPs not seen in the prior 30 days + - ListBucket enumeration spikes (reconnaissance indicator) +5. Generate prioritized findings report. + +```bash +python scripts/agent.py --bucket my-sensitive-data --hours-back 24 --output s3_access_report.json +``` + +## Examples + +### CloudTrail S3 Data Event +```json +{"eventName": "GetObject", "requestParameters": {"bucketName": "sensitive-data", "key": "financials/q4.xlsx"}, + "sourceIPAddress": "203.0.113.50", "userIdentity": {"arn": "arn:aws:iam::123456789012:user/analyst"}} +``` diff --git a/ai/knowledge/skills/auditing-azure-active-directory-configuration/SKILL.md b/ai/knowledge/skills/auditing-azure-active-directory-configuration/SKILL.md new file mode 100644 index 00000000..77a2605c --- /dev/null +++ b/ai/knowledge/skills/auditing-azure-active-directory-configuration/SKILL.md @@ -0,0 +1,268 @@ +--- +name: auditing-azure-active-directory-configuration +description: 'Auditing Microsoft Entra ID (Azure Active Directory) configuration to identify risky authentication policies, + overly permissive role assignments, stale accounts, conditional access gaps, and guest user risks using AzureAD PowerShell, + Microsoft Graph API, and ScoutSuite. + + ' +domain: cybersecurity +subdomain: cloud-security +tags: +- cloud-security +- azure +- entra-id +- active-directory +- iam-audit +- conditional-access +version: '1.0' +author: mahipal +license: Apache-2.0 +nist_csf: +- PR.IR-01 +- ID.AM-08 +- GV.SC-06 +- DE.CM-01 +--- + +# Auditing Azure Active Directory Configuration + +## When to Use + +- When performing a security assessment of an Azure tenant's identity configuration +- When compliance audits require review of authentication policies, MFA enforcement, and role assignments +- When onboarding a new Azure tenant after merger or acquisition +- When investigating suspicious sign-in activity or compromised accounts +- When validating conditional access policies adequately protect against identity-based attacks + +**Do not use** for on-premises Active Directory auditing (use PingCastle or BloodHound AD), for Azure resource-level RBAC auditing without identity context, or for real-time threat detection (use Microsoft Defender for Identity). + +## Prerequisites + +- Global Reader or Security Reader role in the target Microsoft Entra ID tenant +- Microsoft Graph PowerShell SDK installed (`Install-Module Microsoft.Graph`) +- Az CLI authenticated to the target tenant (`az login --tenant TENANT_ID`) +- ScoutSuite with Azure provider configured for automated assessment +- Access to Azure AD audit logs and sign-in logs (requires Azure AD Premium P1/P2) + +## Workflow + +### Step 1: Enumerate Tenant Configuration and Security Defaults + +Assess the tenant's baseline identity security settings including security defaults and legacy authentication status. + +```powershell +# Connect to Microsoft Graph +Connect-MgGraph -Scopes "Directory.Read.All","Policy.Read.All","AuditLog.Read.All" + +# Get tenant details +Get-MgOrganization | Select-Object DisplayName, Id, VerifiedDomains + +# Check if Security Defaults are enabled +Get-MgPolicyIdentitySecurityDefaultEnforcementPolicy | Select-Object IsEnabled + +# List authentication methods policies +Get-MgPolicyAuthenticationMethodPolicy | ConvertTo-Json -Depth 5 + +# Check legacy authentication status via Conditional Access +Get-MgIdentityConditionalAccessPolicy | Where-Object { + $_.Conditions.ClientAppTypes -contains "exchangeActiveSync" -or + $_.Conditions.ClientAppTypes -contains "other" +} | Select-Object DisplayName, State +``` + +### Step 2: Audit Privileged Role Assignments + +Review directory role assignments to identify over-privileged users, permanent admin accounts, and risky role configurations. + +```bash +# List all Global Administrator assignments +az rest --method GET \ + --url "https://graph.microsoft.com/v1.0/directoryRoles/filterByIds" \ + --body '{"ids":["62e90394-69f5-4237-9190-012177145e10"]}' | \ + az rest --method GET \ + --url "https://graph.microsoft.com/v1.0/directoryRoles?filter=displayName eq 'Global Administrator'" \ + --query "value[0].id" -o tsv + +# List all privileged role assignments using Graph API +az rest --method GET \ + --url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?\$expand=principal" \ + --query "value[*].{Role:roleDefinitionId, Principal:principal.displayName, PrincipalType:principal.@odata.type}" \ + -o table + +# Check for users with multiple admin roles +az ad user list --query "[].{UPN:userPrincipalName, DisplayName:displayName}" -o table + +# List service principals with admin role assignments +az rest --method GET \ + --url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?\$filter=principalOrganizationId eq 'TENANT_ID'" \ + -o json +``` + +### Step 3: Review Conditional Access Policies + +Audit conditional access policies for coverage gaps, particularly around MFA enforcement, device compliance, and location-based restrictions. + +```powershell +# List all Conditional Access policies +Get-MgIdentityConditionalAccessPolicy | Select-Object DisplayName, State, @{ + N='GrantControls'; E={$_.GrantControls.BuiltInControls -join ', '} +} | Format-Table -AutoSize + +# Identify policies in report-only mode (not enforced) +Get-MgIdentityConditionalAccessPolicy | Where-Object {$_.State -eq "enabledForReportingButNotEnforced"} | + Select-Object DisplayName + +# Check MFA enforcement coverage +Get-MgIdentityConditionalAccessPolicy | Where-Object { + $_.GrantControls.BuiltInControls -contains "mfa" +} | Select-Object DisplayName, State, @{ + N='Users'; E={$_.Conditions.Users.IncludeUsers -join ', '} +} + +# Find policies that exclude groups (potential bypass) +Get-MgIdentityConditionalAccessPolicy | Where-Object { + $_.Conditions.Users.ExcludeGroups.Count -gt 0 +} | Select-Object DisplayName, @{ + N='ExcludedGroups'; E={$_.Conditions.Users.ExcludeGroups -join ', '} +} +``` + +### Step 4: Identify Stale Accounts and Guest Users + +Find accounts that have not signed in recently, disabled accounts with active role assignments, and risky guest user configurations. + +```bash +# Find users who haven't signed in for 90+ days +az ad user list --query "[?signInActivity.lastSignInDateTime < '2025-11-25T00:00:00Z'].{UPN:userPrincipalName, LastSignIn:signInActivity.lastSignInDateTime, Enabled:accountEnabled}" -o table + +# List all guest users +az ad user list --filter "userType eq 'Guest'" \ + --query "[].{UPN:userPrincipalName, DisplayName:displayName, CreatedDate:createdDateTime}" \ + -o table + +# Find guest users with privileged roles +az rest --method GET \ + --url "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments?\$expand=principal" \ + --query "value[?principal.userType=='Guest'].{Role:roleDefinitionId,Guest:principal.userPrincipalName}" \ + -o table + +# Check for accounts with disabled MFA +az rest --method GET \ + --url "https://graph.microsoft.com/v1.0/reports/authenticationMethods/userRegistrationDetails" \ + --query "value[?!isMfaRegistered].{UPN:userPrincipalName,MfaRegistered:isMfaRegistered}" \ + -o table +``` + +### Step 5: Analyze Sign-In Logs for Risky Activity + +Review sign-in logs to identify anomalous authentication patterns, failed MFA challenges, and risky sign-in detections. + +```bash +# Get risky sign-ins from last 7 days +az rest --method GET \ + --url "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=riskLevelDuringSignIn ne 'none' and createdDateTime ge 2026-02-16T00:00:00Z" \ + --query "value[*].{User:userPrincipalName,Risk:riskLevelDuringSignIn,IP:ipAddress,App:appDisplayName,Status:status.errorCode}" \ + -o table + +# Get sign-ins from unfamiliar locations +az rest --method GET \ + --url "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=riskEventTypes_v2/any(r:r eq 'unfamiliarFeatures')" \ + --query "value[*].{User:userPrincipalName,Location:location.city,IP:ipAddress}" \ + -o table + +# Check for legacy authentication sign-ins +az rest --method GET \ + --url "https://graph.microsoft.com/v1.0/auditLogs/signIns?\$filter=clientAppUsed ne 'Browser' and clientAppUsed ne 'Mobile Apps and Desktop clients'" \ + --query "value[*].{User:userPrincipalName,ClientApp:clientAppUsed,Status:status.errorCode}" \ + -o table +``` + +### Step 6: Run ScoutSuite Automated Assessment + +Execute ScoutSuite for comprehensive automated checks across the Azure tenant configuration. + +```bash +# Run ScoutSuite against Azure +python3 -m ScoutSuite azure --cli \ + --report-dir ./scoutsuite-azure-report \ + --all-subscriptions + +# Review the generated HTML report +open ./scoutsuite-azure-report/azure-report.html +``` + +## Key Concepts + +| Term | Definition | +|------|------------| +| Microsoft Entra ID | Microsoft's cloud identity and access management service, formerly Azure Active Directory, providing authentication and authorization | +| Conditional Access | Policy engine that evaluates signals (user, device, location, risk) to enforce access controls like MFA, device compliance, or block access | +| Security Defaults | Microsoft's baseline identity protection settings that enforce MFA registration, block legacy auth, and protect privileged actions | +| Privileged Identity Management | Azure AD Premium P2 feature enabling just-in-time privileged access with approval workflows and time-bound role activation | +| Legacy Authentication | Older authentication protocols (POP3, IMAP, SMTP, ActiveSync) that do not support MFA and are commonly exploited for credential attacks | +| Risky Sign-In | Microsoft Entra Identity Protection detection of sign-in anomalies including impossible travel, unfamiliar locations, and malware-linked IPs | + +## Tools & Systems + +- **Microsoft Graph API**: Primary programmatic interface for querying Entra ID configuration, policies, roles, and audit logs +- **Microsoft Graph PowerShell SDK**: PowerShell module for Entra ID management and security auditing tasks +- **ScoutSuite**: Multi-cloud auditing tool with Azure provider support for IAM, storage, networking, and identity checks +- **AzureADRecon**: Community tool for comprehensive Azure AD reconnaissance and security assessment reporting +- **Microsoft Defender for Identity**: Cloud-based security solution for detecting identity-based threats and compromised credentials + +## Common Scenarios + +### Scenario: Post-Acquisition Azure Tenant Security Assessment + +**Context**: After acquiring a company, the security team needs to assess the Azure tenant identity posture before integrating it with the corporate Entra ID. + +**Approach**: +1. Enumerate all Global Administrators and check for personal accounts in admin roles +2. Review conditional access policies to verify MFA is enforced for all users, not just admins +3. Identify guest users with privileged access that may indicate third-party vendor over-permissioning +4. Check for stale accounts (no sign-in for 90+ days) that could be targets for credential attacks +5. Review sign-in logs for legacy authentication usage that bypasses MFA +6. Verify Security Defaults or equivalent CA policies block legacy auth protocols +7. Produce a risk report with prioritized remediation steps before tenant integration + +**Pitfalls**: Azure AD Premium P2 is required for risky sign-in detections and PIM. If the acquired tenant uses a lower license tier, many identity protection features will be unavailable. Guest users from partner tenants may have implicit access through dynamic groups that are not visible in standard role assignment queries. + +## Output Format + +``` +Azure Active Directory Security Audit Report +=============================================== +Tenant: acme-acquired.onmicrosoft.com +Tenant ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890 +Audit Date: 2026-02-23 +License: Azure AD Premium P2 + +IDENTITY CONFIGURATION: + Security Defaults: Disabled (Conditional Access in use) + Conditional Access Policies: 12 (8 enforced, 3 report-only, 1 disabled) + Legacy Auth Blocked: Partial (blocked for admins only) + +PRIVILEGED ACCESS: + Global Administrators: 8 (recommended: <= 4) + Permanent admin assignments: 6 (no PIM activation required) + Service principals with admin: 3 + Guest users with privileged roles: 2 + +ACCOUNT HYGIENE: + Total users: 1,247 + Stale accounts (90+ days): 89 + Guest users: 234 + Users without MFA registered: 156 + +SIGN-IN RISK: + Risky sign-ins (last 30 days): 34 + Legacy auth sign-ins (last 7 days): 67 + Impossible travel detections: 5 + Unfamiliar location sign-ins: 12 + +CRITICAL FINDINGS: + 1. 8 Global Administrators with permanent assignments (use PIM) + 2. Legacy authentication not blocked for non-admin users + 3. 156 users without MFA registration + 4. 2 guest users with Privileged Role Administrator role +``` diff --git a/ai/knowledge/skills/auditing-cloud-with-cis-benchmarks/SKILL.md b/ai/knowledge/skills/auditing-cloud-with-cis-benchmarks/SKILL.md new file mode 100644 index 00000000..a333f28a --- /dev/null +++ b/ai/knowledge/skills/auditing-cloud-with-cis-benchmarks/SKILL.md @@ -0,0 +1,264 @@ +--- +name: auditing-cloud-with-cis-benchmarks +description: 'This skill details how to conduct cloud security audits using Center for Internet Security benchmarks for AWS, + Azure, and GCP. It covers interpreting CIS Foundations Benchmark controls, running automated assessments with tools like + Prowler and ScoutSuite, remediating failed controls, and maintaining continuous compliance monitoring against CIS v5 for + AWS, v4 for Azure, and v4 for GCP. + + ' +domain: cybersecurity +subdomain: cloud-security +tags: +- cis-benchmarks +- cloud-audit +- compliance-assessment +- prowler +- security-hardening +version: 1.0.0 +author: mahipal +license: Apache-2.0 +nist_ai_rmf: +- GOVERN-1.1 +- GOVERN-4.2 +- MAP-2.3 +nist_csf: +- PR.IR-01 +- ID.AM-08 +- GV.SC-06 +- DE.CM-01 +--- + +# Auditing Cloud with CIS Benchmarks + +## When to Use + +- When performing initial security audits of cloud environments against industry-standard benchmarks +- When preparing for SOC 2, ISO 27001, or regulatory audits that reference CIS controls +- When establishing a measurable security baseline for new cloud accounts or subscriptions +- When tracking compliance improvement over time with periodic reassessment +- When evaluating the security posture of acquired or inherited cloud environments + +**Do not use** for runtime threat detection (see detecting-cloud-threats-with-guardduty), for application-level security testing (see conducting-cloud-penetration-testing), or for compliance frameworks not based on CIS (refer to specific regulatory skill files). + +## Prerequisites + +- Read-only access to target cloud accounts (AWS SecurityAudit policy, Azure Reader role, GCP Viewer role) +- Prowler, ScoutSuite, or cloud-native CSPM tools installed and configured +- Understanding of CIS benchmark structure: sections, controls, profiles (Level 1 and Level 2) +- Remediation access for implementing fixes (separate from audit credentials) + +## Workflow + +### Step 1: Select Appropriate CIS Benchmark Version + +Choose the correct benchmark version for each cloud provider. Current versions as of 2025 include CIS AWS Foundations Benchmark v5.0, CIS Azure Foundations Benchmark v4.0, and CIS GCP Foundations Benchmark v4.0. + +``` +CIS Benchmark Coverage Areas: ++-------------------+-------------------------+------------------------+ +| Section | AWS v5.0 | Azure v4.0 | ++-------------------+-------------------------+------------------------+ +| Identity & Access | IAM policies, MFA, root | Azure AD, RBAC, PIM | +| Logging | CloudTrail, Config | Activity Log, Diag | +| Monitoring | CloudWatch alarms | Defender, Sentinel | +| Networking | VPC, SG, NACLs | NSG, ASG, Firewall | +| Storage | S3 encryption, access | Storage encryption | +| Database | RDS encryption | SQL TDE, auditing | ++-------------------+-------------------------+------------------------+ + +CIS Profile Levels: + Level 1: Practical security settings that can be implemented without significant + performance impact or reduced functionality + Level 2: Defense-in-depth settings that may reduce functionality or require + additional planning for implementation +``` + +### Step 2: Run Automated Assessment with Prowler + +Execute comprehensive CIS benchmark scans using Prowler for automated control evaluation across AWS, Azure, and GCP. + +```bash +# AWS CIS v5.0 assessment +prowler aws \ + --compliance cis_5.0_aws \ + --profile audit-account \ + --output-formats json-ocsf,html,csv \ + --output-directory ./cis-audit-$(date +%Y%m%d) + +# Azure CIS v4.0 assessment +prowler azure \ + --compliance cis_4.0_azure \ + --subscription-ids "sub-id-1,sub-id-2" \ + --output-formats json-ocsf,html,csv \ + --output-directory ./cis-audit-azure-$(date +%Y%m%d) + +# GCP CIS v4.0 assessment +prowler gcp \ + --compliance cis_4.0_gcp \ + --project-ids "project-1,project-2" \ + --output-formats json-ocsf,html,csv \ + --output-directory ./cis-audit-gcp-$(date +%Y%m%d) + +# Multi-account AWS scan using ScoutSuite +scout suite aws \ + --profile audit-account \ + --report-dir ./scout-report \ + --ruleset cis-5.0 \ + --force +``` + +### Step 3: Interpret Results and Prioritize Remediation + +Analyze audit results by section and severity. Prioritize Level 1 controls first as they represent fundamental security hygiene, then address Level 2 controls for defense in depth. + +```bash +# Parse Prowler results for failed controls +cat ./cis-audit-*/prowler-output-*.json | \ + jq '[.[] | select(.StatusExtended == "FAIL")] | group_by(.CheckID) | + map({control: .[0].CheckID, description: .[0].CheckTitle, + failed_resources: length, severity: .[0].Severity}) | + sort_by(-.failed_resources)' + +# Generate compliance score by section +cat ./cis-audit-*/prowler-output-*.json | \ + jq 'group_by(.Section) | map({ + section: .[0].Section, + total: length, + passed: [.[] | select(.StatusExtended == "PASS")] | length, + failed: [.[] | select(.StatusExtended == "FAIL")] | length, + score: (([.[] | select(.StatusExtended == "PASS")] | length) / length * 100 | round) + })' +``` + +### Step 4: Remediate Critical and High Controls + +Address failed controls starting with the highest impact items. Use AWS Config remediation, Azure Policy, or Terraform to apply fixes systematically. + +```bash +# CIS 1.4: Ensure no root account access key exists +aws iam list-access-keys --user-name root +# If keys exist, delete them +aws iam delete-access-key --user-name root --access-key-id AKIAEXAMPLE + +# CIS 2.1.1: Ensure S3 bucket default encryption is enabled +for bucket in $(aws s3api list-buckets --query 'Buckets[*].Name' --output text); do + aws s3api put-bucket-encryption --bucket "$bucket" \ + --server-side-encryption-configuration '{ + "Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}] + }' 2>/dev/null && echo "Encrypted: $bucket" || echo "FAILED: $bucket" +done + +# CIS 3.1: Ensure CloudTrail is enabled in all regions +aws cloudtrail create-trail \ + --name organization-trail \ + --s3-bucket-name cloudtrail-logs-bucket \ + --is-multi-region-trail \ + --enable-log-file-validation \ + --kms-key-id arn:aws:kms:us-east-1:123456789012:key/key-id + +aws cloudtrail start-logging --name organization-trail + +# CIS 4.x: Configure CloudWatch metric filters and alarms +aws logs put-metric-filter \ + --log-group-name CloudTrail/DefaultLogGroup \ + --filter-name UnauthorizedAPICalls \ + --filter-pattern '{ ($.errorCode = "*UnauthorizedAccess*") || ($.errorCode = "AccessDenied*") }' \ + --metric-transformations metricName=UnauthorizedAPICalls,metricNamespace=CISBenchmark,metricValue=1 +``` + +### Step 5: Establish Continuous Compliance Monitoring + +Deploy automated compliance monitoring to detect configuration drift between periodic audits. Use AWS Security Hub, Azure Policy, or GCP Security Command Center. + +```bash +# AWS: Enable CIS v5.0 in Security Hub +aws securityhub batch-enable-standards \ + --standards-subscription-requests '[ + {"StandardsArn": "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/5.0.0"} + ]' + +# Azure: Assign CIS benchmark policy initiative +az policy assignment create \ + --name cis-azure-benchmark \ + --scope "/subscriptions/" \ + --policy-set-definition "1a5bb27d-173f-493e-9568-eb56638dbd0e" \ + --params '{"effect": {"value": "AuditIfNotExists"}}' + +# Schedule periodic Prowler assessments +# Run weekly via cron or CI/CD pipeline +0 2 * * 1 prowler aws --compliance cis_5.0_aws --output-formats csv --output-directory /opt/audits/weekly-$(date +\%Y\%m\%d) +``` + +## Key Concepts + +| Term | Definition | +|------|------------| +| CIS Benchmark | Prescriptive security configuration guidelines developed by the Center for Internet Security through community consensus | +| Level 1 Profile | Practical security controls implementable without significant performance or functionality impact, representing security hygiene | +| Level 2 Profile | Defense-in-depth controls that may restrict functionality and require careful planning before implementation | +| Foundations Benchmark | CIS benchmark specifically for cloud providers covering IAM, logging, monitoring, networking, and storage security | +| Control ID | Unique numerical identifier for each CIS recommendation (e.g., 1.4 for root access key checks, 2.1.1 for S3 encryption) | +| Compliance Score | Percentage of CIS controls in a passing state, tracked over time to measure security posture improvement | +| Automated Assessment | Tool-driven evaluation of CIS controls using cloud provider APIs to check resource configurations against benchmark requirements | +| Remediation Runbook | Documented step-by-step procedure for fixing a specific failed CIS control, including pre-checks and validation | + +## Tools & Systems + +- **Prowler**: Open-source cloud security tool performing 300+ checks including CIS benchmark assessments for AWS, Azure, and GCP +- **ScoutSuite**: Multi-cloud security auditing tool with CIS benchmark rule sets generating HTML reports +- **AWS Security Hub**: Native AWS service supporting CIS AWS Foundations Benchmark as a security standard +- **Azure Policy**: Governance service with built-in CIS benchmark policy initiatives for automated compliance monitoring +- **GCP Security Command Center**: Native GCP service evaluating configurations against CIS GCP Foundations Benchmark + +## Common Scenarios + +### Scenario: Pre-Audit CIS Assessment for SOC 2 Certification + +**Context**: A SaaS company pursuing SOC 2 Type II certification needs to demonstrate cloud security controls aligned to CIS benchmarks. The auditor requires evidence of continuous compliance monitoring across 45 AWS accounts. + +**Approach**: +1. Run Prowler CIS v5.0 assessment across all 45 accounts to establish the baseline compliance score +2. Export results to CSV and categorize failures by section (IAM, Logging, Monitoring, Networking) +3. Map each CIS control to the relevant SOC 2 Trust Services Criteria (CC6.1, CC6.6, CC7.1, etc.) +4. Remediate all Level 1 control failures within 30 days and Level 2 within 60 days +5. Enable CIS v5.0 in AWS Security Hub for continuous monitoring and automated drift detection +6. Generate weekly compliance reports showing improvement trajectory for the auditor +7. Document exceptions for controls intentionally not implemented with risk acceptance justification + +**Pitfalls**: Remediating controls without testing in a staging environment first can break production workloads. Ignoring Level 2 controls entirely weakens the audit narrative even if they are not strictly required. + +## Output Format + +``` +CIS Benchmark Audit Report +============================ +Cloud Provider: AWS +Benchmark Version: CIS AWS Foundations Benchmark v5.0 +Accounts Assessed: 45 +Assessment Date: 2025-02-23 +Tool: Prowler v4.3.0 + +OVERALL COMPLIANCE SCORE: 74% + +COMPLIANCE BY SECTION: + 1. Identity and Access Management: 68% (41/60 controls passed) + 2. Storage: 82% (28/34 controls passed) + 3. Logging: 91% (20/22 controls passed) + 4. Monitoring: 55% (18/33 controls passed) + 5. Networking: 78% (32/41 controls passed) + +TOP FAILED CONTROLS (by affected accounts): + [1.4] Root account has active access keys - 3/45 accounts + [1.5] MFA not enabled for root account - 2/45 accounts + [2.1.1] S3 default encryption not enabled - 12/45 accounts + [3.1] CloudTrail not multi-region - 8/45 accounts + [4.3] No alarm for root account usage - 28/45 accounts + [5.1] VPC flow logs not enabled - 15/45 accounts + [5.4] Security groups allow 0.0.0.0/0 ingress - 22/45 accounts + +REMEDIATION PRIORITY: + Critical (Fix within 7 days): Root access keys, missing root MFA + High (Fix within 30 days): S3 encryption, CloudTrail, VPC flow logs + Medium (Fix within 60 days): CloudWatch alarms, security group restrictions + Low (Fix within 90 days): Level 2 controls, informational items +``` diff --git a/ai/knowledge/skills/building-cloud-siem-with-sentinel/SKILL.md b/ai/knowledge/skills/building-cloud-siem-with-sentinel/SKILL.md new file mode 100644 index 00000000..878c242b --- /dev/null +++ b/ai/knowledge/skills/building-cloud-siem-with-sentinel/SKILL.md @@ -0,0 +1,317 @@ +--- +name: building-cloud-siem-with-sentinel +description: 'This skill covers deploying Microsoft Sentinel as a cloud-native SIEM and SOAR platform for centralized security + operations. It details configuring data connectors for multi-cloud log ingestion, writing KQL detection queries, building + automated response playbooks with Logic Apps, and leveraging the Sentinel data lake for petabyte-scale threat hunting across + AWS, Azure, and GCP security telemetry. + + ' +domain: cybersecurity +subdomain: cloud-security +tags: +- microsoft-sentinel +- cloud-siem +- kql-queries +- soar-automation +- threat-detection +version: 1.0.0 +author: mahipal +license: Apache-2.0 +nist_ai_rmf: +- MEASURE-2.7 +- MAP-5.1 +- MANAGE-2.4 +atlas_techniques: +- AML.T0070 +- AML.T0066 +- AML.T0082 +nist_csf: +- PR.IR-01 +- ID.AM-08 +- GV.SC-06 +- DE.CM-01 +--- + +# Building Cloud SIEM with Sentinel + +## When to Use + +- When establishing a centralized security operations center for multi-cloud environments +- When migrating from legacy SIEM platforms (Splunk, QRadar) to cloud-native architecture +- When building automated incident response workflows for cloud-specific threats +- When performing large-scale threat hunting across petabytes of security telemetry +- When integrating threat intelligence feeds with cloud security log analysis + +**Do not use** for AWS-only environments where Security Hub and GuardDuty suffice, for endpoint detection requiring EDR capabilities (use Defender for Endpoint), or for compliance posture monitoring (see building-cloud-security-posture-management). + +## Prerequisites + +- Azure subscription with Microsoft Sentinel enabled on a Log Analytics workspace +- Data connector permissions for target log sources (AWS CloudTrail, Azure Activity, GCP) +- Logic Apps or Azure Functions for automated response playbooks +- KQL (Kusto Query Language) proficiency for writing detection rules and hunting queries + +## Workflow + +### Step 1: Provision Sentinel Workspace and Data Connectors + +Create a Log Analytics workspace optimized for security data and enable data connectors for multi-cloud ingestion. + +```powershell +# Create Log Analytics workspace +az monitor log-analytics workspace create \ + --resource-group security-rg \ + --workspace-name sentinel-workspace \ + --location eastus \ + --retention-time 365 \ + --sku PerGB2018 + +# Enable Microsoft Sentinel on the workspace +az sentinel onboarding-state create \ + --resource-group security-rg \ + --workspace-name sentinel-workspace + +# Enable AWS CloudTrail connector +az sentinel data-connector create \ + --resource-group security-rg \ + --workspace-name sentinel-workspace \ + --data-connector-id aws-cloudtrail \ + --kind AmazonWebServicesCloudTrail \ + --aws-cloud-trail-data-connector '{ + "awsRoleArn": "arn:aws:iam::123456789012:role/SentinelCloudTrailRole", + "dataTypes": {"logs": {"state": "Enabled"}} + }' + +# Enable Azure AD sign-in and audit logs +az sentinel data-connector create \ + --resource-group security-rg \ + --workspace-name sentinel-workspace \ + --data-connector-id azure-ad \ + --kind AzureActiveDirectory \ + --azure-active-directory '{ + "dataTypes": { + "alerts": {"state": "Enabled"}, + "signinLogs": {"state": "Enabled"}, + "auditLogs": {"state": "Enabled"} + } + }' +``` + +### Step 2: Write KQL Detection Rules + +Create analytics rules using Kusto Query Language to detect cloud-specific threats. Map each rule to MITRE ATT&CK techniques. + +```kql +// Detect impossible travel - sign-ins from geographically distant locations +let timeframe = 1h; +let distance_threshold = 500; // km +SigninLogs +| where TimeGenerated > ago(timeframe) +| where ResultType == 0 // Successful sign-ins only +| project TimeGenerated, UserPrincipalName, IPAddress, Location, + Latitude = toreal(LocationDetails.geoCoordinates.latitude), + Longitude = toreal(LocationDetails.geoCoordinates.longitude) +| sort by UserPrincipalName asc, TimeGenerated asc +| extend PrevLatitude = prev(Latitude, 1), PrevLongitude = prev(Longitude, 1), + PrevTime = prev(TimeGenerated, 1), PrevUser = prev(UserPrincipalName, 1) +| where UserPrincipalName == PrevUser +| extend TimeDiff = datetime_diff('minute', TimeGenerated, PrevTime) +| where TimeDiff < 60 +| extend Distance = geo_distance_2points(Longitude, Latitude, PrevLongitude, PrevLatitude) / 1000 +| where Distance > distance_threshold +| project TimeGenerated, UserPrincipalName, IPAddress, Location, Distance, TimeDiff +``` + +```kql +// Detect AWS IAM credential abuse from CloudTrail +AWSCloudTrail +| where TimeGenerated > ago(24h) +| where EventName in ("ConsoleLogin", "AssumeRole", "GetSessionToken") +| where ErrorCode == "" +| summarize LoginCount = count(), DistinctIPs = dcount(SourceIpAddress), + IPList = make_set(SourceIpAddress, 10) + by UserIdentityArn, bin(TimeGenerated, 1h) +| where DistinctIPs > 3 +| project TimeGenerated, UserIdentityArn, LoginCount, DistinctIPs, IPList +``` + +```kql +// Detect mass S3 object deletion (potential ransomware) +AWSCloudTrail +| where TimeGenerated > ago(1h) +| where EventName == "DeleteObject" or EventName == "DeleteObjects" +| summarize DeleteCount = count(), BucketsAffected = dcount(RequestParameters_bucketName) + by UserIdentityArn, bin(TimeGenerated, 10m) +| where DeleteCount > 100 +| project TimeGenerated, UserIdentityArn, DeleteCount, BucketsAffected +``` + +### Step 3: Build SOAR Playbooks with Logic Apps + +Create automated response playbooks that execute when analytics rules trigger incidents. Common actions include blocking users, isolating resources, and enriching alerts with threat intelligence. + +```json +{ + "definition": { + "triggers": { + "Microsoft_Sentinel_incident": { + "type": "ApiConnectionWebhook", + "inputs": { + "body": {"incidentArmId": "subscriptions/@{triggerBody()?['workspaceInfo']?['SubscriptionId']}/resourceGroups/@{triggerBody()?['workspaceInfo']?['ResourceGroupName']}/providers/Microsoft.OperationalInsights/workspaces/@{triggerBody()?['workspaceInfo']?['WorkspaceName']}/providers/Microsoft.SecurityInsights/Incidents/@{triggerBody()?['object']?['properties']?['incidentNumber']}"}, + "host": {"connection": {"name": "@parameters('$connections')['microsoftsentinel']['connectionId']"}} + } + } + }, + "actions": { + "Get_incident_entities": { + "type": "ApiConnection", + "inputs": {"method": "post", "path": "/Incidents/entities"} + }, + "For_each_account_entity": { + "type": "Foreach", + "foreach": "@body('Get_incident_entities')?['Accounts']", + "actions": { + "Disable_Azure_AD_user": { + "type": "ApiConnection", + "inputs": { + "method": "PATCH", + "path": "/v1.0/users/@{items('For_each_account_entity')?['AadUserId']}", + "body": {"accountEnabled": false} + } + }, + "Add_comment_to_incident": { + "type": "ApiConnection", + "inputs": { + "body": {"message": "User @{items('For_each_account_entity')?['Name']} disabled by automated playbook"} + } + } + } + } + } + } +} +``` + +### Step 4: Configure Sentinel Data Lake for Long-Term Hunting + +Enable the Sentinel data lake for petabyte-scale log retention and advanced threat hunting using both KQL and SQL endpoints. + +```kql +// Threat hunting query: detect lateral movement across AWS accounts +let suspicious_roles = AWSCloudTrail +| where TimeGenerated > ago(7d) +| where EventName == "AssumeRole" +| extend AssumedRoleArn = tostring(parse_json(RequestParameters).roleArn) +| where AssumedRoleArn contains "cross-account" or AssumedRoleArn contains "admin" +| summarize AssumeCount = count(), UniqueSourceAccounts = dcount(RecipientAccountId) + by UserIdentityArn, AssumedRoleArn +| where AssumeCount > 10 and UniqueSourceAccounts > 2; +suspicious_roles +| join kind=inner ( + AWSCloudTrail + | where TimeGenerated > ago(7d) + | where EventName in ("RunInstances", "CreateFunction", "PutBucketPolicy") +) on UserIdentityArn +| project TimeGenerated, UserIdentityArn, AssumedRoleArn, EventName, SourceIpAddress +``` + +### Step 5: Integrate Threat Intelligence + +Connect threat intelligence providers and create indicator-based matching rules to detect communication with known malicious infrastructure. + +```powershell +# Enable Microsoft Threat Intelligence connector +az sentinel data-connector create \ + --resource-group security-rg \ + --workspace-name sentinel-workspace \ + --data-connector-id microsoft-ti \ + --kind MicrosoftThreatIntelligence \ + --microsoft-threat-intelligence '{ + "dataTypes": {"microsoftEmergingThreatFeed": {"lookbackPeriod": "2025-01-01T00:00:00Z", "state": "Enabled"}} + }' +``` + +```kql +// Match network indicators against cloud flow logs +let TI_IPs = ThreatIntelligenceIndicator +| where TimeGenerated > ago(30d) +| where isnotempty(NetworkIP) +| distinct NetworkIP; +AzureNetworkAnalytics_CL +| where TimeGenerated > ago(24h) +| where DestIP_s in (TI_IPs) +| project TimeGenerated, SrcIP_s, DestIP_s, DestPort_d, FlowType_s +``` + +## Key Concepts + +| Term | Definition | +|------|------------| +| KQL | Kusto Query Language, the primary query language for Microsoft Sentinel used to search, analyze, and visualize security data | +| Analytics Rule | Detection logic in Sentinel that evaluates log data on a schedule and creates incidents when conditions match | +| SOAR Playbook | Automated workflow triggered by incidents that performs response actions such as blocking accounts, enriching alerts, or notifying teams | +| Data Connector | Integration module that ingests security logs from cloud services, identity providers, and third-party tools into Sentinel | +| Sentinel Data Lake | Petabyte-scale storage layer providing long-term log retention with KQL and SQL query interfaces for advanced hunting | +| Workbook | Interactive dashboard in Sentinel displaying visualizations of security data, trends, and operational metrics | +| Watchlist | Reference data tables in Sentinel used to enrich alerts with context such as VIP user lists or approved IP ranges | +| Fusion Detection | Machine learning-powered correlation engine that automatically detects multi-stage attacks across data sources | + +## Tools & Systems + +- **Microsoft Sentinel**: Cloud-native SIEM/SOAR platform built on Azure Log Analytics with AI-powered threat detection +- **Azure Logic Apps**: Low-code automation platform for building SOAR playbooks triggered by Sentinel incidents +- **Microsoft Threat Intelligence**: Integrated threat feeds providing IP, domain, and URL indicators for matching against security logs +- **Azure Data Explorer**: High-performance analytics engine underlying Sentinel KQL queries for large-scale data exploration +- **MITRE ATT&CK Navigator**: Framework for mapping Sentinel detection rules to adversary tactics and techniques + +## Common Scenarios + +### Scenario: Detecting Cross-Cloud Credential Theft Campaign + +**Context**: An attacker compromises an Azure AD account through phishing, then uses the account to access AWS resources via federated identity. Sentinel needs to correlate the Azure sign-in anomaly with unusual AWS API activity. + +**Approach**: +1. Create an analytics rule detecting Azure AD impossible travel or anomalous sign-in risk +2. Write a KQL query correlating the compromised Azure AD identity with AWS CloudTrail AssumeRoleWithSAML events +3. Build a Fusion detection rule that links Azure AD risk events with subsequent AWS privilege escalation activity +4. Deploy a SOAR playbook that automatically disables the Azure AD account and revokes AWS STS sessions +5. Create a workbook showing the timeline from initial compromise through lateral movement to AWS +6. Run a hunting query across the data lake to check for similar patterns affecting other accounts + +**Pitfalls**: Not correlating identity across cloud providers misses the full attack chain. Setting analytics rule frequency too low (e.g., 24 hours) allows attackers hours of undetected access. + +## Output Format + +``` +Microsoft Sentinel SOC Operations Report +========================================== +Workspace: sentinel-workspace +Data Sources: 14 connectors active +Report Period: 2025-02-01 to 2025-02-23 + +DATA INGESTION: + Azure AD Sign-in Logs: 2.3 TB (23 days) + AWS CloudTrail: 1.8 TB (23 days) + Azure Activity: 0.9 TB (23 days) + Defender for Cloud Alerts: 45 GB (23 days) + Total Ingestion: 5.1 TB + +DETECTION SUMMARY: + Active Analytics Rules: 87 + Incidents Created: 234 + Critical: 8 | High: 34 | Medium: 89 | Low: 103 + Mean Time to Detect (MTTD): 4.2 minutes + Mean Time to Respond (MTTR): 18 minutes + +TOP INCIDENT TYPES: + Impossible Travel Detected: 42 incidents + AWS Unauthorized API Call Pattern: 28 incidents + Mass File Deletion in S3: 3 incidents + Suspicious Azure AD App Registration: 12 incidents + +AUTOMATION: + Playbooks Executed: 156 + Accounts Auto-Disabled: 23 + Incidents Auto-Enriched: 198 + False Positive Rate: 12% +``` diff --git a/ai/knowledge/skills/building-identity-federation-with-saml-azure-ad/SKILL.md b/ai/knowledge/skills/building-identity-federation-with-saml-azure-ad/SKILL.md new file mode 100644 index 00000000..b5708454 --- /dev/null +++ b/ai/knowledge/skills/building-identity-federation-with-saml-azure-ad/SKILL.md @@ -0,0 +1,232 @@ +--- +name: building-identity-federation-with-saml-azure-ad +description: Establish SAML 2.0 identity federation between on-premises Active Directory and Azure AD (Microsoft Entra ID) + for seamless cross-domain authentication and SSO to cloud applications. +domain: cybersecurity +subdomain: identity-access-management +tags: +- saml +- azure-ad +- entra-id +- federation +- identity +- sso +- adfs +- hybrid-identity +version: '1.0' +author: mahipal +license: Apache-2.0 +nist_csf: +- PR.AA-01 +- PR.AA-02 +- PR.AA-05 +- PR.AA-06 +--- + +# Building Identity Federation with SAML Azure AD + +## Overview + +Identity federation enables users authenticated by one identity provider to access resources managed by another without maintaining separate credentials. This skill covers establishing SAML 2.0 federation between an organization's on-premises Active Directory (via AD FS or third-party IdP) and Microsoft Entra ID (formerly Azure AD), as well as configuring federated SSO for third-party SaaS applications. Federation eliminates password synchronization concerns and keeps authentication authority on-premises while extending SSO to cloud resources. + + +## When to Use + +- When deploying or configuring building identity federation with saml azure ad capabilities in your environment +- When establishing security controls aligned to compliance requirements +- When building or improving security architecture for this domain +- When conducting security assessments that require this implementation + +## Prerequisites + +- On-premises Active Directory domain +- AD FS 2019+ or third-party SAML IdP (Okta, Ping, etc.) +- Microsoft Entra ID tenant (P1 or P2 license recommended) +- Azure AD Connect (if using hybrid identity with password hash sync as backup) +- Public TLS certificate for federation endpoint +- DNS records for federation service name + +## Core Concepts + +### Federation Models + +| Model | Authentication Authority | Use Case | +|-------|------------------------|----------| +| Federated (AD FS) | On-premises AD FS | Regulatory requirement to keep auth on-prem | +| Managed (PHS) | Azure AD with password hash sync | Simplest cloud auth, AD FS not needed | +| Managed (PTA) | On-premises via pass-through agent | Cloud auth validated against on-prem AD | +| Third-Party Federation | External IdP (Okta, Ping) | Multi-IdP environment | + +### SAML Federation Architecture + +``` +User → Cloud App (SP) + │ + └── Redirect to Azure AD + │ + ├── Azure AD checks federated domain + │ + └── Redirect to on-premises AD FS + │ + ├── AD FS authenticates against Active Directory + │ + ├── AD FS issues SAML token + │ + └── Token posted back to Azure AD + │ + ├── Azure AD validates federation trust + │ + ├── Azure AD issues its own token + │ + └── User receives access token for cloud app +``` + +### Federation Trust Components + +| Component | Description | +|-----------|-------------| +| Token-Signing Certificate | X.509 certificate used by IdP to sign SAML assertions | +| Federation Metadata | XML document describing IdP endpoints and capabilities | +| Relying Party Trust | Configuration in AD FS for each SP (Azure AD) | +| Claims Rules | Transform AD attributes into SAML claims | +| Issuer URI | Unique identifier for the IdP (entity ID) | + +## Workflow + +### Step 1: Prepare AD FS Infrastructure + +```powershell +# Install AD FS role +Install-WindowsFeature ADFS-Federation -IncludeManagementTools + +# Configure AD FS farm +Install-AdfsFarm ` + -CertificateThumbprint $certThumbprint ` + -FederationServiceDisplayName "Corp Federation Service" ` + -FederationServiceName "fs.corp.example.com" ` + -ServiceAccountCredential $gmsaCredential + +# Verify AD FS is operational +Get-AdfsProperties | Select-Object HostName, Identifier, FederationPassiveAddress +``` + +### Step 2: Configure Azure AD Federated Domain + +```powershell +# Install Microsoft Graph PowerShell module +Install-Module Microsoft.Graph -Scope CurrentUser + +# Connect to Microsoft Graph +Connect-MgGraph -Scopes "Domain.ReadWrite.All" + +# Convert managed domain to federated +# Using AD FS federation metadata URL +$domainId = "corp.example.com" +$federationConfig = @{ + issuerUri = "http://fs.corp.example.com/adfs/services/trust" + metadataExchangeUri = "https://fs.corp.example.com/adfs/services/trust/mex" + passiveSignInUri = "https://fs.corp.example.com/adfs/ls/" + signOutUri = "https://fs.corp.example.com/adfs/ls/?wa=wsignout1.0" + signingCertificate = $base64Cert + preferredAuthenticationProtocol = "saml" +} + +# Apply federation settings to domain +New-MgDomainFederationConfiguration -DomainId $domainId -BodyParameter $federationConfig +``` + +### Step 3: Configure AD FS Claims Rules + +```powershell +# Add Relying Party Trust for Azure AD +Add-AdfsRelyingPartyTrust ` + -Name "Microsoft Office 365 Identity Platform" ` + -MetadataUrl "https://nexus.microsoftonline-p.com/federationmetadata/2007-06/federationmetadata.xml" + +# Configure claim rules +$rules = @" +@RuleTemplate = "LdapClaims" +@RuleName = "Extract AD Attributes" +c:[Type == "http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", + Issuer == "AD AUTHORITY"] +=> issue(store = "Active Directory", + types = ("http://schemas.xmlsoap.org/claims/UPN", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname", + "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"), + query = ";userPrincipalName,mail,givenName,sn;{0}", + param = c.Value); + +@RuleTemplate = "PassThroughClaims" +@RuleName = "Pass Through UPN as NameID" +c:[Type == "http://schemas.xmlsoap.org/claims/UPN"] +=> issue(Type = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", + Issuer = c.Issuer, OriginalIssuer = c.OriginalIssuer, + Value = c.Value, + ValueType = c.ValueType, + Properties["http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format"] + = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"); +"@ + +Set-AdfsRelyingPartyTrust ` + -TargetName "Microsoft Office 365 Identity Platform" ` + -IssuanceTransformRules $rules +``` + +### Step 4: Configure Third-Party SaaS Federation + +For each SaaS application that supports SAML SSO via Azure AD: + +1. Navigate to Microsoft Entra Admin Center > Enterprise Applications +2. Add the application from the gallery (or create custom SAML) +3. Configure Single Sign-On > SAML: + - Identifier (Entity ID): Application's entity ID + - Reply URL (ACS): Application's assertion consumer service URL + - Sign-on URL: Application's login URL +4. Map user attributes/claims: + - NameID: user.userprincipalname (email format) + - Additional claims as required by the application +5. Download the Federation Metadata XML or certificate +6. Configure the SaaS app with Azure AD's federation details + +### Step 5: Certificate Lifecycle Management + +AD FS token-signing certificates expire and must be renewed: + +```powershell +# Check current certificate expiration +Get-AdfsCertificate -CertificateType Token-Signing | Select-Object Thumbprint, NotAfter + +# AD FS supports auto-rollover (enabled by default) +Get-AdfsProperties | Select-Object AutoCertificateRollover + +# If manual rotation is needed: +# 1. Add new certificate as secondary +Set-AdfsCertificate -CertificateType Token-Signing -Thumbprint $newThumbprint -IsPrimary $false +# 2. Update Azure AD with new certificate +# 3. Promote to primary +Set-AdfsCertificate -CertificateType Token-Signing -Thumbprint $newThumbprint -IsPrimary $true +# 4. Remove old certificate +Remove-AdfsCertificate -CertificateType Token-Signing -Thumbprint $oldThumbprint +``` + +## Validation Checklist + +- [ ] AD FS farm operational with valid TLS and token-signing certificates +- [ ] Azure AD domain configured as federated with correct metadata +- [ ] Claims rules properly transform AD attributes to SAML assertions +- [ ] Test user can authenticate through federation flow end-to-end +- [ ] MFA enforced at AD FS or Azure AD conditional access level +- [ ] Certificate auto-rollover enabled or manual rotation scheduled +- [ ] Federation metadata endpoint publicly accessible +- [ ] Smart lockout configured to prevent brute force +- [ ] Extranet lockout policies configured on AD FS +- [ ] Monitoring configured for AD FS health and certificate expiry +- [ ] Disaster recovery: managed authentication fallback documented + +## References + +- [Microsoft Entra Federation Documentation](https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/whatis-fed) +- [AD FS Design Guide](https://learn.microsoft.com/en-us/windows-server/identity/ad-fs/design/ad-fs-design-guide) +- [Configure AD FS for Azure AD Federation](https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-fed-management) +- [SAML 2.0 Authentication - OASIS](https://docs.oasis-open.org/security/saml/v2.0/) diff --git a/ai/knowledge/skills/building-identity-governance-lifecycle-process/SKILL.md b/ai/knowledge/skills/building-identity-governance-lifecycle-process/SKILL.md new file mode 100644 index 00000000..9567cb7a --- /dev/null +++ b/ai/knowledge/skills/building-identity-governance-lifecycle-process/SKILL.md @@ -0,0 +1,691 @@ +--- +name: building-identity-governance-lifecycle-process +description: 'Builds comprehensive identity governance and lifecycle management processes including joiner-mover-leaver automation, + role mining, access request workflows, periodic recertification, and orphaned account remediation using IGA platforms. Activates + for requests involving identity lifecycle management, JML processes, role-based access provisioning, or identity governance + program design. + + ' +domain: cybersecurity +subdomain: identity-access-management +tags: +- identity-governance +- lifecycle-management +- JML +- access-provisioning +- RBAC +- IGA +version: '1.0' +author: mahipal +license: Apache-2.0 +nist_ai_rmf: +- GOVERN-1.1 +- GOVERN-1.7 +- MAP-1.1 +nist_csf: +- PR.AA-01 +- PR.AA-02 +- PR.AA-05 +- PR.AA-06 +--- + +# Building Identity Governance Lifecycle Process + +## When to Use + +- Organization lacks automated joiner-mover-leaver (JML) processes for identity management +- Access provisioning is manual and takes days, creating productivity loss and security gaps +- Former employees retain access to systems after termination (orphaned accounts) +- Role explosion has created thousands of roles with unclear ownership and overlapping entitlements +- Compliance requirements mandate documented identity lifecycle processes (SOX, HIPAA, GDPR) +- No centralized visibility into who has access to what across the enterprise + +**Do not use** for single-application user management; identity governance addresses cross-system lifecycle management requiring correlation of authoritative HR sources with downstream application provisioning. + +## Prerequisites + +- Authoritative HR system (Workday, SAP SuccessFactors, BambooHR) as identity source of truth +- IGA platform (SailPoint, Saviynt, One Identity) or Microsoft Entra ID Governance +- Active Directory and/or Azure AD as primary directory services +- Application connectors for target systems requiring automated provisioning +- Defined organizational role structure and reporting hierarchy +- Stakeholder buy-in from HR, IT, security, and business unit managers + +## Workflow + +### Step 1: Define Identity Lifecycle States and Transitions + +Map the identity lifecycle from hire to termination: + +```python +""" +Identity Lifecycle State Machine +Defines all identity states and valid transitions with automated actions. +""" + +IDENTITY_LIFECYCLE = { + "states": { + "PRE_HIRE": { + "description": "Identity created from HR feed before start date", + "automated_actions": [ + "Create identity record in IGA platform", + "Generate unique employee ID", + "Create mailbox reservation", + "Assign birthright roles based on job code", + "Initiate background check workflow" + ], + "valid_transitions": ["ACTIVE", "CANCELLED"] + }, + "ACTIVE": { + "description": "Employee has started, full access provisioned", + "automated_actions": [ + "Create Active Directory account", + "Create email mailbox", + "Provision birthright application access", + "Assign department-specific roles", + "Add to distribution groups", + "Issue MFA token/security key", + "Create VPN account if remote worker" + ], + "valid_transitions": ["ROLE_CHANGE", "LEAVE_OF_ABSENCE", "TERMINATED"] + }, + "ROLE_CHANGE": { + "description": "Employee transferred, promoted, or changed departments", + "automated_actions": [ + "Recalculate role assignments based on new job code", + "Remove access from previous department applications", + "Provision access for new department applications", + "Update group memberships", + "Transfer manager in directory", + "Trigger access review for retained entitlements", + "Notify new manager of inherited access" + ], + "valid_transitions": ["ACTIVE", "LEAVE_OF_ABSENCE", "TERMINATED"] + }, + "LEAVE_OF_ABSENCE": { + "description": "Employee on extended leave (medical, parental, sabbatical)", + "automated_actions": [ + "Disable interactive login (preserve account)", + "Suspend VPN access", + "Set out-of-office auto-reply", + "Delegate mailbox to manager", + "Preserve all role assignments for return", + "Set reactivation date from HR feed" + ], + "valid_transitions": ["ACTIVE", "TERMINATED"] + }, + "TERMINATED": { + "description": "Employee has left the organization", + "automated_actions": [ + "Disable AD account immediately", + "Revoke all application access", + "Revoke VPN and remote access", + "Convert mailbox to shared (manager access for 90 days)", + "Transfer OneDrive files to manager", + "Remove from all security and distribution groups", + "Revoke OAuth tokens and API keys", + "Wipe corporate data from mobile devices", + "Archive identity record", + "Schedule account deletion after retention period" + ], + "valid_transitions": ["REHIRE", "DELETED"] + }, + "REHIRE": { + "description": "Previously terminated employee returning", + "automated_actions": [ + "Reactivate existing identity record", + "Reset credentials and require MFA re-enrollment", + "Provision based on new job code (not previous access)", + "Flag for enhanced access review in first 30 days" + ], + "valid_transitions": ["ACTIVE"] + }, + "DELETED": { + "description": "Account permanently removed after retention period", + "automated_actions": [ + "Delete AD account", + "Delete email mailbox archive", + "Remove identity record from IGA", + "Generate deletion audit log" + ], + "valid_transitions": [] + } + }, + "retention_periods": { + "terminated_to_deleted": "90 days (default)", + "mailbox_retention": "90 days as shared mailbox", + "onedrive_retention": "30 days manager access, then archived", + "audit_log_retention": "7 years for compliance" + } +} +``` + +### Step 2: Implement Authoritative Source Integration + +Connect HR system as the single source of truth for identity data: + +```python +""" +HR Source Integration - Workday to IGA Platform Connector +Polls Workday for employee lifecycle events and triggers provisioning. +""" +import requests +from datetime import datetime, timedelta +import logging + +class WorkdayIdentityConnector: + def __init__(self, config): + self.base_url = config["workday_api_url"] + self.tenant = config["tenant"] + self.client_id = config["client_id"] + self.client_secret = config["client_secret"] + self.session = requests.Session() + self.logger = logging.getLogger("workday_connector") + + def get_access_token(self): + """Authenticate to Workday REST API.""" + token_url = f"{self.base_url}/ccx/oauth2/{self.tenant}/token" + response = self.session.post(token_url, data={ + "grant_type": "client_credentials", + "client_id": self.client_id, + "client_secret": self.client_secret + }) + response.raise_for_status() + return response.json()["access_token"] + + def fetch_worker_changes(self, since_datetime): + """Fetch all worker lifecycle events since the last sync.""" + headers = {"Authorization": f"Bearer {self.get_access_token()}"} + params = { + "Updated_From": since_datetime.isoformat(), + "Updated_Through": datetime.utcnow().isoformat(), + "Count": 100 + } + + workers = [] + url = f"{self.base_url}/ccx/api/v1/{self.tenant}/workers" + + while url: + response = self.session.get(url, headers=headers, params=params) + response.raise_for_status() + data = response.json() + workers.extend(data.get("data", [])) + url = data.get("next", None) + params = {} + + return workers + + def map_lifecycle_event(self, worker): + """Map Workday worker data to identity lifecycle event.""" + worker_data = worker.get("workerData", {}) + employment = worker_data.get("employmentData", {}) + personal = worker_data.get("personalData", {}) + + event = { + "employee_id": worker.get("id"), + "first_name": personal.get("legalName", {}).get("firstName"), + "last_name": personal.get("legalName", {}).get("lastName"), + "email": worker_data.get("emailAddress"), + "job_code": employment.get("jobProfile", {}).get("id"), + "job_title": employment.get("jobProfile", {}).get("name"), + "department": employment.get("organization", {}).get("name"), + "department_code": employment.get("organization", {}).get("id"), + "manager_id": employment.get("managerId"), + "location": employment.get("location", {}).get("name"), + "cost_center": employment.get("costCenter", {}).get("id"), + "hire_date": employment.get("hireDate"), + "termination_date": employment.get("terminationDate"), + "status": employment.get("status"), + "worker_type": employment.get("workerType"), + } + + # Determine lifecycle transition + if event["status"] == "Active" and event["hire_date"]: + hire_date = datetime.fromisoformat(event["hire_date"]) + if hire_date > datetime.utcnow(): + event["lifecycle_event"] = "PRE_HIRE" + else: + event["lifecycle_event"] = "JOINER" + elif event["status"] == "Active": + event["lifecycle_event"] = "MOVER" # Department or role change + elif event["status"] == "Terminated": + event["lifecycle_event"] = "LEAVER" + elif event["status"] == "On Leave": + event["lifecycle_event"] = "LEAVE_OF_ABSENCE" + + return event + + def process_lifecycle_events(self, since_datetime): + """Main processing loop for identity lifecycle events.""" + workers = self.fetch_worker_changes(since_datetime) + events = [] + + for worker in workers: + event = self.map_lifecycle_event(worker) + events.append(event) + self.logger.info( + f"Lifecycle event: {event['lifecycle_event']} for " + f"{event['first_name']} {event['last_name']} " + f"(EmpID: {event['employee_id']})" + ) + + return events +``` + +### Step 3: Implement Role Mining and Birthright Access + +Define roles based on job functions for automated provisioning: + +```python +""" +Role Mining Engine +Analyzes existing access patterns to derive role definitions +for birthright (automatic) provisioning. +""" +import pandas as pd +from collections import Counter +from itertools import combinations + +class RoleMiningEngine: + def __init__(self, access_data): + """ + access_data: DataFrame with columns + [employee_id, job_code, department, application, entitlement] + """ + self.access_data = access_data + + def mine_birthright_roles(self, min_assignment_pct=0.8): + """ + Identify entitlements that should be automatically assigned + based on job code. If 80%+ of users with same job code + have an entitlement, it becomes birthright access. + """ + birthright_roles = {} + + for job_code, group in self.access_data.groupby("job_code"): + total_users = group["employee_id"].nunique() + entitlement_counts = group.groupby( + ["application", "entitlement"] + )["employee_id"].nunique() + + birthright_entitlements = [] + for (app, ent), count in entitlement_counts.items(): + pct = count / total_users + if pct >= min_assignment_pct: + birthright_entitlements.append({ + "application": app, + "entitlement": ent, + "assignment_percentage": round(pct * 100, 1), + "user_count": count + }) + + if birthright_entitlements: + birthright_roles[job_code] = { + "job_code": job_code, + "total_users": total_users, + "birthright_entitlements": birthright_entitlements + } + + return birthright_roles + + def detect_role_explosion(self): + """Identify roles with excessive overlap indicating need for consolidation.""" + roles = self.access_data.groupby("job_code").apply( + lambda x: set(zip(x["application"], x["entitlement"])) + ) + + overlap_report = [] + for (role1, ents1), (role2, ents2) in combinations(roles.items(), 2): + if len(ents1) == 0 or len(ents2) == 0: + continue + overlap = len(ents1 & ents2) + max_size = max(len(ents1), len(ents2)) + overlap_pct = overlap / max_size * 100 + + if overlap_pct > 70: + overlap_report.append({ + "role_1": role1, + "role_2": role2, + "role_1_entitlements": len(ents1), + "role_2_entitlements": len(ents2), + "overlapping_entitlements": overlap, + "overlap_percentage": round(overlap_pct, 1), + "recommendation": "CONSOLIDATE" if overlap_pct > 90 else "REVIEW" + }) + + return sorted(overlap_report, key=lambda x: x["overlap_percentage"], reverse=True) + + def find_orphaned_access(self): + """ + Find entitlements that no longer align with any role definition. + These are exceptions that accumulated over time. + """ + # Get birthright definitions + birthright = self.mine_birthright_roles(min_assignment_pct=0.5) + + orphaned = [] + for _, row in self.access_data.iterrows(): + job_birthright = birthright.get(row["job_code"], {}) + expected_ents = set() + for ent in job_birthright.get("birthright_entitlements", []): + expected_ents.add((ent["application"], ent["entitlement"])) + + current_ent = (row["application"], row["entitlement"]) + if current_ent not in expected_ents: + orphaned.append({ + "employee_id": row["employee_id"], + "job_code": row["job_code"], + "application": row["application"], + "entitlement": row["entitlement"], + "recommendation": "Review for revocation" + }) + + return pd.DataFrame(orphaned) +``` + +### Step 4: Build Access Request and Approval Workflow + +Implement self-service access request with risk-based approvals: + +```python +""" +Access Request Workflow Engine +Handles self-service access requests with multi-level approvals +based on risk classification of requested entitlements. +""" + +ACCESS_REQUEST_WORKFLOW = { + "risk_levels": { + "LOW": { + "description": "Standard business applications", + "examples": ["Email distribution groups", "SharePoint team sites", "Standard SaaS apps"], + "approval_chain": ["manager"], + "sla_hours": 4, + "auto_approve_if_birthright": True + }, + "MEDIUM": { + "description": "Sensitive data access or elevated permissions", + "examples": ["CRM admin", "Financial reporting", "HR systems"], + "approval_chain": ["manager", "application_owner"], + "sla_hours": 24, + "auto_approve_if_birthright": False + }, + "HIGH": { + "description": "Privileged access or regulated data", + "examples": ["Database admin", "Cloud admin", "PAM vault access"], + "approval_chain": ["manager", "application_owner", "security_team"], + "sla_hours": 48, + "auto_approve_if_birthright": False, + "require_justification": True, + "require_time_limit": True + }, + "CRITICAL": { + "description": "Domain admin, root access, or production data modification", + "examples": ["Domain Admin", "AWS root", "Production DB write"], + "approval_chain": ["manager", "application_owner", "security_team", "ciso"], + "sla_hours": 72, + "auto_approve_if_birthright": False, + "require_justification": True, + "require_time_limit": True, + "require_sod_check": True, + "max_duration_days": 90 + } + } +} + +class AccessRequestEngine: + def __init__(self, iga_client, risk_catalog): + self.iga = iga_client + self.risk_catalog = risk_catalog + + def submit_request(self, requester_id, entitlement_id, justification, duration_days=None): + """Submit an access request with automatic risk classification.""" + # Classify risk level of requested entitlement + risk_level = self.risk_catalog.get_risk_level(entitlement_id) + workflow = ACCESS_REQUEST_WORKFLOW["risk_levels"][risk_level] + + # Check if entitlement is birthright for requester's role + requester = self.iga.get_identity(requester_id) + is_birthright = self.iga.is_birthright_for_role( + entitlement_id, requester["job_code"] + ) + + if is_birthright and workflow.get("auto_approve_if_birthright"): + return self._auto_approve(requester_id, entitlement_id, "Birthright access") + + # Run SOD check if required + if workflow.get("require_sod_check"): + sod_violations = self.iga.check_sod(requester_id, entitlement_id) + if sod_violations: + return { + "status": "SOD_VIOLATION", + "violations": sod_violations, + "action": "Request requires compensating control approval" + } + + # Create approval chain + request = { + "requester": requester_id, + "entitlement": entitlement_id, + "risk_level": risk_level, + "justification": justification, + "duration_days": duration_days or workflow.get("max_duration_days"), + "approval_chain": self._build_approval_chain( + requester, workflow["approval_chain"] + ), + "sla_deadline": workflow["sla_hours"], + "status": "PENDING_APPROVAL" + } + + return self.iga.create_request(request) + + def _build_approval_chain(self, requester, approver_types): + """Resolve approval chain to actual approver identities.""" + chain = [] + for approver_type in approver_types: + if approver_type == "manager": + chain.append({ + "type": "manager", + "identity": requester["manager_id"], + "fallback": requester.get("skip_manager_id") + }) + elif approver_type == "application_owner": + chain.append({ + "type": "application_owner", + "identity": "resolved_at_runtime", + "fallback": "it-governance-team" + }) + elif approver_type == "security_team": + chain.append({ + "type": "group", + "identity": "security-governance-team", + "required_approvals": 1 + }) + elif approver_type == "ciso": + chain.append({ + "type": "role", + "identity": "CISO", + "fallback": "deputy-ciso" + }) + return chain +``` + +### Step 5: Implement Orphaned Account Detection and Remediation + +Identify and remediate accounts without active identity associations: + +```python +""" +Orphaned Account Detection +Identifies accounts in target systems that have no corresponding +active identity in the authoritative HR source. +""" + +class OrphanedAccountDetector: + def __init__(self, hr_connector, app_connectors): + self.hr = hr_connector + self.apps = app_connectors + + def detect_orphaned_accounts(self): + """Compare application accounts against HR active employees.""" + active_employees = set(self.hr.get_active_employee_ids()) + orphaned_accounts = [] + + for app_name, connector in self.apps.items(): + app_accounts = connector.get_all_accounts() + + for account in app_accounts: + correlated_id = account.get("employee_id") or account.get("correlation_id") + + if correlated_id and correlated_id not in active_employees: + # Check if recently terminated (within grace period) + termination_info = self.hr.get_termination_info(correlated_id) + + orphaned_accounts.append({ + "application": app_name, + "account_name": account["username"], + "correlated_employee_id": correlated_id, + "account_status": account.get("status", "unknown"), + "last_login": account.get("last_login"), + "termination_date": termination_info.get("date") if termination_info else None, + "days_since_termination": ( + (datetime.utcnow() - termination_info["date"]).days + if termination_info and termination_info.get("date") else None + ), + "risk_level": self._assess_orphan_risk(account, termination_info) + }) + + elif not correlated_id: + # Uncorrelated account - no link to any employee + orphaned_accounts.append({ + "application": app_name, + "account_name": account["username"], + "correlated_employee_id": None, + "account_status": account.get("status", "unknown"), + "last_login": account.get("last_login"), + "risk_level": "HIGH", + "reason": "Uncorrelated - no employee association" + }) + + return orphaned_accounts + + def _assess_orphan_risk(self, account, termination_info): + """Assess risk level of orphaned account.""" + if account.get("is_privileged"): + return "CRITICAL" + if termination_info and termination_info.get("involuntary"): + return "HIGH" + if account.get("status") == "active": + return "HIGH" + return "MEDIUM" + + def generate_remediation_plan(self, orphaned_accounts): + """Create remediation actions for orphaned accounts.""" + plan = [] + for account in orphaned_accounts: + if account["risk_level"] == "CRITICAL": + action = "DISABLE_IMMEDIATELY" + sla = "4 hours" + elif account["risk_level"] == "HIGH": + action = "DISABLE_WITHIN_24H" + sla = "24 hours" + else: + action = "REVIEW_AND_DISABLE" + sla = "7 days" + + plan.append({ + **account, + "remediation_action": action, + "sla": sla, + "assigned_to": "identity-governance-team" + }) + + return sorted(plan, key=lambda x: ["CRITICAL", "HIGH", "MEDIUM", "LOW"].index(x["risk_level"])) +``` + +## Key Concepts + +| Term | Definition | +|------|------------| +| **Joiner-Mover-Leaver (JML)** | Core identity lifecycle transitions covering employee onboarding (joiner), role/department changes (mover), and offboarding (leaver) | +| **Birthright Access** | Baseline entitlements automatically provisioned based on job code, department, or location without requiring an access request | +| **Role Mining** | Analysis of existing access patterns to derive role definitions by identifying common entitlement groupings across similar job functions | +| **Orphaned Account** | Application account that no longer has a corresponding active identity in the authoritative HR source, representing a security risk | +| **Authoritative Source** | System of record (typically HR) that serves as the single source of truth for identity attributes and employment status | +| **Access Request Workflow** | Self-service process enabling users to request additional entitlements with risk-based approval routing | + +## Tools & Systems + +- **SailPoint IdentityIQ/IdentityNow**: Enterprise IGA platform for lifecycle management, access certifications, and automated provisioning +- **Saviynt Enterprise Identity Cloud**: Cloud-native IGA with identity warehouse, access governance, and application access management +- **Microsoft Entra ID Governance**: Identity governance capabilities including lifecycle workflows, access reviews, and entitlement management +- **One Identity Manager**: IGA solution with business role management, attestation, and IT shop for access requests + +## Common Scenarios + +### Scenario: Building JML Process for 10,000-Employee Organization + +**Context**: Rapidly growing company has no automated identity lifecycle. IT manually creates accounts, taking 3-5 days for new hires. Terminated employees retain access for weeks. Audit found 2,300 orphaned accounts across 45 applications. + +**Approach**: +1. Integrate Workday as authoritative source with daily delta sync to IGA platform +2. Mine existing access patterns to define birthright roles for the top 20 job codes (covering 80% of employees) +3. Implement pre-hire provisioning triggered 7 days before start date for AD, email, and birthright apps +4. Build termination workflow that disables all access within 1 hour of HR status change +5. Create mover workflow that recalculates roles when job code or department changes +6. Deploy self-service access request portal with risk-based approval chains +7. Run orphaned account detection to identify and remediate the 2,300 existing orphans +8. Schedule quarterly access certifications to prevent access accumulation + +**Pitfalls**: +- Not defining a single authoritative source leads to conflicting identity data from multiple HR systems +- Mining roles without business validation creates technical roles that do not align with organizational structure +- Automating termination without grace period for knowledge transfer frustrates business managers +- Not handling contractor and vendor identities that exist outside the HR system + +## Output Format + +``` +IDENTITY GOVERNANCE LIFECYCLE REPORT +======================================= +Authoritative Source: Workday +IGA Platform: SailPoint IdentityIQ +Total Identities: 10,247 +Active Employees: 9,834 +Contractors: 413 + +LIFECYCLE AUTOMATION +Joiner (Pre-Hire) SLA: Target: 0 days | Actual: 0.2 days avg +Mover Processing SLA: Target: 1 day | Actual: 0.8 days avg +Leaver Disablement SLA: Target: 1 hour | Actual: 0.5 hours avg + +PROVISIONING METRICS (Last 30 Days) +New Hires Provisioned: 187 + Auto-Provisioned: 174 (93.0%) + Manual Intervention: 13 (7.0%) +Role Changes Processed: 89 +Terminations Processed: 43 + Within 1-Hour SLA: 41 (95.3%) + +ROLE GOVERNANCE +Defined Roles: 127 +Birthright Roles: 48 +Average Entitlements/Role: 12.3 +Role Overlap > 70%: 8 pairs (consolidation recommended) + +ORPHANED ACCOUNTS +Detected: 23 + Critical: 2 (privileged accounts) + High: 8 + Medium: 13 +Remediated (30 days): 19 +Outstanding: 4 + +ACCESS REQUESTS +Submitted: 342 +Auto-Approved (Birthright):87 (25.4%) +Approved: 231 (67.5%) +Denied: 24 (7.0%) +Average Approval Time: 6.2 hours +SOD Violations Flagged: 12 +``` From a7fc608da771c55c7baf9692fbeb988d95c0cfc0 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 02:39:38 +0100 Subject: [PATCH 08/16] feat: wire Azure skill documents into embedding pipeline --- ai/embed.py | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/ai/embed.py b/ai/embed.py index 0b04702b..d4d68c5d 100644 --- a/ai/embed.py +++ b/ai/embed.py @@ -1,4 +1,5 @@ -"""Build the OpenShield knowledge base vector store for RAG AI insights.""" +"""Build the OpenShield knowledge base vector store for RAG AI insights""" + import importlib.util import json @@ -12,6 +13,7 @@ REPO_ROOT = Path(__file__).resolve().parent.parent RULES_DIR = REPO_ROOT / "scanner" / "rules" FRAMEWORKS_DIR = REPO_ROOT / "compliance" / "frameworks" +SKILLS_DIR = REPO_ROOT / "ai" / "knowledge" / "skills" VECTORSTORE_DIR = REPO_ROOT / "ai" / "vectorstore" COLLECTION_NAME = "openshield" @@ -23,6 +25,29 @@ def _load_rule_module(path): return module +def _collect_skill_documents(): + documents = [] + if not SKILLS_DIR.exists(): + logger.warning("Skills directory not found, skipping: %s", SKILLS_DIR) + return documents + for path in sorted(SKILLS_DIR.rglob("SKILL.md")): + try: + text = path.read_text(encoding="utf-8") + except Exception as exc: + logger.warning("Skipping %s: %s", path.name, exc) + continue + if not text.strip(): + continue + skill_name = path.parent.name + documents.append({ + "id": f"skill-{skill_name}", + "text": text, + "source": skill_name, + "type": "skill", + }) + return documents + + def _collect_rule_documents(): documents = [] for path in sorted(RULES_DIR.glob("az_*.py")): @@ -80,14 +105,17 @@ def build_vectorstore(): VECTORSTORE_DIR.mkdir(parents=True, exist_ok=True) client = chromadb.PersistentClient(path=str(VECTORSTORE_DIR)) - # Start clean so a refresh never leaves stale entries behind. try: client.delete_collection(COLLECTION_NAME) except Exception: pass collection = client.create_collection(COLLECTION_NAME) - documents = _collect_rule_documents() + _collect_compliance_documents() + documents = ( + _collect_skill_documents() + + _collect_rule_documents() + + _collect_compliance_documents() + ) if not documents: raise RuntimeError("No documents found to embed. Check repo paths.") @@ -103,6 +131,7 @@ def build_vectorstore(): ) return len(documents) + if __name__ == "__main__": logging.basicConfig(level=logging.INFO) count = build_vectorstore() From 2f15fd0fe43eb41d9b8ac9b55fb9e50920d3b1d4 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 02:40:17 +0100 Subject: [PATCH 09/16] Add sentence-transformers version 2.7.0 to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f643453b..43d9ede6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,4 @@ msrest==0.7.1 azure-mgmt-postgresqlflexibleservers==1.0.0b1 azure-keyvault-certificates==4.8.0 chromadb==0.4.24 +sentence-transformers==2.7.0 From ef5d3597fbe7f30ff3ebc617e1040aafb2bbf11c Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 03:20:58 +0100 Subject: [PATCH 10/16] ignore generated ai vectorstore directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 83972fad..f369c429 100644 --- a/.gitignore +++ b/.gitignore @@ -216,3 +216,4 @@ __marimo__/ # Streamlit .streamlit/secrets.toml +ai/vectorstore/ From 87472955fb346b8e5613447f2e88aba65adeda38 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 03:24:40 +0100 Subject: [PATCH 11/16] Refactor AI insights routes for clarity and updates --- api/routes/ai.py | 174 ++++++++++++++++------------------------------- 1 file changed, 57 insertions(+), 117 deletions(-) diff --git a/api/routes/ai.py b/api/routes/ai.py index 19f5c8aa..0d67bd7e 100644 --- a/api/routes/ai.py +++ b/api/routes/ai.py @@ -1,35 +1,30 @@ -<<<<<<< HEAD -"""AI insights routes: RAG grounded summary, prioritisation and Q&A.""" +"""AI insights routes: RAG grounded summary, prioritisation, Q&A, and executive insights.""" import json -======= -"""AI insights route: executive summary and prioritised remediation plan.""" - ->>>>>>> origin/dev import logging from flask import Blueprint, jsonify, request -<<<<<<< HEAD +from api.services.ai_provider import PROVIDERS as SUPPORTED_PROVIDERS from api.services.ai_provider import get_completion from ai.retriever import retrieve, VectorStoreNotBuilt ai_bp = Blueprint("ai", __name__) logger = logging.getLogger(__name__) -SEVERITY_ORDER = {"HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3} +SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4, "INFORMATIONAL": 4} def _findings_to_text(findings): ordered = sorted( findings, - key=lambda f: SEVERITY_ORDER.get(str(f.get("severity", "")).upper(), 4), + key=lambda f: SEVERITY_ORDER.get(str(f.get("severity", "")).upper(), 5), ) lines = [] for i, f in enumerate(ordered, 1): lines.append( f"{i}. [{f.get('severity', 'UNKNOWN')}] " - f"{f.get('rule_name', 'Unknown')} on " + f"{f.get('rule_name', f.get('title', 'Unknown'))} on " f"{f.get('resource_name', 'unknown resource')}: " f"{f.get('description', '')}" ) @@ -54,6 +49,55 @@ def _read_request(): return body, None +@ai_bp.post("/api/ai/insights") +def insights(): + data = request.get_json(silent=True) + if data is None: + return jsonify({"error": "Request body must be valid JSON"}), 400 + + provider = str(data.get("provider") or "").strip().lower() + api_key = str(data.get("api_key") or "").strip() + findings = data.get("findings") + + if not provider: + return jsonify({"error": "Missing required field: provider"}), 400 + if provider not in SUPPORTED_PROVIDERS: + return jsonify({"error": f"Unsupported provider: {provider}"}), 400 + if not api_key: + return jsonify({"error": "Missing required field: api_key"}), 400 + if findings is None: + return jsonify({"error": "Missing required field: findings"}), 400 + if not isinstance(findings, list): + return jsonify({"error": "findings must be a list"}), 400 + if len(findings) == 0: + return jsonify({"error": "findings must not be empty"}), 400 + + findings_text = _findings_to_text(findings) + summary_prompt = ( + "You are a security advisor writing for a non-technical executive audience.\n" + "Based on the following cloud security findings, write a concise executive summary.\n" + "Avoid technical jargon. Mention the overall security risk level and likely business impact.\n" + f"Findings:\n{findings_text}\n\nExecutive Summary:" + ) + remediation_prompt = ( + "You are a cloud security engineer writing a remediation plan.\n" + "The findings below are sorted by severity. For each finding, provide actionable fix steps.\n" + f"Findings:\n{findings_text}\n\nPrioritised Remediation Plan:" + ) + + try: + executive_summary = get_completion(provider, api_key, summary_prompt) + remediation_plan = get_completion(provider, api_key, remediation_prompt) + except Exception: + logger.warning("AI provider request failed for provider=%s", provider) + return jsonify({"error": "AI provider request failed"}), 502 + + return jsonify({ + "executive_summary": executive_summary, + "remediation_plan": remediation_plan, + }) + + @ai_bp.post("/api/ai/summary") def ai_summary(): body, error = _read_request() @@ -76,9 +120,7 @@ def ai_summary(): f"GROUNDED KNOWLEDGE:\n{context}\n\nFINDINGS:\n{findings_text}" ) try: - answer = get_completion( - body["provider"], body["api_key"], prompt, model=body.get("model") - ) + answer = get_completion(body["provider"], body["api_key"], prompt, model=body.get("model")) except ValueError as exc: return jsonify({"error": str(exc)}), 400 except RuntimeError as exc: @@ -116,9 +158,7 @@ def ai_prioritise(): f"GROUNDED KNOWLEDGE:\n{context}\n\nFINDINGS:\n{findings_text}" ) try: - raw = get_completion( - body["provider"], body["api_key"], prompt, model=body.get("model") - ) + raw = get_completion(body["provider"], body["api_key"], prompt, model=body.get("model")) except ValueError as exc: return jsonify({"error": str(exc)}), 400 except RuntimeError as exc: @@ -162,9 +202,7 @@ def ai_ask(): f"CURRENT FINDINGS:\n{findings_text}\n\nQUESTION: {question}" ) try: - answer = get_completion( - body["provider"], body["api_key"], prompt, model=body.get("model") - ) + answer = get_completion(body["provider"], body["api_key"], prompt, model=body.get("model")) except ValueError as exc: return jsonify({"error": str(exc)}), 400 except RuntimeError as exc: @@ -175,102 +213,4 @@ def ai_ask(): "sources": sources, "provider": body["provider"], "model": body.get("model"), -======= -from api.services.ai_provider import PROVIDERS as SUPPORTED_PROVIDERS -from api.services.ai_provider import get_completion - -ai_bp = Blueprint("ai", __name__, url_prefix="/api/ai") -logger = logging.getLogger(__name__) - -_SEVERITY_RANK = { - "CRITICAL": 5, - "HIGH": 4, - "MEDIUM": 3, - "LOW": 2, - "INFORMATIONAL": 1, - "INFO": 1, -} - - -def severity_rank(finding: dict) -> int: - return _SEVERITY_RANK.get(str(finding.get("severity", "")).upper(), 0) - - -def _build_summary_prompt(findings: list) -> str: - lines = [] - for f in findings: - lines.append( - f"- [{f.get('severity', 'UNKNOWN')}] {f.get('title', 'Untitled')}: {f.get('description', 'No description provided.')}" - ) - findings_text = "\n".join(lines) - return ( - "You are a security advisor writing for a non-technical executive audience.\n" - "Based on the following cloud security findings, write a concise executive summary.\n" - "Avoid technical jargon. Mention the overall security risk level and likely business or operational impact.\n" - "Do not invent findings. If information is missing, say so clearly.\n\n" - f"Findings:\n{findings_text}\n\n" - "Executive Summary:" - ) - - -def _build_remediation_prompt(sorted_findings: list) -> str: - lines = [] - for f in sorted_findings: - rule_id = f.get("rule_id", "") - title = f.get("title", "Untitled") - severity = f.get("severity", "UNKNOWN") - remediation = f.get("remediation", "No remediation detail provided.") - label = f"{rule_id} — {title}" if rule_id else title - lines.append(f"- [{severity}] {label}: {remediation}") - findings_text = "\n".join(lines) - return ( - "You are a cloud security engineer writing a remediation plan.\n" - "The findings below are already sorted by severity (Critical first, then High, Medium, Low, Informational).\n" - "For each finding, provide practical, actionable fix steps.\n" - "Reference the rule ID and title where available.\n" - "Do not invent findings. If a finding lacks remediation detail, state what information is missing.\n\n" - f"Findings (severity order):\n{findings_text}\n\n" - "Prioritised Remediation Plan:" - ) - - -@ai_bp.post("/insights") -def insights(): - data = request.get_json(silent=True) - if data is None: - return jsonify({"error": "Request body must be valid JSON"}), 400 - - provider = str(data.get("provider") or "").strip().lower() - api_key = str(data.get("api_key") or "").strip() - findings = data.get("findings") - - if not provider: - return jsonify({"error": "Missing required field: provider"}), 400 - if provider not in SUPPORTED_PROVIDERS: - return jsonify({"error": f"Unsupported provider: {provider}"}), 400 - if not api_key: - return jsonify({"error": "Missing required field: api_key"}), 400 - if findings is None: - return jsonify({"error": "Missing required field: findings"}), 400 - if not isinstance(findings, list): - return jsonify({"error": "findings must be a list"}), 400 - if len(findings) == 0: - return jsonify({"error": "findings must not be empty"}), 400 - - sorted_findings = sorted(findings, key=severity_rank, reverse=True) - - summary_prompt = _build_summary_prompt(sorted_findings) - remediation_prompt = _build_remediation_prompt(sorted_findings) - - try: - executive_summary = get_completion(provider, api_key, summary_prompt) - remediation_plan = get_completion(provider, api_key, remediation_prompt) - except Exception: - logger.warning("AI provider request failed for provider=%s", provider) - return jsonify({"error": "AI provider request failed"}), 502 - - return jsonify({ - "executive_summary": executive_summary, - "remediation_plan": remediation_plan, ->>>>>>> origin/dev }) From cb76c20fcef50c4a1377c90f3be5eaca8dbd7a99 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Sun, 31 May 2026 03:39:35 +0100 Subject: [PATCH 12/16] fix: deduplicate ai_bp and restore missing score_bp import in app.py --- api/app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/app.py b/api/app.py index 0ad78e1c..d7d68d82 100644 --- a/api/app.py +++ b/api/app.py @@ -109,7 +109,6 @@ def verify_jwt() -> None: # ------------------------------------------------------------------ # # Blueprints # # ------------------------------------------------------------------ # - from api.routes.ai import ai_bp from api.routes.compliance import compliance_bp from api.routes.findings import findings_bp from api.routes.scans import scans_bp @@ -120,7 +119,6 @@ def verify_jwt() -> None: app.register_blueprint(scans_bp) app.register_blueprint(score_bp) app.register_blueprint(compliance_bp) - app.register_blueprint(ai_bp) # ------------------------------------------------------------------ # # Routes (public) # From 62518a70d4b1d2e529757cd6941fd879935ca364 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Mon, 1 Jun 2026 01:32:20 +0100 Subject: [PATCH 13/16] Refactor severity handling and update prompts --- api/routes/ai.py | 127 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 106 insertions(+), 21 deletions(-) diff --git a/api/routes/ai.py b/api/routes/ai.py index 0d67bd7e..22293d3d 100644 --- a/api/routes/ai.py +++ b/api/routes/ai.py @@ -1,4 +1,4 @@ -"""AI insights routes: RAG grounded summary, prioritisation, Q&A, and executive insights.""" +"""AI insights routes: executive summary, RAG-grounded analysis, and Q&A.""" import json import logging @@ -12,19 +12,97 @@ ai_bp = Blueprint("ai", __name__) logger = logging.getLogger(__name__) -SEVERITY_ORDER = {"CRITICAL": 0, "HIGH": 1, "MEDIUM": 2, "LOW": 3, "INFO": 4, "INFORMATIONAL": 4} +_SEVERITY_RANK = { + "CRITICAL": 5, + "HIGH": 4, + "MEDIUM": 3, + "LOW": 2, + "INFORMATIONAL": 1, + "INFO": 1, +} + +SEVERITY_ORDER = {"CRITICAL": -1, "HIGH": 0, "MEDIUM": 1, "LOW": 2, "INFO": 3, "INFORMATIONAL": 3} + + +def severity_rank(finding: dict) -> int: + return _SEVERITY_RANK.get(str(finding.get("severity", "")).upper(), 0) + + +def _build_summary_prompt(findings: list) -> str: + lines = [] + for f in findings: + lines.append( + f"- [{f.get('severity', 'UNKNOWN')}] {f.get('title', 'Untitled')}: {f.get('description', 'No description provided.')}" + ) + findings_text = "\n".join(lines) + return ( + "You are a security advisor writing for a non-technical executive audience.\n" + "Based on the following cloud security findings, write a concise executive summary.\n" + "Avoid technical jargon. Mention the overall security risk level and likely business or operational impact.\n" + "Do not invent findings. If information is missing, say so clearly.\n\n" + f"Findings:\n{findings_text}\n\n" + "Executive Summary:" + ) + + +def _build_question_prompt(sorted_findings: list, question: str) -> str: + lines = [] + for f in sorted_findings: + rule_id = f.get("rule_id", "") + title = f.get("title", "Untitled") + severity = f.get("severity", "UNKNOWN") + description = f.get("description", "No description provided.") + remediation = f.get("remediation", "No remediation detail provided.") + label = f"{rule_id} — {title}" if rule_id else title + lines.append( + f"- [{severity}] {label}: {description} Remediation: {remediation}" + ) + findings_text = "\n".join(lines) + return ( + "You are a cloud security assistant.\n" + "Answer the user's question using only the scan findings provided below.\n" + "Do not invent facts or assume scan results that are not listed.\n" + "Prioritise high severity, exploitable, and compliance-impacting findings, " + "and consider remediation urgency.\n" + "Be concise but useful. If the findings are insufficient to answer " + "confidently, say what evidence is missing.\n\n" + f"Question: {question}\n\n" + f"Findings (severity order):\n{findings_text}\n\n" + "Answer:" + ) + + +def _build_remediation_prompt(sorted_findings: list) -> str: + lines = [] + for f in sorted_findings: + rule_id = f.get("rule_id", "") + title = f.get("title", "Untitled") + severity = f.get("severity", "UNKNOWN") + remediation = f.get("remediation", "No remediation detail provided.") + label = f"{rule_id} — {title}" if rule_id else title + lines.append(f"- [{severity}] {label}: {remediation}") + findings_text = "\n".join(lines) + return ( + "You are a cloud security engineer writing a remediation plan.\n" + "The findings below are already sorted by severity (Critical first, then High, Medium, Low, Informational).\n" + "For each finding, provide practical, actionable fix steps.\n" + "Reference the rule ID and title where available.\n" + "Do not invent findings. If a finding lacks remediation detail, state what information is missing.\n\n" + f"Findings (severity order):\n{findings_text}\n\n" + "Prioritised Remediation Plan:" + ) def _findings_to_text(findings): ordered = sorted( findings, - key=lambda f: SEVERITY_ORDER.get(str(f.get("severity", "")).upper(), 5), + key=lambda f: SEVERITY_ORDER.get(str(f.get("severity", "")).upper(), 4), ) lines = [] for i, f in enumerate(ordered, 1): lines.append( f"{i}. [{f.get('severity', 'UNKNOWN')}] " - f"{f.get('rule_name', f.get('title', 'Unknown'))} on " + f"{f.get('rule_name', 'Unknown')} on " f"{f.get('resource_name', 'unknown resource')}: " f"{f.get('description', '')}" ) @@ -58,6 +136,7 @@ def insights(): provider = str(data.get("provider") or "").strip().lower() api_key = str(data.get("api_key") or "").strip() findings = data.get("findings") + question = str(data.get("question") or "").strip() if not provider: return jsonify({"error": "Missing required field: provider"}), 400 @@ -72,30 +151,30 @@ def insights(): if len(findings) == 0: return jsonify({"error": "findings must not be empty"}), 400 - findings_text = _findings_to_text(findings) - summary_prompt = ( - "You are a security advisor writing for a non-technical executive audience.\n" - "Based on the following cloud security findings, write a concise executive summary.\n" - "Avoid technical jargon. Mention the overall security risk level and likely business impact.\n" - f"Findings:\n{findings_text}\n\nExecutive Summary:" - ) - remediation_prompt = ( - "You are a cloud security engineer writing a remediation plan.\n" - "The findings below are sorted by severity. For each finding, provide actionable fix steps.\n" - f"Findings:\n{findings_text}\n\nPrioritised Remediation Plan:" - ) + sorted_findings = sorted(findings, key=severity_rank, reverse=True) + + summary_prompt = _build_summary_prompt(sorted_findings) + remediation_prompt = _build_remediation_prompt(sorted_findings) try: executive_summary = get_completion(provider, api_key, summary_prompt) remediation_plan = get_completion(provider, api_key, remediation_prompt) + answer = None + if question: + question_prompt = _build_question_prompt(sorted_findings, question) + answer = get_completion(provider, api_key, question_prompt) except Exception: logger.warning("AI provider request failed for provider=%s", provider) return jsonify({"error": "AI provider request failed"}), 502 - return jsonify({ + response = { "executive_summary": executive_summary, "remediation_plan": remediation_plan, - }) + } + if question: + response["answer"] = answer + + return jsonify(response) @ai_bp.post("/api/ai/summary") @@ -120,7 +199,9 @@ def ai_summary(): f"GROUNDED KNOWLEDGE:\n{context}\n\nFINDINGS:\n{findings_text}" ) try: - answer = get_completion(body["provider"], body["api_key"], prompt, model=body.get("model")) + answer = get_completion( + body["provider"], body["api_key"], prompt, model=body.get("model") + ) except ValueError as exc: return jsonify({"error": str(exc)}), 400 except RuntimeError as exc: @@ -158,7 +239,9 @@ def ai_prioritise(): f"GROUNDED KNOWLEDGE:\n{context}\n\nFINDINGS:\n{findings_text}" ) try: - raw = get_completion(body["provider"], body["api_key"], prompt, model=body.get("model")) + raw = get_completion( + body["provider"], body["api_key"], prompt, model=body.get("model") + ) except ValueError as exc: return jsonify({"error": str(exc)}), 400 except RuntimeError as exc: @@ -202,7 +285,9 @@ def ai_ask(): f"CURRENT FINDINGS:\n{findings_text}\n\nQUESTION: {question}" ) try: - answer = get_completion(body["provider"], body["api_key"], prompt, model=body.get("model")) + answer = get_completion( + body["provider"], body["api_key"], prompt, model=body.get("model") + ) except ValueError as exc: return jsonify({"error": str(exc)}), 400 except RuntimeError as exc: From 860dfd1f8b6178f4e4e3bfe997db188811705810 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Mon, 1 Jun 2026 01:52:08 +0100 Subject: [PATCH 14/16] merge dev into feat/rag-ai-insights, resolve ai.py conflict --- api/routes/ai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/routes/ai.py b/api/routes/ai.py index 22293d3d..238f68fc 100644 --- a/api/routes/ai.py +++ b/api/routes/ai.py @@ -298,4 +298,4 @@ def ai_ask(): "sources": sources, "provider": body["provider"], "model": body.get("model"), - }) + }) \ No newline at end of file From c91dfe37e934009347b6286ce7b041b49eb22ca7 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Mon, 1 Jun 2026 01:57:26 +0100 Subject: [PATCH 15/16] trigger CI From 0baed828e5b136d6cbe0e5b0b8ae3bf998269fb6 Mon Sep 17 00:00:00 2001 From: Tanvir Farhad Date: Mon, 1 Jun 2026 02:26:28 +0100 Subject: [PATCH 16/16] Remove AZ-CMP-003 and update AZ-NET-014 description Removed control AZ-CMP-003 related to unauthorized software detection and updated control AZ-NET-014 description for clarity. --- compliance/frameworks/soc2.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/compliance/frameworks/soc2.json b/compliance/frameworks/soc2.json index 4f407952..ee823f64 100644 --- a/compliance/frameworks/soc2.json +++ b/compliance/frameworks/soc2.json @@ -118,11 +118,6 @@ "control_name": "System Vulnerabilities are Identified and Managed", "description": "The virtual machine does not have automatic OS patching enabled. CC7.1 requires that vulnerabilities in system components are identified and managed through a defined process. Without automatic patching, known OS vulnerabilities are left unmitigated and exploitable." }, - "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", @@ -168,11 +163,10 @@ "control_name": "Restricts Access from Outside the Network Boundary", "description": "A virtual network without an Azure Firewall relies on NSGs alone and lacks a centralized point to inspect, filter, and log traffic crossing the network boundary. CC6.6 requires that logical access from outside the network boundary is restricted and controlled. Deploying an Azure Firewall enforces inspected, logged perimeter access for the network." }, - "AZ-NET-014": { + "AZ-NET-014": { "control_id": "CC6.6", "control_name": "Restricts Access from Outside the Network Boundary", "description": "VNet peering with allowGatewayTransit or useRemoteGateways enabled allows traffic to cross network boundaries through shared gateways, weakening the logical separation between network zones. CC6.6 requires that logical access from outside the network boundary is restricted and controlled. Gateway transit on peering connections should be disabled to enforce boundary separation." - "description": "Enabling 'Allow access to Azure services' on a SQL Server firewall creates a rule that permits any Azure-hosted resource — including services from other tenants — to connect to the database. CC6.6 requires that access from outside the network boundary is restricted to authorised sources. Disabling this setting and replacing it with explicit firewall rules or private endpoints enforces the network boundary and ensures only known and trusted systems can reach the SQL Server." } } }