diff --git a/package-lock.json b/package-lock.json index 5837447ee..b5c56cee9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,7 +66,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -624,6 +623,19 @@ "@noble/ciphers": "^1.0.0" } }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@inquirer/ansi": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.1.tgz", @@ -786,12 +798,13 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.24.0.tgz", - "integrity": "sha512-D8h5KXY2vHFW8zTuxn2vuZGN0HGrQ5No6LkHwlEA9trVgNdPL3TF1dSqKA7Dny6BbBYKSW/rOBDXdC8KJAjUCg==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", + "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", "dev": true, "license": "MIT", "dependencies": { + "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -799,13 +812,15 @@ "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", - "express": "^5.0.1", - "express-rate-limit": "^7.5.0", - "jose": "^6.1.1", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.0" + "zod-to-json-schema": "^3.25.1" }, "engines": { "node": ">=18" @@ -847,7 +862,6 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -1115,9 +1129,9 @@ } }, "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "dev": true, "license": "MIT", "dependencies": { @@ -1127,7 +1141,7 @@ "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", + "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" }, @@ -1177,7 +1191,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -1432,16 +1445,17 @@ } }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", "dev": true, "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/content-type": { @@ -1860,7 +1874,6 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -1900,11 +1913,14 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", "dev": true, "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, "engines": { "node": ">= 16" }, @@ -2038,9 +2054,9 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "dev": true, "license": "MIT", "dependencies": { @@ -2052,7 +2068,11 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/formdata-polyfill": { @@ -2305,31 +2325,35 @@ "dev": true, "license": "MIT" }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "node_modules/hono": { + "version": "4.11.7", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", + "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", "dev": true, "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, "engines": { - "node": ">= 0.8" + "node": ">=16.9.0" } }, - "node_modules/http-errors/node_modules/statuses": { + "node_modules/http-errors": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, "engines": { "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/https-proxy-agent": { @@ -2357,9 +2381,9 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "dev": true, "license": "MIT", "dependencies": { @@ -2407,6 +2431,16 @@ "dev": true, "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -2633,6 +2667,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2815,16 +2856,20 @@ } }, "node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "dev": true, "license": "MIT", "dependencies": { "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-fn": { @@ -3265,7 +3310,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -3424,16 +3468,16 @@ } }, "node_modules/raw-body": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz", - "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "dev": true, "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.7.0", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { "node": ">= 0.10" @@ -3601,27 +3645,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -3640,32 +3663,36 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "dev": true, "license": "MIT", "dependencies": { @@ -3676,6 +3703,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -4402,15 +4433,14 @@ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } }, "node_modules/zod-to-json-schema": { - "version": "3.25.0", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.0.tgz", - "integrity": "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==", + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "dev": true, "license": "ISC", "peerDependencies": { diff --git a/src/powershell/private/core/Add-ZtTestResultDetail.ps1 b/src/powershell/private/core/Add-ZtTestResultDetail.ps1 index 13f81ddaa..be3d7191f 100644 --- a/src/powershell/private/core/Add-ZtTestResultDetail.ps1 +++ b/src/powershell/private/core/Add-ZtTestResultDetail.ps1 @@ -67,7 +67,7 @@ function Add-ZtTestResultDetail { [ValidateSet('NotConnectedAzure', 'NotConnectedExchange', 'NotDotGovDomain', 'NotLicensedEntraIDP1', 'NotConnectedSecurityCompliance', 'NotLicensedEntraIDP2', 'NotLicensedEntraIDGovernance', 'NotLicensedEntraWorkloadID', 'NotSupported', 'UnderConstruction', - 'NotLicensedIntune', 'NoAzureAccess' + 'NotLicensedIntune', 'NoAzureAccess', 'NotApplicable' )] [string] $SkippedBecause, diff --git a/src/powershell/private/core/Get-ZtSkippedReason.ps1 b/src/powershell/private/core/Get-ZtSkippedReason.ps1 index a8b0571ce..3a896a46d 100644 --- a/src/powershell/private/core/Get-ZtSkippedReason.ps1 +++ b/src/powershell/private/core/Get-ZtSkippedReason.ps1 @@ -21,6 +21,7 @@ function Get-ZtSkippedReason { "NotLicensedIntune" { "This test is for tenants that are licensed for Microsoft Intune. See [Microsoft Intune licensing](https://learn.microsoft.com/intune/intune-service/fundamentals/licenses)"; break} "NotSupported" { "This test relies on capabilities not currently available (e.g., cmdlets that are not available on all platforms, Resolve-DnsName)"; break} "NoAzureAccess" { "The signed in user does not have access to the Azure subscription to perform this test."; break} + "NotApplicable" { "This test is not applicable to the current environment."; break} default { $SkippedBecause; break} } } diff --git a/src/powershell/private/tests-shared/Get-ApplicationNameFromId.ps1 b/src/powershell/private/tests-shared/Get-ApplicationNameFromId.ps1 new file mode 100644 index 000000000..9d0964dc8 --- /dev/null +++ b/src/powershell/private/tests-shared/Get-ApplicationNameFromId.ps1 @@ -0,0 +1,82 @@ +function Get-ApplicationNameFromId { + <# + .SYNOPSIS + Resolves application GUIDs to display names from the database. + + .DESCRIPTION + Takes an array of targets (GUIDs or strings) and resolves GUIDs to application display names + by querying the database for ServicePrincipal and Application data. + + .PARAMETER TargetsArray + Array of target values (GUIDs or strings like 'AllApplications') + + .PARAMETER Database + Database connection to query + + .EXAMPLE + $resolved = Get-ApplicationNameFromId -TargetsArray $targets -Database $db + Returns an array of resolved display names + + .OUTPUTS + Array of strings (resolved names or original values) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string[]]$TargetsArray, + + [Parameter(Mandatory = $true)] + $Database + ) + + $displayArray = @() + $targetMap = @{} + # Use HashSet for deduplication of GUIDs to query + $guidsToQuery = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) + + # 1. Classification & Deduplication + foreach ($target in $TargetsArray) { + $targetMap[$target] = $target # Default fallback + + $guidRef = [System.Guid]::Empty + if ([System.Guid]::TryParse($target, [ref]$guidRef)) { + [void]$guidsToQuery.Add($target) + } + } + + # 2. Query + if ($guidsToQuery.Count -gt 0) { + try { + # Build IN clause for all GUIDs + $guidInClause = ($guidsToQuery | ForEach-Object { "'$($_.Replace("'", "''"))'" }) -join ',' + + # Single query to resolve all GUIDs at once + $sqlApp = @" +SELECT id, appId, displayName FROM ServicePrincipal WHERE id IN ($guidInClause) OR appId IN ($guidInClause) +UNION +SELECT id, appId, displayName FROM Application WHERE id IN ($guidInClause) OR appId IN ($guidInClause) +"@ + $resolvedApps = Invoke-DatabaseQuery -Database $Database -Sql $sqlApp + + # 3. Build Lookup Hash + foreach ($app in $resolvedApps) { + if (-not [string]::IsNullOrEmpty($app.displayName)) { + # Handle DB returning Guid objects by forcing string conversion for keys + if ($app.id) { $targetMap["$($app.id)"] = $app.displayName } + if ($app.appId) { $targetMap["$($app.appId)"] = $app.displayName } + } + } + } + catch { + Write-PSFMessage -Level Warning -Message "Failed to resolve application GUIDs from database: $_" + } + } + + # 4. Reconstruct Output + foreach ($target in $TargetsArray) { + $displayArray += $targetMap[$target] + } + + # Comma operator prevents PowerShell from unrolling single-element arrays + return ,$displayArray +} diff --git a/src/powershell/tests/Test-Assessment.25539.md b/src/powershell/tests/Test-Assessment.25539.md new file mode 100644 index 000000000..0bba2c3eb --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25539.md @@ -0,0 +1,13 @@ +Azure Firewall Premium offers signature-based IDPS to quickly detect attacks by identifying specific patterns, such as byte sequences in network traffic or known malicious instruction sequences used by malware. These IDPS signatures apply to both application and network-level traffic (Layers 3-7). They are fully managed and continuously updated. IDPS can be applied to inbound, spoke-to-spoke (East-West), and outbound traffic, including traffic to/from an on-premises network. + +This check verifies that the Intrusion Detection and Prevention System (IDPS) is enabled in “Alert and deny” mode in the Azure Firewall policy configuration. The check will fail if Intrusion Detection and Prevention System (IDPS) is either Disabled (Off) or if it is configured in “Alert” only mode, in the firewall policy attached to the firewall. + +If this check does not pass, it means that the Intrusion Detection and Prevention System (IDPS) is not analyzing, detecting and actively blocking malicious patterns in legitimate looking traffic. + +**Remediation action** + +- Please check the IDPS section of this article for guidance on how to enable Intrusion Detection and Prevention System (IDPS) in “Alert and Deny” mode in the Azure Firewall Policy. + +- [Azure Firewall Premium features implementation guide | Microsoft Learn](https://learn.microsoft.com/en-us/azure/firewall/premium-features) + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.25539.ps1 b/src/powershell/tests/Test-Assessment.25539.ps1 new file mode 100644 index 000000000..be9599795 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25539.ps1 @@ -0,0 +1,265 @@ +<# +.SYNOPSIS + Validates Intrusion Detection is Enabled in Deny Mode on Azure Firewall. +.DESCRIPTION + This test validates that Azure Firewall Policies have Intrusion Detection enabled in Deny mode. + Checks all firewall policies in the subscription and reports their intrusion detection status. +.NOTES + Test ID: 25539 + Category: Azure Network Security + Required API: Azure Firewall Policies +#> + +function Test-Assessment-25539 { + [ZtTest( + Category = 'Azure Network Security', + ImplementationCost = 'Low', + MinimumLicense = ('Azure_Firewall_Premium'), + Pillar = 'Network', + RiskLevel = 'High', + SfiPillar = 'Protect networks', + TenantType = ('Workforce','External'), + TestId = 25539, + Title = 'IDPS Inspection is Enabled in Deny Mode on Azure Firewall', + UserImpact = 'Low' + )] + [CmdletBinding()] + param() + + #Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + + #region Data Collection + $activity = 'Azure Firewall Intrusion Detection' + Write-ZtProgress ` + -Activity $activity ` + -Status 'Checking Azure connection' + + # Check if connected to Azure + $azContext = Get-AzContext -ErrorAction SilentlyContinue + if (-not $azContext) { + Write-PSFMessage 'Not connected to Azure.' -Level Warning + Add-ZtTestResultDetail -SkippedBecause NotConnectedAzure + return + } + + # Check the supported environment + Write-ZtProgress -Activity $activity -Status 'Checking Azure environment' + if ($azContext.Environment.Name -ne 'AzureCloud') { + Write-PSFMessage 'This test is only applicable to the AzureCloud environment.' -Tag Test -Level VeryVerbose + Add-ZtTestResultDetail -SkippedBecause NotApplicable + return + } + + Write-ZtProgress ` + -Activity $activity ` + -Status 'Enumerating Firewall Policies' + + # Query subscriptions using REST API + $resourceManagerUrl = $azContext.Environment.ResourceManagerUrl.TrimEnd('/') + $subscriptionsUri = "$resourceManagerUrl/subscriptions?api-version=2025-03-01" + + try { + $subscriptionsResponse = Invoke-AzRestMethod -Method GET -Uri $subscriptionsUri -ErrorAction Stop + + if ($subscriptionsResponse.StatusCode -eq 403) { + Write-PSFMessage 'The signed in user does not have access to check subscriptions.' -Tag Firewall -Level Warning + Add-ZtTestResultDetail -SkippedBecause NoAzureAccess + return + } + + if ($subscriptionsResponse.StatusCode -ge 400) { + Write-PSFMessage "Subscriptions request failed with status code $($subscriptionsResponse.StatusCode)" -Tag Firewall -Level Warning + Add-ZtTestResultDetail -SkippedBecause NoAzureAccess + return + } + + $subscriptionsContent = $subscriptionsResponse.Content + $subscriptions = ($subscriptionsContent | ConvertFrom-Json).value + } + catch { + Write-PSFMessage "Unable to enumerate subscriptions: $($_.Exception.Message)" -Tag Firewall -Level Warning + Add-ZtTestResultDetail -SkippedBecause NoAzureAccess + return + } + + $results = @() + + foreach ($sub in $subscriptions) { + + # Switch subscription context + try { + Set-AzContext -SubscriptionId $sub.subscriptionId -ErrorAction Stop | Out-Null + } + catch { + Write-PSFMessage "Unable to switch to subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + continue + } + + # Query Azure Firewall Policies + try { + $policiesUri = "$resourceManagerUrl/subscriptions/$($sub.subscriptionId)/providers/Microsoft.Network/firewallPolicies?api-version=2025-03-01" + Write-ZtProgress -Activity $activity -Status "Enumerating policies in subscription $($sub.displayName)" + + $policyResponse = Invoke-AzRestMethod -Method GET -Uri $policiesUri -ErrorAction Stop + + if ($policyResponse.StatusCode -eq 403) { + Write-PSFMessage "Access denied to firewall policies in subscription $($sub.displayName): Insufficient permissions" -Tag Firewall -Level Warning + continue + } + + if ($policyResponse.StatusCode -ge 400) { + Write-PSFMessage "Firewall policies request failed with status code $($policyResponse.StatusCode)" -Tag Firewall -Level Warning + continue + } + + $policyResponseContent = $policyResponse.Content + if (-not $policyResponseContent) { + Write-PSFMessage "No response content for policies in subscription $($sub.displayName)" -Tag Firewall -Level Warning + continue + } + + $policies = ($policyResponseContent | ConvertFrom-Json).value + } + catch { + Write-PSFMessage "Unable to enumerate firewall policies in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + continue + } + + if (-not $policies) { continue } + + # Get individual firewall policy details + $detailedPolicies = @() + foreach ($policyResource in $policies) { + try { + $detailUri = "$resourceManagerUrl$($policyResource.id)?api-version=2025-03-01" + $detailResponse = Invoke-AzRestMethod -Method GET -Uri $detailUri -ErrorAction Stop + + if ($detailResponse.StatusCode -eq 403) { + Write-PSFMessage "Access denied to firewall policy details in subscription $($sub.displayName): Insufficient permissions" -Tag Firewall -Level Warning + continue + } + + if ($detailResponse.StatusCode -ge 400) { + Write-PSFMessage "Firewall policy details request failed with status code $($detailResponse.StatusCode)" -Tag Firewall -Level Warning + continue + } + + $detailResponseContent = $detailResponse.Content + if (-not $detailResponseContent) { + Write-PSFMessage "No response content for policy $($policyResource.name) in subscription $($sub.displayName)" -Tag Firewall -Level Warning + continue + } + + $detailedPolicy = $detailResponseContent | ConvertFrom-Json + $detailedPolicies += $detailedPolicy + } + catch { + Write-PSFMessage "Unable to get detailed policy information for $($policyResource.name) in subscription $($sub.displayName): $($_.Exception.Message)" -Tag Firewall -Level Warning + } + } + + # Check intrusion detection mode for each firewall policy + foreach ($policyResource in $detailedPolicies) { + + # Skip if policy is missing required properties + if (-not $policyResource -or -not $policyResource.Name -or -not $policyResource.Id -or -not $policyResource.properties) { + Write-PSFMessage "Firewall policy is missing required properties. Skipping." -Tag Firewall -Level Verbose + continue + } + + # Skip if SKU tier is not Premium + if ($policyResource.properties.sku.tier -ne 'Premium') { + Write-PSFMessage "Firewall policy '$($policyResource.name)' does not have Premium SKU. Skipping." -Tag Firewall -Level Verbose + continue + } + + # Get intrusion detection mode - if not configured, it's disabled by default (FAIL) + $idMode = if ($policyResource.properties.intrusionDetection) { + $policyResource.properties.intrusionDetection.mode + } else { + 'Off' + } + # Map intrusion detection mode to user-friendly display values + $detectionModeDisplay = switch ($idMode) { + 'Deny' { 'Alert and Deny' } + 'Alert' { 'Alert Only' } + 'Off' { 'Disabled' } + } + + $subContext = Get-AzContext + + $results += [PSCustomObject]@{ + PolicyName = $policyResource.Name + SubscriptionName = $subContext.Subscription.Name + SubscriptionId = $subContext.Subscription.Id + IntrusionDetectionMode = $detectionModeDisplay + PolicyID = $policyResource.Id + Passed = $idMode -eq 'Deny' + } + } + } + #endregion Data Collection + + #region Assessment Logic + + # If no Premium firewall policies found, skip the test + if ($results.Count -eq 0) { + Write-PSFMessage 'No Azure Firewall Premium policies found to evaluate.' -Tag Firewall -Level Verbose + Add-ZtTestResultDetail -SkippedBecause NotApplicable + return + } + + $failedPolicies = @($results | Where-Object { -not $_.Passed }) + $passed = $failedPolicies.Count -eq 0 + + if ($passed) { + $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is set to Deny for Azure Firewall policies.`n`n%TestResult%" + } + else { + $testResultMarkdown = "Intrusion Detection System (IDPS) inspection is not set to Deny for Azure Firewall policies.`n`n%TestResult%" + } + #endregion Assessment Logic + + #region Report Generation + $reportTitle = "Firewall policies" + $tableRows = "" + $mdInfo = "" + + if ($results.Count -gt 0) { + # Create a here-string with format placeholders {0}, {1}, etc. + $formatTemplate = @' + +## {0} + +| Policy name | Subscription name | Result | +| :--- | :--- | :--- | +{1} + +'@ + + foreach ($item in $results | Sort-Object PolicyName) { + $policyLink = "https://portal.azure.com/#resource$($item.PolicyID)" + $subLink = "https://portal.azure.com/#resource/subscriptions/$($item.SubscriptionId)" + $policyMd = "[$(Get-SafeMarkdown -Text $item.PolicyName)]($policyLink)" + $subMd = "[$(Get-SafeMarkdown -Text $item.SubscriptionName)]($subLink)" + $icon = if ($item.Passed) { '✅' } else { '❌' } + $resultText = "$icon $($item.IntrusionDetectionMode)" + $tableRows += "| $policyMd | $subMd | $resultText |`n" + } + + # Format the template by replacing placeholders with values + $mdInfo = $formatTemplate -f $reportTitle, $tableRows + } + + # Replace the placeholder with the detailed information + $testResultMarkdown = $testResultMarkdown -replace "%TestResult%", $mdInfo + #endregion Report Generation + + $params = @{ + TestId = '25539' + Status = $passed + Result = $testResultMarkdown + } + + Add-ZtTestResultDetail @params +} diff --git a/src/powershell/tests/Test-Assessment.35039.md b/src/powershell/tests/Test-Assessment.35039.md new file mode 100644 index 000000000..ab1d058f8 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35039.md @@ -0,0 +1,39 @@ +Communication Compliance policies with Copilot content detection enable organizations to monitor and investigate how users interact with Microsoft Copilot in Teams, Outlook, and Microsoft 365 apps. Without Communication Compliance policies configured to capture Copilot interactions, organizations cannot detect when sensitive data is being exposed to AI services, how users are leveraging Copilot with confidential information, or detect potential policy violations involving AI-assisted data processing. + +Copilot interaction capture through Communication Compliance enables organizations to implement governance and oversight of AI usage while maintaining user communication privacy controls. Users may unknowingly expose sensitive data (customer records, financial information, source code, trade secrets) to Copilot, creating a data spillage risk that becomes invisible without activity monitoring. Organizations must enable Communication Compliance policies targeting Copilot interactions to maintain visibility into how AI features are being used with sensitive data and ensure compliance with data governance policies. + +**Remediation action** + +To create and enable Communication Compliance policies for Copilot interaction capture: + +1. Sign in as a Global Administrator or Compliance Administrator to the [Microsoft Purview portal](https://purview.microsoft.com) +2. Navigate to Communication Compliance > Policies +3. Select "+ Create policy" to start the policy creation workflow +4. Choose the "Monitor for sensitive content" template or create a custom policy +5. Name the policy (e.g., "Copilot Data Protection") +6. Configure the scope (all users or specific groups) +7. On the Conditions page, add conditions to detect: + - Sensitive information types (credit cards, SSN, financial data) + - Keywords related to confidential data + - Custom patterns for your organization's sensitive data +8. On the Review settings page, configure: + - Reviewers (compliance team members) + - Alert volume preference + - Review mailbox for alerts +9. Enable the policy +10. Verify rule creation via PowerShell using Query 1 and 2 + +Via PowerShell (creation requires portal, but verification via cmdlets): + +```powershell +Connect-ExchangeOnline +Get-SupervisoryReviewRule -IncludeRuleXml | Select-Object Name, Policy +Get-SupervisoryReviewPolicyV2 | Select-Object Name, Enabled, ReviewMailbox +``` + +For more information: +- [Communication Compliance overview](https://learn.microsoft.com/en-us/purview/communication-compliance) +- [Create Communication Compliance policies](https://learn.microsoft.com/en-us/purview/communication-compliance-policies) +- [SupervisoryReview cmdlet reference](https://learn.microsoft.com/en-us/powershell/module/exchange/get-supervisoryreviewpolicyv2) + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.35039.ps1 b/src/powershell/tests/Test-Assessment.35039.ps1 new file mode 100644 index 000000000..1b38e74ea --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35039.ps1 @@ -0,0 +1,253 @@ +<# +.SYNOPSIS + Validates that Communication Compliance rules are configured to detect and monitor Copilot content. + +.DESCRIPTION + This test verifies that Communication Compliance rules targeting Copilot interactions are properly + configured and enabled. It checks that supervisory review policies with Copilot-targeting rules + are active and have configured review mailboxes for processing alerts. + +.NOTES + Test ID: 35039 + Category: Data Security Posture Management + Pillar: Data + Required Module: ExchangeOnlineManagement + Required Connection: Security & Compliance PowerShell +#> + +function Test-Assessment-35039 { + [ZtTest( + Category = 'Data Security Posture Management', + ImplementationCost = 'Medium', + MinimumLicense = ('Microsoft 365 E5'), + Pillar = 'Data', + RiskLevel = 'High', + SfiPillar = 'Protect tenants and production systems', + TenantType = ('Workforce'), + TestId = 35039, + Title = 'Copilot Communication Compliance Monitoring Configured', + UserImpact = 'Medium' + )] + [CmdletBinding()] + param() + + #region Data Collection + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + + $activity = 'Checking Communication Compliance Rules for Copilot Content' + Write-ZtProgress -Activity $activity -Status 'Getting supervisory review rules' + + # Q1: Find Communication Compliance rules targeting Copilot content + $copilotRules = @() + $errorMsg = $null + + try { + $allRules = Get-SupervisoryReviewRule -IncludeRuleXml -ErrorAction Stop + $allReviewPolicy = Get-SupervisoryReviewPolicyV2 + + foreach ($rule in $allRules) { + if (-not [string]::IsNullOrWhiteSpace($rule.RuleXml)) { + try { + # Wrap RuleXml in a root element to handle multiple rule elements + $wrappedXml = "$($rule.RuleXml)" + $ruleXml = [xml]$wrappedXml + $hasCopilotConfig = $false + + # Check for Copilot in Workloads array within JSON value elements + if ($ruleXml.root) { + $valueElements = $ruleXml.root.GetElementsByTagName('value') + foreach ($valueElement in $valueElements) { + if (-not [string]::IsNullOrWhiteSpace($valueElement.'#text')) { + try { + $jsonData = $valueElement.'#text' | ConvertFrom-Json + if ($jsonData.Workloads -and $jsonData.Workloads -contains 'Copilot') { + $hasCopilotConfig = $true + break + } + } + catch { + # Skip if JSON parsing fails + } + } + } + } + + if ($hasCopilotConfig) { + # Lookup policy name from $allReviewPolicy using Policy ID + $policyId = $rule.Policy + $policyName = ($allReviewPolicy | Where-Object { $_.Guid -eq $policyId }).Name + + $copilotRules += [PSCustomObject]@{ + RuleName = $rule.Name + PolicyId = $policyId + PolicyName = if ($policyName) { $policyName } else { 'Unknown' } + } + } + } + catch { + Write-PSFMessage "Error parsing RuleXml for rule '$($rule.Name)': $_" -Level Warning + } + } + + } + } + catch { + $errorMsg = $_ + Write-PSFMessage "Failed to retrieve supervisory review rules: $_" -Tag Test -Level Warning + } + + # Q2: Resolve Copilot-targeting policies and verify enabled status + $enabledCopilotPolicies = @() + if ($copilotRules -and -not $errorMsg) { + #Write-ZtProgress -Activity $activity -Status 'Verifying policy enabled status' + + try { + $copilotPolicyIdentities = @($copilotRules | Select-Object -ExpandProperty PolicyId -Unique) + $policies = foreach ($id in $copilotPolicyIdentities) { + $allReviewPolicy | Where-Object { $_.Guid -eq $id } + } + $enabledCopilotPolicies = @($policies | Where-Object { $_ -and $_.Enabled -eq $true }) + } + catch { + Write-PSFMessage "Failed to retrieve supervisory review policies: $_" -Tag Test -Level Warning + } + } + + # Q3: Verify Copilot capture is active by checking audit logs (optional) + $policyHits = $null + if ($enabledCopilotPolicies) { + Write-ZtProgress -Activity $activity -Status 'Checking audit logs' + + try { + $startDate = (Get-Date).AddDays(-30) + $endDate = Get-Date + $hits = Search-UnifiedAuditLog -StartDate $startDate -EndDate $endDate -Operations SupervisionRuleMatch -ErrorAction Stop + + if ($hits) { + $policyNamePattern = ($enabledCopilotPolicies.Name | ForEach-Object { [regex]::Escape($_) }) -join '|' + $policyHits = @($hits | Where-Object { $_.AuditData -match $policyNamePattern -and ($_.AuditData -match 'Copilot') }) + } + } + catch { + Write-PSFMessage "Failed to check audit logs: $_" -Tag Test -Level Warning + } + } + #endregion Data Collection + + #region Assessment Logic + $passed = $false + + # Evaluation Logic: + # 1. If Query 1 returns at least 1 rule with Copilot in RuleXml, proceed to Query 2 + if ($copilotRules.Count -gt 0) { + # 2. If Query 2 returns at least 1 enabled policy with ReviewMailbox configured, then Pass + $hasValidPolicies = @($enabledCopilotPolicies | Where-Object { $_.ReviewMailbox }).Count -gt 0 + $passed = $hasValidPolicies + } + # 3. If Query 1 returns no rules or Query 2 returns no enabled policies, then Fail + else { + $passed = $false + } + # Query 3 (audit logs) is optional and used only for evidence display + #endregion Assessment Logic + + #region Report Generation + $mdInfo = '' + + if ($passed) { + $statusIcon = '✅ Pass' + $statusMessage = 'Communication Compliance rules targeting Copilot content are properly configured and enabled.' + } + else { + $statusIcon = '❌ Fail' + $statusMessage = 'Communication Compliance rules targeting Copilot content are not properly configured or enabled.' + } + + # Copilot-Targeting Rules section + if ($copilotRules -and $copilotRules.Count -gt 0) { + $rulesTableRows = '' + foreach ($rule in $copilotRules | Sort-Object RuleName) { + $rulesTableRows += "| $($rule.RuleName) | $($rule.PolicyName) |`n" + } + + $rulesTemplate = @' + +### Copilot-Targeting Rules + +| Rule Name | Associated Policy | +| :------ | :---- | +{0} +'@ + $mdInfo += $rulesTemplate -f $rulesTableRows + } + else { + $mdInfo += "`n### Copilot-Targeting Rules`n`nNo Copilot-targeting rules found.`n" + } + + # Enabled Policies section + if ($enabledCopilotPolicies -and $enabledCopilotPolicies.Count -gt 0) { + $policiesTableRows = '' + foreach ($policy in $enabledCopilotPolicies | Sort-Object Name) { + $reviewMailbox = if ($policy.ReviewMailbox) { $policy.ReviewMailbox } else { 'Not configured' } + $enabledStatus = if ($policy.Enabled -eq $true) { 'True' } else { 'False' } + $policiesTableRows += "| $($policy.Name) | $enabledStatus | $reviewMailbox |`n" + } + + $policiesTemplate = @' + +### Enabled Policies + +| Policy Name | Enabled | Review Mailbox | +| :------ | :---- | :---- | +{0} +'@ + $mdInfo += $policiesTemplate -f $policiesTableRows + } + else { + $mdInfo += "`n### Enabled Policies`n`nNo enabled policies with Copilot rules found.`n" + } + + # Activity Evidence section + $evidenceText = if ($policyHits -and $policyHits.Count -gt 0) { + "Recent Copilot Matches (30 days): $($policyHits.Count)" + } + elseif ($enabledCopilotPolicies -and $enabledCopilotPolicies.Count -gt 0) { + "Recent Copilot Matches (30 days): 0" + } + else { + "Recent Copilot Matches (30 days): No policies configured for audit review." + } + + $mdInfo += "`n### Activity Evidence`n`n$evidenceText`n" + + # Summary + $summaryTemplate = @' + +**Summary:** + + Status: {0} + + Total Copilot Rules Found: {1} + + Enabled Policies with Copilot Rules: {2} + +**Portal Access:** + + [Microsoft Purview Communication Compliance > Policies](https://purview.microsoft.com/communicationcompliance/policies) + +'@ + + $mdInfo += $summaryTemplate -f $statusIcon, $copilotRules.Count, $enabledCopilotPolicies.Count + + $testResultMarkdown = "$statusMessage`n$mdInfo" + + #endregion Report Generation + + $params = @{ + TestId = '35039' + Status = $passed + Result = $testResultMarkdown + } + + Add-ZtTestResultDetail @params +}