├── README.md
├── AzureFunctionUnsupportedRuntimeCheck.ps1
├── AzureBlobContainerCreation.ps1
├── AzureADCheckSecretsToExpire.ps1
├── StartStopAzureSQLVMWithDowngradePremiumDisks.ps1
├── PowershellAzureFileShare.ps1
├── D365BCAppSourceUpdatesNotifications.ps1
├── BlobStorageLatencyTest.ps1
├── ApplicationInsightsPurgeTelemetry.ps1
├── CreateApplicationInsightsD365BCTelemetryConfiguration.ps1
└── EntraIDGuestUsersExport.ps1
/README.md:
--------------------------------------------------------------------------------
1 | # PowershellCloudScripts
2 | Powershell scripts for managing Azure and Dynamics 365 Business Central tasks
3 |
--------------------------------------------------------------------------------
/AzureFunctionUnsupportedRuntimeCheck.ps1:
--------------------------------------------------------------------------------
1 | $functionRuntime=@{l="FunctionRuntimeVersion";e={(Get-AzFunctionAppSetting -Name $_.Name -ResourceGroupName $_.ResourceGroupName)["FUNCTIONS_EXTENSION_VERSION"]}}
2 |
3 | (Get-AzFunctionApp | Where-Object { $(if ((Get-AzFunctionAppSetting -Name $_.Name -ResourceGroupName $_.ResourceGroupName) -eq $null) {""} else {(Get-AzFunctionAppSetting -Name $_.Name -ResourceGroupName $_.ResourceGroupName)["FUNCTIONS_EXTENSION_VERSION"]}) -ne "~4" } ) | Select-Object Name,ResourceGroupname,$functionRuntime |Format-Table -AutoSize
--------------------------------------------------------------------------------
/AzureBlobContainerCreation.ps1:
--------------------------------------------------------------------------------
1 | ################# Azure Blob Storage - PowerShell ####################
2 |
3 | ## Input Parameters
4 | $resourceGroupName="YOUR_RESOURCE_GROUP_NAME"
5 | $storageAccountName="YOUR_STORAGE_ACCOUNT_NAME"
6 | $storageContainerName="YOUR_CONTAINER_NAME"
7 |
8 | ## Connect to Azure Account
9 | Connect-AzAccount
10 |
11 | ## Function to create the storage container
12 | Function CreateStorageContainer
13 | {
14 | Write-Host "Creating storage container.."
15 | ## Get the storage account in which container has to be created
16 | $storageAccount=Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName
17 | ## Get the storage account context
18 | $ctx=$storageAccount.Context
19 |
20 | ## Check if the storage container exists
21 | if(Get-AzStorageContainer -Name $storageContainerName -Context $ctx -ErrorAction SilentlyContinue)
22 | {
23 | Write-Host $storageContainerName "- container already exists."
24 | }
25 | else
26 | {
27 | Write-Host $storageContainerName "- container does not exist."
28 | ## Create a new Azure Storage Account
29 | New-AzStorageContainer -Name $storageContainerName -Context $ctx -Permission Container
30 | }
31 | }
32 |
33 | CreateStorageContainer
34 |
35 | ## Disconnect from Azure Account
36 | Disconnect-AzAccount
37 |
--------------------------------------------------------------------------------
/AzureADCheckSecretsToExpire.ps1:
--------------------------------------------------------------------------------
1 | Connect-AzureAD
2 |
3 | $LimitExpirationDays = 31 #secret expiration date filter
4 |
5 | #Retrieving the list of secrets that expires in the above days
6 | $SecretsToExpire = Get-AzureADApplication -All:$true | ForEach-Object {
7 | $app = $_
8 | @(
9 | Get-AzureADApplicationPasswordCredential -ObjectId $_.ObjectId
10 | Get-AzureADApplicationKeyCredential -ObjectId $_.ObjectId
11 | ) | Where-Object {
12 | $_.EndDate -lt (Get-Date).AddDays($LimitExpirationDays)
13 | } | ForEach-Object {
14 | $id = "Not set"
15 | if($_.CustomKeyIdentifier) {
16 | $id = [System.Text.Encoding]::UTF8.GetString($_.CustomKeyIdentifier)
17 | }
18 | [PSCustomObject] @{
19 | App = $app.DisplayName
20 | ObjectID = $app.ObjectId
21 | AppId = $app.AppId
22 | Type = $_.GetType().name
23 | KeyIdentifier = $id
24 | EndDate = $_.EndDate
25 | }
26 | }
27 | }
28 |
29 | #Gridview list
30 | #$SecretsToExpire | Out-GridView
31 |
32 | #Printing the list of secrets that are near to expire
33 | if($SecretsToExpire.Count -EQ 0) {
34 | Write-Output "No secrets found that will expire in this range"
35 | }
36 | else {
37 | Write-Output "Secrets that will expire in this range:"
38 | Write-Output $SecretsToExpire.Count
39 | Write-Output $SecretsToExpire
40 | }
--------------------------------------------------------------------------------
/StartStopAzureSQLVMWithDowngradePremiumDisks.ps1:
--------------------------------------------------------------------------------
1 | Connect-AzAccount
2 |
3 | $ResourceGroupName = "YOUR_VM_RESOURCE_GROUP"
4 |
5 | $vmnames = get-AzVM -ResourceGroupName $ResourceGroupName -status
6 | foreach ($vmname in $vmnames)
7 | {
8 | $vms = ((Get-AzVM -ResourceGroupName $ResourceGroupName -VMName $vmname.name -Status).Statuses[1]).Code
9 | if ($vms -eq 'PowerState/running')
10 | {
11 | {
12 | stop-AzVM -ResourceGroupName $ResourceGroupName -Name $vmname.name
13 | }
14 |
15 | $vdisks = $vmname.StorageProfile.DataDisks
16 |
17 | foreach ($vdisk in $vdisks) {
18 | $d = Get-AzDisk -DiskName $vdisk.Name
19 | if ($d.sku.tier -eq "premium") {
20 | $storageType = 'Standard_LRS'
21 | $d.Sku = [Microsoft.Azure.Management.Compute.Models.DiskSku]::new($storageType)
22 | $d | Update-AzDisk
23 | }
24 | }
25 | }
26 | }
27 |
28 |
29 | $ResourceGroupName = "YOUR_VM_RESOURCE_GROUP"
30 | $VM = "YOUR_VM_NAME"
31 |
32 | $v=get-AzVM -ResourceGroupName $ResourceGroupName -VMName $VM
33 |
34 | $vms=((Get-AzVM -ResourceGroupName $ResourceGroupName -VMName $VM -Status).Statuses[1]).Code
35 |
36 |
37 | $vdisks=$v.StorageProfile.DataDisks
38 |
39 | foreach ($vdisk in $vdisks)
40 | {
41 | $d=Get-AzDisk -DiskName $vdisk.Name
42 |
43 | if ($d.sku.tier -eq "standard")
44 | {
45 | $storageType = 'Premium_LRS'
46 | $d.Sku = [Microsoft.Azure.Management.Compute.Models.DiskSku]::new($storageType)
47 | $d | Update-AzDisk
48 | }
49 | }
50 |
51 |
52 | if ($vms -ne 'PowerState/running')
53 | {
54 | start-AzVM -ResourceGroupName $ResourceGroupName -Name $VM
55 | }
56 |
57 |
58 |
--------------------------------------------------------------------------------
/PowershellAzureFileShare.ps1:
--------------------------------------------------------------------------------
1 | Install-Module -Name Az -AllowClobber -Scope AllUsers
2 |
3 | Connect-AzAccount
4 |
5 | $location = 'West Europe';
6 | $resourceGroupName = 'd365bcfilesharerg'
7 | $storageAccountName = 'd365bcfilesharestorage'
8 | $storageShareName = 'd365bcfileshare'
9 | $driveLetterMapping = 'x'
10 |
11 | #Create resource group
12 | New-AzResourceGroup -Name $resourceGroupName -Location $location
13 |
14 | #Create storage account
15 | New-AzStorageAccount -Name $storageAccountName -ResourceGroupName $resourceGroupName -Location $location -Type 'Standard_LRS'
16 |
17 | #Retrieving references for the storage account
18 | $storageAccount = Get-AzStorageAccount -ResourceGroupName $resourceGroupName -Name $storageAccountName
19 | $storageKey = (Get-AzStorageAccountKey -ResourceGroupName $storageAccount.ResourceGroupName -Name $storageAccount.StorageAccountName | select -first 1).Value
20 | $storageContext = New-AzStorageContext -StorageAccountName $storageAccount.StorageAccountName -StorageAccountKey $storageKey
21 |
22 | #Create file share:
23 | New-AzStorageShare -Name $storageShareName -Context $storageContext
24 |
25 | #Creating a local drive
26 | $secKey = ConvertTo-SecureString -String $storageKey -AsPlainText -Force
27 | $credential = New-Object System.Management.Automation.PSCredential -ArgumentList "Azure\$($storageAccount.StorageAccountName)", $secKey
28 |
29 | #Drive mapping (persistent)
30 | #Open the regular PowerShell or Command Prompt to run these commands. If you run them as administrator, the drive won’t appear in File Explorer.
31 | $root = "\\$($storageAccount.StorageAccountName).file.core.windows.net\$storageShareName"
32 | Write-Output 'Mapping drive ' $driveLetterMapping' to ' $root
33 | New-PSDrive -Name $driveLetterMapping -PSProvider FileSystem -Root $root -Credential $credential -Persist -Scope Global
34 |
35 | #Temporary drives exist only in the current PowerShell session and in sessions that you create in the current session.
36 | #Because temporary drives are known only to PowerShell, you can't access them by using File Explorer, Windows Management Instrumentation (WMI), Component Object Model (COM), Microsoft .NET Framework, or with tools such as net use.
37 |
38 |
39 | #To remove the drive, use this cmdlet:
40 | #Remove-PSDrive -Name $driveLetterMapping
41 |
--------------------------------------------------------------------------------
/D365BCAppSourceUpdatesNotifications.ps1:
--------------------------------------------------------------------------------
1 | ##########################################################################################
2 | # Checks for available updates for AppSource apps installed on a given tenant
3 | ##########################################################################################
4 | $clientid = "YOUR_CLIENT_ID"
5 | $clientsecret = "YOUR_CLIENT_SECRET"
6 | $scope = "https://api.businesscentral.dynamics.com/.default"
7 | $tenant = "YOUR_TENANT_ID"
8 | $environment = "YOUR_ENVIRONMENT_NAME"
9 | # Get access token
10 | $token = Get-MsalToken `
11 | -ClientId $clientid `
12 | -TenantId $tenant `
13 | -Scopes $scope `
14 | -ClientSecret (ConvertTo-SecureString -String $clientsecret -AsPlainText -Force)
15 | $accessToken = ConvertTo-SecureString -String $token.AccessToken -AsPlainText -Force
16 |
17 | # Get available updates
18 | $response= Invoke-WebRequest `
19 | -Method Get `
20 | -Uri "https://api.businesscentral.dynamics.com/admin/v2.1/applications/businesscentral/environments/$environment/apps/availableUpdates" `
21 | -Authentication OAuth `
22 | -Token $accessToken
23 |
24 | if ((ConvertFrom-Json $response.Content).value.length -gt 0) {
25 | $jsonresponse = ConvertFrom-Json $response.Content
26 | $mailBody = "APP UPDATES AVAILABLE:
"
27 | foreach($app in $jsonresponse.value)
28 | {
29 | $mailBody += "App ID: " + $app.id + " Name: " + $app.name + " Publisher: " + $app.publisher + " Version: " + $app.version + "
"
30 | }
31 | }
32 | else {
33 | Write-Output "NO APP UPDATES FOUND."
34 | $mailBody = "";
35 | }
36 |
37 | if ($mailBody.Length -gt 0)
38 | {
39 | #Sending a notification email
40 | $UserName = "YOUR_SMTP_USERNAME"
41 | $Password = "YOUR_SMTP_PASSWORD"
42 | $from = "YOUR_SENDING_EMAIL_ADDRESS";
43 | $to = "YOUR_RECEIVING_EMAIL_ADDRESS";
44 | $SecurePassword = ConvertTo-SecureString -string $password -AsPlainText -Force
45 | $Cred = New-Object System.Management.Automation.PSCredential -argumentlist $UserName, $SecurePassword
46 | $EmailParams = @{
47 | From = $from
48 | To = $to
49 | Subject = "APP UPDATES AVAILABLE FOR TENANT " + $tenant
50 | Body = $mailBody
51 | SmtpServer = "smtp.office365.com"
52 | Port = 587
53 | UseSsl = $true
54 | Credential = $Cred
55 | BodyAsHtml = $true
56 | }
57 | Send-MailMessage @EmailParams
58 | }
59 |
--------------------------------------------------------------------------------
/BlobStorageLatencyTest.ps1:
--------------------------------------------------------------------------------
1 | Connect-AzAccount
2 |
3 | $blobUri = "YOUR_BLOB_URI"
4 |
5 | #Download blob
6 | $iterations = 100
7 | $stopwatch = [System.Diagnostics.Stopwatch]::new()
8 | $origProgressPref = $ProgressPreference
9 |
10 | $rawResults = [System.Collections.ArrayList]::new()
11 | $regionResult = @{
12 | PSTypeName = 'AzureRegionLatencyResult'
13 | Region = 'westeurope'
14 | ComputerName = $env:COMPUTERNAME
15 | }
16 |
17 | $ProgressPreference = 'SilentlyContinue'
18 | for ($i = 0; $i -lt $iterations; $i++) {
19 | $stopwatch.Start()
20 | Invoke-WebRequest -Uri $blobUri -UseBasicParsing > $null
21 | $stopwatch.Stop()
22 |
23 | $rawResults.Add($stopwatch.ElapsedMilliseconds) > $null
24 |
25 | $stopwatch.Reset()
26 | }
27 | $ProgressPreference = $origProgressPref
28 |
29 | $regionResult.Average = ($rawResults | Measure-Object -Average).Average
30 | $regionResult.Minimum = ($rawResults | Measure-Object -Minimum).Minimum
31 | $regionResult.Maximum = ($rawResults | Measure-Object -Maximum).Maximum
32 |
33 | $finalResult = [PSCustomObject]$regionResult
34 |
35 | $finalResult
36 |
37 |
38 | #UPLOAD BLOB
39 | $StorageAccount = "YOURSTORAGEACCOUNT";
40 | $ContainerName = "YOURCONTAINERNAME";
41 | $Context = New-AzStorageContext -StorageAccountName $StorageAccount -UseConnectedAccount
42 | $Blob = @{
43 | File = 'C:\Temp\test.pdf'
44 | Container = $ContainerName
45 | Blob = 'test.pdf'
46 | Context = $Context
47 | }
48 |
49 | $iterations = 100
50 | $stopwatch = [System.Diagnostics.Stopwatch]::new()
51 | $origProgressPref = $ProgressPreference
52 |
53 | $rawResults = [System.Collections.ArrayList]::new()
54 | $regionResult = @{
55 | PSTypeName = 'AzureRegionLatencyResult'
56 | Region = 'westeurope'
57 | ComputerName = $env:COMPUTERNAME
58 | }
59 |
60 | $ProgressPreference = 'SilentlyContinue'
61 | for ($i = 0; $i -lt $iterations; $i++) {
62 | $stopwatch.Start()
63 | Set-AzStorageBlobContent @Blob -Force
64 | $stopwatch.Stop()
65 |
66 | $rawResults.Add($stopwatch.ElapsedMilliseconds) > $null
67 |
68 | $stopwatch.Reset()
69 | }
70 | $ProgressPreference = $origProgressPref
71 |
72 | $regionResult.Average = ($rawResults | Measure-Object -Average).Average
73 | $regionResult.Minimum = ($rawResults | Measure-Object -Minimum).Minimum
74 | $regionResult.Maximum = ($rawResults | Measure-Object -Maximum).Maximum
75 |
76 | $finalResult = [PSCustomObject]$regionResult
77 |
78 | $finalResult
--------------------------------------------------------------------------------
/ApplicationInsightsPurgeTelemetry.ps1:
--------------------------------------------------------------------------------
1 | #### AUTHENTICATION ####
2 | # Import the ADAL module found in AzureRM.Profile
3 | Import-Module AzureRM.Profile
4 |
5 | #Application ID of the registered app
6 | $appId = "YOUR_APPLICATION_ID"
7 | #Client secret value for the app registration
8 | $key = "YOUR_CLIENT_SECRET"
9 | #AAD tenant ID
10 | $tenantId = "YOUR_AAD_TENANT_ID"
11 | #Azure Subscription ID
12 | $subscriptionId = "YOUR_SUBSCRIPTION_ID"
13 | #Resource group where Application Insight Instance is assigned to
14 | $resourceGroupName = "YOUR_APPLICATION_INSIGHTS_RESOURCE_GROUP"
15 | #Name of the Application Insight instance
16 | $resourceName = "YOUR_APPLICATION_INSIGHTS_INSTANCE_NAME"
17 |
18 | # Create the authentication URL and get the authentication context
19 | $authUrl = "https://login.windows.net/${tenantId}"
20 | $AuthContext = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext]$authUrl
21 |
22 | # Build the credential object and get the token form AAD
23 | $credentials = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential -ArgumentList $appId,$key
24 | $result = $AuthContext.AcquireToken("https://management.core.windows.net/",$credentials)
25 | # Build the authorization header JSON object
26 | $authHeader = @{
27 | 'Content-Type'='application/json'
28 | 'Authorization'=$result.CreateAuthorizationHeader()
29 | }
30 | #### END AUTHENTICATION ####
31 |
32 |
33 |
34 |
35 | #### PURGE DATA ####
36 | #Creates the API URI
37 | $URI = "https://management.azure.com/subscriptions/${subscriptionId}/resourceGroups/${resourceGroupName}/providers/Microsoft.Insights/components/${resourceName}/purge?api-version=2015-05-01"
38 |
39 | $body = @"
40 | {
41 | "table": "customEvents",
42 | "filters": [
43 | {
44 | "column": "timestamp",
45 | "operator": "<",
46 | "value": "2018-01-01T00:00:00.000"
47 | }
48 | ]
49 | }
50 | "@
51 |
52 | #Invoke the REST API to purge the data on Application Insights
53 | $purgeID=Invoke-RestMethod -Uri $URI -Method POST -Headers $authHeader -Body $body
54 | # Write the purge ID
55 | Write-Host $purgeID.operationId -ForegroundColor Green
56 | #### END PURGE DATA ####
57 |
58 |
59 | #### GET PURGE STATUS ####
60 | # Creation of the API URI to get the purge status
61 | $purgeURI="https://management.azure.com/subscriptions/$subscriptionId/resourceGroups/${resourceGroupName}/providers/Microsoft.Insights/components/${resourceName}/operations/$($purgeID.operationId)?api-version=2015-05-01"
62 | Invoke-RestMethod -Uri $purgeURI -Method GET -Headers $authHeader
63 | #### END GET PURGE STATUS ####
--------------------------------------------------------------------------------
/CreateApplicationInsightsD365BCTelemetryConfiguration.ps1:
--------------------------------------------------------------------------------
1 | #Name of the Application Insights Resource
2 | $appInsightsName = "YOURAPPLICATIONINSIGHTSNAME"
3 |
4 | #Name of the Resource Group to use.
5 | $resourceGroupName = "YOURRESOURCEGROUPNAME"
6 |
7 | #Name of the workspave
8 | $WorkspaceName = "YOURWORKSPACENAME"
9 |
10 | #Azure location
11 | $Location = "westeurope"
12 |
13 | #Data retention for Application Insights (days)
14 | $dataretentiondays = 30
15 |
16 | #Daily Cap (GB) for Application Insights instance
17 | $dailycap = 15
18 |
19 | #Parameters for connecting to Dynamics 365 Business Central tenant
20 | #Business Central tenant id
21 | $aadTenantId = "TENANTID"
22 | #Name of the D365BC Environment
23 | $D365BCenvironmentName = "YOURD365BCENVIRONMENTNAME"
24 | #Partner's AAD app id
25 | $aadAppId = "CLIENTID"
26 | #Partner's AAD app redirect URI
27 | $aadAppRedirectUri = "nativeBusinessCentralClient://auth"
28 |
29 | Connect-AzAccount
30 |
31 | New-AzResourceGroup -Name $resourceGroupName -Location $Location
32 |
33 | New-AzOperationalInsightsWorkspace -Location $Location -Name $WorkspaceName -ResourceGroupName $resourceGroupName
34 | $Resource = Get-AzOperationalInsightsWorkspace -Name $WorkspaceName -ResourceGroupName $resourceGroupName
35 | $workspaceId = $Resource.ResourceId
36 |
37 | New-AzApplicationInsights -ResourceGroupName $resourceGroupName -Name $appInsightsName -location $Location -WorkspaceResourceId $workspaceId
38 | $Resource = Get-AzResource -ResourceType Microsoft.Insights/components -ResourceGroupName $resourceGroupName -ResourceName $appInsightsName
39 | $connectionString = $resource.Properties.ConnectionString
40 | Write-Host "Connection String = " $connectionString
41 | #Set data retention
42 | $Resource.Properties.RetentionInDays = $dataretentiondays
43 | $Resource | Set-AzResource -Force
44 | #Set daily cap (GB)
45 | Set-AzApplicationInsightsDailyCap -ResourceGroupName $resourceGroupName -Name $appInsightsName -DailyCapGB $dailycap
46 |
47 |
48 | # Load Microsoft.IdentityModel.Clients.ActiveDirectory.dll
49 | Add-Type -Path "C:\Program Files\WindowsPowerShell\Modules\AzureAD\2.0.2.140\Microsoft.IdentityModel.Clients.ActiveDirectory.dll" # Install-Module AzureAD to get this
50 |
51 |
52 | # Get access token
53 | $ctx = [Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext]::new("https://login.microsoftonline.com/$aadTenantId")
54 | $redirectUri = New-Object -TypeName System.Uri -ArgumentList $aadAppRedirectUri
55 | $platformParameters = New-Object -TypeName Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters -ArgumentList ([Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Always)
56 | $accessToken = $ctx.AcquireTokenAsync("https://api.businesscentral.dynamics.com", $aadAppId, $redirectUri, $platformParameters).GetAwaiter().GetResult().AccessToken
57 |
58 | Write-Host $accessToken
59 |
60 | $response = Invoke-WebRequest `
61 | -Method Post `
62 | -Uri "https://api.businesscentral.dynamics.com/admin/v2.11/applications/businesscentral/environments/$D365BCenvironmentName/settings/appinsightskey" `
63 | -Body (@{
64 | key = $connectionString
65 | } | ConvertTo-Json) `
66 | -Headers @{Authorization=("Bearer $accessToken")} `
67 | -ContentType "application/json"
68 | Write-Host "Responded with: $($response.StatusCode) $($response.StatusDescription)"
--------------------------------------------------------------------------------
/EntraIDGuestUsersExport.ps1:
--------------------------------------------------------------------------------
1 | <#
2 | .SYNOPSIS
3 | Export guest users from Microsoft Entra ID (Azure AD) with detailed information, such as last login and account creation dates.
4 |
5 | .DESCRIPTION
6 | This script connects to Microsoft Graph API to retrieve all guest users (`userType eq 'Guest'`) in Azure AD.
7 | It extracts the following information:
8 | - DisplayName (properly escaped if it contains commas or special characters)
9 | - Email
10 | - Account creation date
11 | - Days since account creation (if never logged in)
12 | - Last login date
13 | - Days since last login
14 | The results are exported to a user-specified CSV file.
15 |
16 | .PARAMETER None
17 | No additional parameters are required. The script will prompt for the file save location.
18 |
19 | .NOTES
20 | Version: 1.0.0
21 |
22 | .REQUIREMENTS
23 | Before running the script, ensure you meet the following requirements:
24 | - Microsoft Entra ID (formerly Azure AD) – Your account must be linked to an Entra ID tenant.
25 | - Admin Role – Requires User.Read.All or higher permissions in Entra ID.
26 | - Microsoft Graph PowerShell Module – Installed automatically by the script if missing.
27 | - PowerShell Execution Policy – Must allow script execution (Set-ExecutionPolicy RemoteSigned).
28 |
29 | Open PowerShell with elevated permissions (Run as Administrator).
30 | Run the script using:
31 | .\EntraIDGuestUsersExport.ps1
32 |
33 | #>
34 |
35 | # Ensure the script stops on errors
36 | $ErrorActionPreference = "Stop"
37 |
38 | # Function to display messages with timestamps
39 | function Write-Log {
40 | param([string]$Message)
41 | $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
42 | Write-Host "[$timestamp] $Message" -ForegroundColor Cyan
43 | }
44 |
45 | # Function to escape fields with special characters (e.g., commas)
46 | function Escape-CsvField {
47 | param([string]$FieldValue)
48 | if ($FieldValue -and $FieldValue.Contains(",")) {
49 | return "`"$FieldValue`"" # Wrap the field in double quotes
50 | } else {
51 | return $FieldValue
52 | }
53 | }
54 |
55 | Write-Log "Starting Entra ID Guest User Export..."
56 |
57 | # Prompt user to select a save location for the CSV file
58 | Write-Log "Please choose where to save the output CSV file..."
59 | $FileBrowser = New-Object -ComObject Shell.Application
60 | $Folder = $FileBrowser.BrowseForFolder(0, "Select Folder to Save CSV File", 0)
61 |
62 | if ($Folder) {
63 | $outputFolder = $Folder.Self.Path
64 | $outputFile = "$outputFolder\EntraIDGuestUsers_LastSignIn.csv"
65 | } else {
66 | Write-Host "No folder selected. Using default script directory." -ForegroundColor Yellow
67 | $outputFile = "$PSScriptRoot\EntraIDGuestUsers_LastSignIn.csv"
68 | }
69 |
70 | Write-Log "CSV File will be saved as: $outputFile"
71 |
72 | # Check if Microsoft Graph Users module is installed
73 | if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Users)) {
74 | Write-Log "Microsoft Graph module not found. Installing now..."
75 | Install-Module Microsoft.Graph -Scope CurrentUser -Force
76 | }
77 |
78 | # Import only required Microsoft Graph submodule
79 | Import-Module Microsoft.Graph.Users
80 |
81 | # Connect to Microsoft Graph
82 | Write-Log "Connecting to Microsoft Graph API..."
83 | try {
84 | Connect-MgGraph -Scopes "User.Read.All" -ErrorAction Stop
85 | Write-Log "Connected successfully to Microsoft Graph."
86 | } catch {
87 | Write-Host "ERROR: Failed to connect to Microsoft Graph. Ensure you have the correct permissions." -ForegroundColor Red
88 | exit
89 | }
90 |
91 | # Fetch all guest users with additional properties
92 | Write-Log "Retrieving guest users from Microsoft Entra ID (Azure AD)..."
93 | try {
94 | $guestUsers = Get-MgUser -Filter "userType eq 'Guest'" -Property Id, DisplayName, Mail, SignInActivity, CreatedDateTime -All
95 | Write-Log "Retrieved $($guestUsers.Count) guest users."
96 | } catch {
97 | Write-Host "ERROR: Failed to retrieve guest users. Ensure you have the necessary permissions." -ForegroundColor Red
98 | exit
99 | }
100 |
101 | # Initialize an array to store the results
102 | $results = @()
103 |
104 | # Get the current date for calculation
105 | $today = Get-Date
106 |
107 | Write-Log "Processing users and cleaning data..."
108 | foreach ($user in $guestUsers) {
109 | # Extract and clean data
110 | $displayName = if ($user.DisplayName) {
111 | Escape-CsvField($user.DisplayName.Trim() -replace "\s+", " ") # Normalize spaces and wrap if needed
112 | } else {
113 | "Unknown Name"
114 | }
115 |
116 | $email = if ($user.Mail) {
117 | Escape-CsvField($user.Mail.Trim()) # Wrap email if it contains commas
118 | } else {
119 | "No Email Provided"
120 | }
121 |
122 | $createdDate = $user.CreatedDateTime
123 | $lastSignInDate = $user.SignInActivity.LastSignInDateTime
124 |
125 | # Calculate days since last login or creation
126 | $daysSinceLastLogin = if ($lastSignInDate) {
127 | ($today - $lastSignInDate).Days
128 | } else {
129 | "Never Logged In"
130 | }
131 |
132 | $daysSinceCreation = if (!$lastSignInDate -and $createdDate) {
133 | ($today - $createdDate).Days
134 | } else {
135 | ""
136 | }
137 |
138 | # Add a clean record to the results array
139 | $results += [PSCustomObject]@{
140 | DisplayName = $displayName
141 | Email = $email
142 | CreatedDate = if ($createdDate) { $createdDate.ToString("dd-MM-yyyy HH:mm") } else { "Unknown" }
143 | DaysSinceCreation = if ($daysSinceCreation -ne "") { $daysSinceCreation } else { "N/A" }
144 | LastSignIn = if ($lastSignInDate) { $lastSignInDate.ToString("dd-MM-yyyy HH:mm") } else { "Never Logged In" }
145 | DaysSinceLastLogin = $daysSinceLastLogin
146 | }
147 | }
148 |
149 | # Export results to CSV
150 | Write-Log "Exporting data to CSV file..."
151 | try {
152 | $results | Export-Csv -Path $outputFile -NoTypeInformation
153 | Write-Log "Export completed successfully! File saved as: $outputFile"
154 | } catch {
155 | Write-Host "ERROR: Failed to save the CSV file. Check file permissions." -ForegroundColor Red
156 | }
157 |
158 | # Disconnect from Microsoft Graph
159 | Write-Log "Disconnecting from Microsoft Graph API..."
160 | Disconnect-MgGraph
161 | Write-Log "Script completed!"
--------------------------------------------------------------------------------