From 99d00c89bfb74cf1790bf52e3b236a0da3c91983 Mon Sep 17 00:00:00 2001 From: athreyam Date: Mon, 11 May 2026 19:54:56 +0530 Subject: [PATCH 1/2] add conditional access policy based location management --- .pre-commit-config.yaml | 10 +- LICENSE | 2 +- NOTICE | 2 +- README.md | 106 ++++++- __init__.py | 2 +- manual_readme_content.md | 8 + msadgraph.json | 417 +++++++++++++++++++++++++++- msadgraph_connector.py | 158 ++++++++++- msadgraph_consts.py | 5 +- msadgraph_get_group.html | 2 +- msadgraph_list_group_members.html | 2 +- msadgraph_list_groups.html | 2 +- msadgraph_list_user_attributes.html | 2 +- msadgraph_list_user_devices.html | 2 +- msadgraph_list_users.html | 2 +- msadgraph_view.py | 2 +- release_notes/unreleased.md | 2 + 17 files changed, 706 insertions(+), 20 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77272d1..2d1f824 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ default_stages: [pre-commit] # This is a template for connector pre-commit hooks repos: - repo: https://github.com/compilerla/conventional-pre-commit - rev: v4.2.0 + rev: v4.4.0 hooks: - id: conventional-pre-commit stages: [commit-msg] @@ -27,7 +27,7 @@ repos: - id: check-json - id: check-yaml - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.13.3 + rev: v0.15.12 hooks: - id: ruff args: [ "--fix", "--unsafe-fixes"] # Allow unsafe fixes (ruff pretty strict about what it can fix) @@ -43,12 +43,12 @@ repos: - id: soar-app-linter args: ["--single-repo", "--message-level", "error"] - repo: https://github.com/hukkin/mdformat - rev: 0.7.22 + rev: 1.0.0 hooks: - id: mdformat exclude: "release_notes/.*" - repo: https://github.com/returntocorp/semgrep - rev: v1.89.0 + rev: v1.154.0 hooks: - id: semgrep - repo: https://github.com/Yelp/detect-secrets @@ -60,7 +60,7 @@ repos: exclude: "README.md" # Central hooks - repo: https://github.com/phantomcyber/dev-cicd-tools - rev: v2.1.0 + rev: v2.1.4 hooks: - id: build-docs language: python diff --git a/LICENSE b/LICENSE index bcb195d..c6a91d1 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright (c) 2022-2025 Splunk Inc. + Copyright (c) 2022-2026 Splunk Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/NOTICE b/NOTICE index 3c14100..bcd0f84 100644 --- a/NOTICE +++ b/NOTICE @@ -1,2 +1,2 @@ Splunk SOAR App: MS Graph for Active Directory -Copyright (c) 2022-2025 Splunk Inc. +Copyright (c) 2022-2026 Splunk Inc. diff --git a/README.md b/README.md index 62813f5..d81e4ce 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,8 @@ Choose **either** Delegated OR Application permissions based on your use case: - `Group.ReadWrite.All` - `GroupMember.ReadWrite.All` - `RoleManagement.ReadWrite.Directory` + - `Policy.Read.All` + - `Policy.ReadWrite.ConditionalAccess` - `offline_access` 1. Click **Add permissions** 1. Click **Grant admin consent for [Your Organization]** @@ -102,6 +104,8 @@ Choose **either** Delegated OR Application permissions based on your use case: - `Group.ReadWrite.All` - `GroupMember.ReadWrite.All` - `RoleManagement.ReadWrite.Directory` + - `Policy.Read.All` + - `Policy.ReadWrite.ConditionalAccess` - `User-PasswordProfile.ReadWrite.All` 1. Click **Add permissions** 1. Click **Grant admin consent for [Your Organization]** @@ -210,6 +214,9 @@ The following table shows the minimum required permissions for each action: | **List Group Members** | `GroupMember.Read.All` | `GroupMember.Read.All` | Directory Readers | | **Validate Group** | `User.Read.All` | `User.Read.All` | Directory Readers | | **List Directory Roles** | `RoleManagement.Read.Directory` | `RoleManagement.Read.Directory` | Directory Readers | +| **List Named Locations** | `Policy.Read.All` | `Policy.Read.All` | Conditional Access Administrator | +| **Add CIDR to Named Location** | `Policy.Read.All`, `Policy.ReadWrite.ConditionalAccess` | `Policy.Read.All`, `Policy.ReadWrite.ConditionalAccess` | Conditional Access Administrator | +| **Remove CIDR from Named Location** | `Policy.Read.All`, `Policy.ReadWrite.ConditionalAccess` | `Policy.Read.All`, `Policy.ReadWrite.ConditionalAccess` | Conditional Access Administrator | ### Full vs Minimum Permissions @@ -217,6 +224,7 @@ The following table shows the minimum required permissions for each action: - `User.ReadWrite.All`, `Directory.ReadWrite.All`, `User.ManageIdentities.All` - `Group.ReadWrite.All`, `GroupMember.ReadWrite.All`, `RoleManagement.ReadWrite.Directory` +- `Policy.Read.All`, `Policy.ReadWrite.ConditionalAccess` **Minimum Required** (For read-only operations): @@ -321,6 +329,9 @@ VARIABLE | REQUIRED | TYPE | DESCRIPTION [list group members](#action-list-group-members) - List the members in a group
[validate group](#action-validate-group) - Returns true if a user is in a group; otherwise, false
[list directory roles](#action-list-directory-roles) - List the directory roles that are activated in the tenant
+[list named locations](#action-list-named-locations) - List named locations in Entra Conditional Access
+[add cidr to named location](#action-add-cidr-to-named-location) - Add a CIDR range to an IP-based named location in Entra Conditional Access
+[remove cidr from named location](#action-remove-cidr-from-named-location) - Remove a CIDR range from an IP-based named location in Entra Conditional Access
[generate token](#action-generate-token) - Generate a token ## action: 'test connectivity' @@ -1163,6 +1174,99 @@ action_result.message | string | | Num directory roles: 9 | summary.total_objects | numeric | | 1 | summary.total_objects_successful | numeric | | 1 | +## action: 'list named locations' + +List named locations in Entra Conditional Access + +Type: **investigate**
+Read only: **True** + +For more information on using the filter and select parameters, refer to https://learn.microsoft.com/en-us/graph/filter-query-parameter and https://learn.microsoft.com/en-us/graph/query-parameters#select-parameter. + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**location_type** | optional | Type of named locations to return | string | | +**filter** | optional | Optional OData filter, for example contains(displayName,'Blocked') | string | | +**select** | optional | Optional select string to get specific properties. Separate multiple values with commas | string | | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.location_type | string | | all ip country | +action_result.parameter.filter | string | | contains(displayName,'Blocked') | +action_result.parameter.select | string | | id,displayName | +action_result.data.\*.@odata.type | string | | #microsoft.graph.ipNamedLocation #microsoft.graph.countryNamedLocation | +action_result.data.\*.id | string | `directory object id` | 0c0c6d27-93e7-4fd7-88a4-952c6d61a697 | +action_result.data.\*.displayName | string | | Named locations to be blocked | +action_result.data.\*.createdDateTime | string | `datetime` | 2021-03-23T04:59:25.8014022Z | +action_result.data.\*.modifiedDateTime | string | `datetime` | 2021-03-23T08:05:02.1027085Z | +action_result.data.\*.isTrusted | boolean | | True False | +action_result.data.\*.ipRanges.\*.@odata.type | string | | #microsoft.graph.iPv6CidrRange #microsoft.graph.iPv4CidrRange | +action_result.data.\*.ipRanges.\*.cidrAddress | string | `ip` `cidr` | 127.0.0.1/32 2001:8000::/20 | +action_result.data.\*.countriesAndRegions.\* | string | | US CA | +action_result.data.\*.countryLookupMethod | string | | clientIpAddress authenticatorAppGps | +action_result.data.\*.includeUnknownCountriesAndRegions | boolean | | True False | +action_result.summary.num_named_locations | numeric | | 2 | +action_result.message | string | | Successfully listed named locations | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'add cidr to named location' + +Add a CIDR range to an IP-based named location in Entra Conditional Access + +Type: **contain**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**cidr_range** | required | IPv4 or IPv6 CIDR range, for example 1.1.1.0/24 or 2001:db8::/32 | string | `ip` `cidr` | +**location_id** | required | Object ID of the IP-based named location | string | `directory object id` | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.cidr_range | string | `ip` `cidr` | 1.1.1.0/24 2001:db8::/32 | +action_result.parameter.location_id | string | `directory object id` | 0c0c6d27-93e7-4fd7-88a4-952c6d61a697 | +action_result.summary.status | string | | Successfully added 1.1.1.0/24 CIDR already present | +action_result.message | string | | Successfully added 1.1.1.0/24 CIDR already present in named location | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + +## action: 'remove cidr from named location' + +Remove a CIDR range from an IP-based named location in Entra Conditional Access + +Type: **correct**
+Read only: **False** + +#### Action Parameters + +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**cidr_range** | required | IPv4 or IPv6 CIDR range, for example 1.1.1.0/24 or 2001:db8::/32 | string | `ip` `cidr` | +**location_id** | required | Object ID of the IP-based named location | string | `directory object id` | + +#### Action Output + +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed | +action_result.parameter.cidr_range | string | `ip` `cidr` | 1.1.1.0/24 2001:db8::/32 | +action_result.parameter.location_id | string | `directory object id` | 0c0c6d27-93e7-4fd7-88a4-952c6d61a697 | +action_result.summary.status | string | | Successfully removed 1.1.1.0/24 CIDR not present | +action_result.message | string | | Successfully removed 1.1.1.0/24 CIDR not present in named location | +summary.total_objects | numeric | | 1 | +summary.total_objects_successful | numeric | | 1 | + ## action: 'generate token' Generate a token @@ -1189,7 +1293,7 @@ ______________________________________________________________________ Auto-generated Splunk SOAR Connector documentation. -Copyright 2025 Splunk Inc. +Copyright 2026 Splunk Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/__init__.py b/__init__.py index e43afc5..81ac8b1 100644 --- a/__init__.py +++ b/__init__.py @@ -1,6 +1,6 @@ # File: __init__.py # -# Copyright (c) 2022-2025 Splunk Inc. +# Copyright (c) 2022-2026 Splunk Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/manual_readme_content.md b/manual_readme_content.md index 129ddd2..9f571bc 100644 --- a/manual_readme_content.md +++ b/manual_readme_content.md @@ -75,6 +75,8 @@ Choose **either** Delegated OR Application permissions based on your use case: - `Group.ReadWrite.All` - `GroupMember.ReadWrite.All` - `RoleManagement.ReadWrite.Directory` + - `Policy.Read.All` + - `Policy.ReadWrite.ConditionalAccess` - `offline_access` 1. Click **Add permissions** 1. Click **Grant admin consent for [Your Organization]** @@ -92,6 +94,8 @@ Choose **either** Delegated OR Application permissions based on your use case: - `Group.ReadWrite.All` - `GroupMember.ReadWrite.All` - `RoleManagement.ReadWrite.Directory` + - `Policy.Read.All` + - `Policy.ReadWrite.ConditionalAccess` - `User-PasswordProfile.ReadWrite.All` 1. Click **Add permissions** 1. Click **Grant admin consent for [Your Organization]** @@ -200,6 +204,9 @@ The following table shows the minimum required permissions for each action: | **List Group Members** | `GroupMember.Read.All` | `GroupMember.Read.All` | Directory Readers | | **Validate Group** | `User.Read.All` | `User.Read.All` | Directory Readers | | **List Directory Roles** | `RoleManagement.Read.Directory` | `RoleManagement.Read.Directory` | Directory Readers | +| **List Named Locations** | `Policy.Read.All` | `Policy.Read.All` | Conditional Access Administrator | +| **Add CIDR to Named Location** | `Policy.Read.All`, `Policy.ReadWrite.ConditionalAccess` | `Policy.Read.All`, `Policy.ReadWrite.ConditionalAccess` | Conditional Access Administrator | +| **Remove CIDR from Named Location** | `Policy.Read.All`, `Policy.ReadWrite.ConditionalAccess` | `Policy.Read.All`, `Policy.ReadWrite.ConditionalAccess` | Conditional Access Administrator | ### Full vs Minimum Permissions @@ -207,6 +214,7 @@ The following table shows the minimum required permissions for each action: - `User.ReadWrite.All`, `Directory.ReadWrite.All`, `User.ManageIdentities.All` - `Group.ReadWrite.All`, `GroupMember.ReadWrite.All`, `RoleManagement.ReadWrite.Directory` +- `Policy.Read.All`, `Policy.ReadWrite.ConditionalAccess` **Minimum Required** (For read-only operations): diff --git a/msadgraph.json b/msadgraph.json index 93efb04..ea66975 100644 --- a/msadgraph.json +++ b/msadgraph.json @@ -14,7 +14,7 @@ "name": "Mathieu A. Cormier" } ], - "license": "Copyright (c) 2022-2025 Splunk Inc.", + "license": "Copyright (c) 2022-2026 Splunk Inc.", "app_version": "1.4.4", "python_version": "3.9, 3.13", "utctime_updated": "2025-10-07T16:21:23.014841Z", @@ -3792,6 +3792,421 @@ }, "versions": "EQ(*)" }, + { + "action": "list named locations", + "description": "List named locations in Entra Conditional Access", + "verbose": "For more information on using the filter and select parameters, refer to https://learn.microsoft.com/en-us/graph/filter-query-parameter and https://learn.microsoft.com/en-us/graph/query-parameters#select-parameter.", + "type": "investigate", + "identifier": "list_named_locations", + "read_only": true, + "parameters": { + "location_type": { + "description": "Type of named locations to return", + "data_type": "string", + "default": "all", + "order": 0, + "value_list": [ + "all", + "ip", + "country" + ] + }, + "filter": { + "description": "Optional OData filter, for example contains(displayName,'Blocked')", + "data_type": "string", + "order": 1 + }, + "select": { + "description": "Optional select string to get specific properties. Separate multiple values with commas", + "data_type": "string", + "order": 2 + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": [ + "success", + "failed" + ] + }, + { + "data_path": "action_result.parameter.location_type", + "data_type": "string", + "example_values": [ + "all", + "ip", + "country" + ], + "column_name": "Location Type", + "column_order": 0 + }, + { + "data_path": "action_result.parameter.filter", + "data_type": "string", + "example_values": [ + "contains(displayName,'Blocked')" + ], + "column_name": "Filter", + "column_order": 1 + }, + { + "data_path": "action_result.parameter.select", + "data_type": "string", + "example_values": [ + "id,displayName" + ], + "column_name": "Select", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.@odata.type", + "data_type": "string", + "example_values": [ + "#microsoft.graph.ipNamedLocation", + "#microsoft.graph.countryNamedLocation" + ], + "column_name": "Type", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string", + "contains": [ + "directory object id" + ], + "example_values": [ + "0c0c6d27-93e7-4fd7-88a4-952c6d61a697" + ], + "column_name": "Object ID", + "column_order": 4 + }, + { + "data_path": "action_result.data.*.displayName", + "data_type": "string", + "example_values": [ + "Named locations to be blocked" + ], + "column_name": "Display Name", + "column_order": 5 + }, + { + "data_path": "action_result.data.*.createdDateTime", + "data_type": "string", + "contains": [ + "datetime" + ], + "example_values": [ + "2021-03-23T04:59:25.8014022Z" + ], + "column_name": "Created", + "column_order": 6 + }, + { + "data_path": "action_result.data.*.modifiedDateTime", + "data_type": "string", + "contains": [ + "datetime" + ], + "example_values": [ + "2021-03-23T08:05:02.1027085Z" + ], + "column_name": "Modified", + "column_order": 7 + }, + { + "data_path": "action_result.data.*.isTrusted", + "data_type": "boolean", + "example_values": [ + true, + false + ], + "column_name": "Trusted", + "column_order": 8 + }, + { + "data_path": "action_result.data.*.ipRanges.*.@odata.type", + "data_type": "string", + "example_values": [ + "#microsoft.graph.iPv6CidrRange", + "#microsoft.graph.iPv4CidrRange" + ] + }, + { + "data_path": "action_result.data.*.ipRanges.*.cidrAddress", + "data_type": "string", + "contains": [ + "ip", + "cidr" + ], + "example_values": [ + "127.0.0.1/32", + "2001:8000::/20" + ] + }, + { + "data_path": "action_result.data.*.countriesAndRegions.*", + "data_type": "string", + "example_values": [ + "US", + "CA" + ] + }, + { + "data_path": "action_result.data.*.countryLookupMethod", + "data_type": "string", + "example_values": [ + "clientIpAddress", + "authenticatorAppGps" + ] + }, + { + "data_path": "action_result.data.*.includeUnknownCountriesAndRegions", + "data_type": "boolean", + "example_values": [ + true, + false + ] + }, + { + "data_path": "action_result.summary.num_named_locations", + "data_type": "numeric", + "example_values": [ + 2 + ] + }, + { + "data_path": "action_result.message", + "data_type": "string", + "example_values": [ + "Successfully listed named locations" + ] + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [ + 1 + ] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [ + 1 + ] + } + ], + "render": { + "type": "table" + }, + "versions": "EQ(*)" + }, + { + "action": "add cidr to named location", + "description": "Add a CIDR range to an IP-based named location in Entra Conditional Access", + "type": "contain", + "identifier": "add_cidr_to_named_location", + "read_only": false, + "parameters": { + "cidr_range": { + "description": "IPv4 or IPv6 CIDR range, for example 1.1.1.0/24 or 2001:db8::/32", + "data_type": "string", + "required": true, + "primary": true, + "order": 0, + "contains": [ + "ip", + "cidr" + ] + }, + "location_id": { + "description": "Object ID of the IP-based named location", + "data_type": "string", + "required": true, + "primary": true, + "order": 1, + "contains": [ + "directory object id" + ] + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": [ + "success", + "failed" + ], + "column_name": "Status", + "column_order": 2 + }, + { + "data_path": "action_result.parameter.cidr_range", + "data_type": "string", + "contains": [ + "ip", + "cidr" + ], + "example_values": [ + "1.1.1.0/24", + "2001:db8::/32" + ], + "column_name": "CIDR Range", + "column_order": 0 + }, + { + "data_path": "action_result.parameter.location_id", + "data_type": "string", + "contains": [ + "directory object id" + ], + "example_values": [ + "0c0c6d27-93e7-4fd7-88a4-952c6d61a697" + ], + "column_name": "Location ID", + "column_order": 1 + }, + { + "data_path": "action_result.summary.status", + "data_type": "string", + "example_values": [ + "Successfully added 1.1.1.0/24", + "CIDR already present" + ] + }, + { + "data_path": "action_result.message", + "data_type": "string", + "example_values": [ + "Successfully added 1.1.1.0/24", + "CIDR already present in named location" + ] + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [ + 1 + ] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [ + 1 + ] + } + ], + "render": { + "type": "table" + }, + "versions": "EQ(*)" + }, + { + "action": "remove cidr from named location", + "description": "Remove a CIDR range from an IP-based named location in Entra Conditional Access", + "type": "correct", + "identifier": "remove_cidr_from_named_location", + "read_only": false, + "parameters": { + "cidr_range": { + "description": "IPv4 or IPv6 CIDR range, for example 1.1.1.0/24 or 2001:db8::/32", + "data_type": "string", + "required": true, + "primary": true, + "order": 0, + "contains": [ + "ip", + "cidr" + ] + }, + "location_id": { + "description": "Object ID of the IP-based named location", + "data_type": "string", + "required": true, + "primary": true, + "order": 1, + "contains": [ + "directory object id" + ] + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": [ + "success", + "failed" + ], + "column_name": "Status", + "column_order": 2 + }, + { + "data_path": "action_result.parameter.cidr_range", + "data_type": "string", + "contains": [ + "ip", + "cidr" + ], + "example_values": [ + "1.1.1.0/24", + "2001:db8::/32" + ], + "column_name": "CIDR Range", + "column_order": 0 + }, + { + "data_path": "action_result.parameter.location_id", + "data_type": "string", + "contains": [ + "directory object id" + ], + "example_values": [ + "0c0c6d27-93e7-4fd7-88a4-952c6d61a697" + ], + "column_name": "Location ID", + "column_order": 1 + }, + { + "data_path": "action_result.summary.status", + "data_type": "string", + "example_values": [ + "Successfully removed 1.1.1.0/24", + "CIDR not present" + ] + }, + { + "data_path": "action_result.message", + "data_type": "string", + "example_values": [ + "Successfully removed 1.1.1.0/24", + "CIDR not present in named location" + ] + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [ + 1 + ] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [ + 1 + ] + } + ], + "render": { + "type": "table" + }, + "versions": "EQ(*)" + }, { "action": "generate token", "identifier": "generate_token", diff --git a/msadgraph_connector.py b/msadgraph_connector.py index 5f9cd55..1700431 100644 --- a/msadgraph_connector.py +++ b/msadgraph_connector.py @@ -1,6 +1,6 @@ # File: msadgraph_connector.py # -# Copyright (c) 2022-2025 Splunk Inc. +# Copyright (c) 2022-2026 Splunk Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import sys import time import urllib.parse as urlparse +from ipaddress import ip_network import encryption_helper import phantom.app as phantom @@ -1232,6 +1233,152 @@ def _handle_list_directory_roles(self, param): self.save_progress(f"Completed action handler for: {self.get_action_identifier()}") return action_result.set_status(phantom.APP_SUCCESS) + def _handle_list_named_locations(self, param): + self.save_progress(f"In action handler for: {self.get_action_identifier()}") + action_result = self.add_action_result(ActionResult(dict(param))) + + filter_string = param.get("filter") + location_type = param.get("location_type", "all") + select_string = param.get("select") + + headers = {"ConsistencyLevel": "eventual"} + parameters = {"$count": "true"} + + type_filter = None + if location_type == "ip": + type_filter = "isof('microsoft.graph.ipNamedLocation')" + elif location_type == "country": + type_filter = "isof('microsoft.graph.countryNamedLocation')" + + if filter_string: + filter_string = filter_string.strip() + if filter_string and type_filter: + parameters["$filter"] = f"{type_filter} and ({filter_string})" + elif filter_string: + parameters["$filter"] = filter_string + + if type_filter and "$filter" not in parameters: + parameters["$filter"] = type_filter + + if select_string: + select_values = [param_value.strip() for param_value in select_string.split(",")] + select_values = list(filter(None, select_values)) + if select_values: + parameters["$select"] = ",".join(select_values) + + endpoint = "/identity/conditionalAccess/namedLocations" + ret_val = self._handle_pagination(action_result, endpoint, headers=headers, params=parameters) + + if phantom.is_fail(ret_val): + return action_result.get_status() + + summary = action_result.update_summary({}) + resp_data = action_result.get_data() + if resp_data and resp_data[action_result.get_data_size() - 1] == "Empty response": + summary["num_named_locations"] = action_result.get_data_size() - 1 + else: + summary["num_named_locations"] = action_result.get_data_size() + + self.save_progress(f"Completed action handler for: {self.get_action_identifier()}") + return action_result.set_status(phantom.APP_SUCCESS, "Successfully listed named locations") + + def _handle_add_cidr_to_named_location(self, param): + self.save_progress(f"In action handler for: {self.get_action_identifier()}") + action_result = self.add_action_result(ActionResult(dict(param))) + + cidr_range = param["cidr_range"] + location_id = param["location_id"] + + try: + network = ip_network(cidr_range, strict=False) + except ValueError as e: + return action_result.set_status(phantom.APP_ERROR, f"Invalid CIDR range: {e}") + + cidr_type = "#microsoft.graph.iPv4CidrRange" if network.version == 4 else "#microsoft.graph.iPv6CidrRange" + normalized_cidr = str(network) + + endpoint = f"/identity/conditionalAccess/namedLocations/{location_id}" + ret_val, existing_location = self._make_rest_call_helper(action_result, endpoint, method="get") + + if phantom.is_fail(ret_val): + return action_result.get_status() + + location_type = existing_location.get("@odata.type") + if location_type != "#microsoft.graph.ipNamedLocation": + return action_result.set_status(phantom.APP_ERROR, f"Named location {location_id} is not an IP-based named location") + + current_ranges = existing_location.get("ipRanges", []) + if any(existing_range.get("cidrAddress") == normalized_cidr for existing_range in current_ranges): + summary = action_result.update_summary({}) + summary["status"] = "CIDR already present" + self.save_progress(f"CIDR {normalized_cidr} already exists in named location {location_id}") + return action_result.set_status(phantom.APP_SUCCESS, "CIDR already present in named location") + + updated_ranges = list(current_ranges) + updated_ranges.append({"@odata.type": cidr_type, "cidrAddress": normalized_cidr}) + + patch_body = {"@odata.type": "#microsoft.graph.ipNamedLocation", "ipRanges": updated_ranges} + ret_val, response = self._make_rest_call_helper(action_result, endpoint, json=patch_body, method="patch") + + if phantom.is_fail(ret_val): + return action_result.get_status() + + if response: + action_result.add_data(response) + + summary = action_result.update_summary({}) + summary["status"] = f"Successfully added {normalized_cidr}" + + self.save_progress(f"Completed action handler for: {self.get_action_identifier()}") + return action_result.set_status(phantom.APP_SUCCESS, f"Successfully added {normalized_cidr}") + + def _handle_remove_cidr_from_named_location(self, param): + self.save_progress(f"In action handler for: {self.get_action_identifier()}") + action_result = self.add_action_result(ActionResult(dict(param))) + + cidr_range = param["cidr_range"] + location_id = param["location_id"] + + try: + network = ip_network(cidr_range, strict=False) + except ValueError as e: + return action_result.set_status(phantom.APP_ERROR, f"Invalid CIDR range: {e}") + + normalized_cidr = str(network) + endpoint = f"/identity/conditionalAccess/namedLocations/{location_id}" + ret_val, existing_location = self._make_rest_call_helper(action_result, endpoint, method="get") + + if phantom.is_fail(ret_val): + return action_result.get_status() + + location_type = existing_location.get("@odata.type") + if location_type != "#microsoft.graph.ipNamedLocation": + return action_result.set_status(phantom.APP_ERROR, f"Named location {location_id} is not an IP-based named location") + + current_ranges = existing_location.get("ipRanges", []) + updated_ranges = [existing_range for existing_range in current_ranges if existing_range.get("cidrAddress") != normalized_cidr] + + if len(updated_ranges) == len(current_ranges): + summary = action_result.update_summary({}) + summary["status"] = "CIDR not present" + self.save_progress(f"CIDR {normalized_cidr} not found in named location {location_id}") + return action_result.set_status(phantom.APP_SUCCESS, "CIDR not present in named location") + + patch_body = {"@odata.type": "#microsoft.graph.ipNamedLocation", "ipRanges": updated_ranges} + ret_val, response = self._make_rest_call_helper(action_result, endpoint, json=patch_body, method="patch") + + if phantom.is_fail(ret_val): + return action_result.get_status() + + if response: + action_result.add_data(response) + + summary = action_result.update_summary({}) + summary["status"] = f"Successfully removed {normalized_cidr}" + + self.save_progress(f"Completed action handler for: {self.get_action_identifier()}") + return action_result.set_status(phantom.APP_SUCCESS, f"Successfully removed {normalized_cidr}") + def _handle_validate_group(self, param): self.save_progress(f"In action handler for: {self.get_action_identifier()}") action_result = self.add_action_result(ActionResult(dict(param))) @@ -1395,6 +1542,15 @@ def handle_action(self, param): elif action_id == "list_directory_roles": ret_val = self._handle_list_directory_roles(param) + elif action_id == "list_named_locations": + ret_val = self._handle_list_named_locations(param) + + elif action_id == "add_cidr_to_named_location": + ret_val = self._handle_add_cidr_to_named_location(param) + + elif action_id == "remove_cidr_from_named_location": + ret_val = self._handle_remove_cidr_from_named_location(param) + elif action_id == "generate_token": ret_val = self._handle_generate_token(param) diff --git a/msadgraph_consts.py b/msadgraph_consts.py index b07a8c4..c9a191a 100644 --- a/msadgraph_consts.py +++ b/msadgraph_consts.py @@ -1,6 +1,6 @@ # File: msadgraph_consts.py # -# Copyright (c) 2022-2025 Splunk Inc. +# Copyright (c) 2022-2026 Splunk Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -65,7 +65,8 @@ MS_TC_STATUS_SLEEP = 3 MS_AZURE_WAIT_FOR_URL_SLEEP = 5 MS_AZURE_CODE_GENERATION_SCOPE = "offline_access Group.ReadWrite.All User.Read.All User.ReadWrite.All Directory.ReadWrite.All \ -Directory.AccessAsUser.All User.ManageIdentities.All GroupMember.ReadWrite.All RoleManagement.ReadWrite.Directory" +Directory.AccessAsUser.All User.ManageIdentities.All GroupMember.ReadWrite.All RoleManagement.ReadWrite.Directory \ +Policy.Read.All Policy.ReadWrite.ConditionalAccess" MS_AZURE_AUTHORIZE_TROUBLESHOOT_MESSAGE = ( "If authorization URL fails to communicate with your SOAR instance, check whether you have: " " 1. Specified the Web Redirect URL of your App -- The Redirect URL should be /result . " diff --git a/msadgraph_get_group.html b/msadgraph_get_group.html index 029b999..adb3508 100644 --- a/msadgraph_get_group.html +++ b/msadgraph_get_group.html @@ -12,7 +12,7 @@ {% block widget_content %}