├── 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!" --------------------------------------------------------------------------------