diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 671a3ea..0fe0391 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.1.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.11.7
+ rev: v0.15.12
hooks:
- id: ruff
args: [ "--fix", "--unsafe-fixes"] # Allow unsafe fixes (ruff pretty strict about what it can fix)
@@ -43,15 +43,14 @@ 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.136.0
+ rev: v1.154.0
hooks:
- id: semgrep
- additional_dependencies: ["setuptools==81.0.0"]
- repo: https://github.com/Yelp/detect-secrets
rev: v1.5.0
hooks:
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..af81603 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 not filter_string:
+ 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 %}