├── AboutTheAuthor.md ├── Blueprints └── Add Purview Role Group to a Service Principal.md ├── Lego ├── Build-Signature.md ├── CheckCertificateInstalled.md ├── CheckConfigurationFileAvailable.md ├── CheckIfElevated.md ├── CheckPowerShellVersion.md ├── CheckRequiredModules.md ├── CreateCSVFile.md ├── CreateCodeSigningCertificate.md ├── CreateConfigFile.md ├── CreateNewEntraApp.md ├── CreateTaskSchedulerTask.md ├── HOWTO.md ├── HashCredentials.md ├── ScriptVariables.md ├── SelfSign.md ├── Services Connections │ ├── Connect2EDM.md │ ├── Connect2MicrosoftGraphService.md │ ├── Connect2MicrosoftGraphWithCertificate.md │ └── Connect2Purview.md ├── UnHashCredentials.md ├── UpdateEntraApp.md ├── ValidateCmdlet.md ├── ValidateExistingGroupField.md ├── ValidateIfCSVisOpenByAnotherApp.md ├── ValidateUPNInCSVFIle.md └── WriteToLogsAnalytics.md ├── LegoPlus ├── AboutLegoPlus.md └── Microsoft Entra │ ├── CreateMicrosoftEntraGroup.md │ └── Update-ExistingGroup.md ├── README.md ├── Samples ├── Defender │ └── AdvanceHunting.md ├── Exchange │ └── CreateInboxRules.md ├── General │ └── Hash-UnHash.md ├── MDCA │ └── GetMDCAMatchingFiles.md ├── Microsoft Entra │ └── NestedGroupsBasedOnManager.md ├── Purview │ ├── MSPurviewDLPCollector.md │ ├── MSPurviewIPCollector.md │ ├── OCR.md │ └── Purview Role Groups.md ├── UpdateInfo │ └── Update.json └── WhatCanIFindHere.md └── Support └── HowToConfigureAzureAIVision.md /AboutTheAuthor.md: -------------------------------------------------------------------------------- 1 | # About the Author 2 | 3 |

4 |

5 |

Sebastian Zamorano

6 |
7 | 8 | Welcome! My name is Sebastian Zamorano, and I’m a seasoned professional in data governance, cybersecurity, and information protection, with a specific focus on Microsoft Purview and its extensive capabilities. My journey in the tech world has been driven by a passion for empowering organizations to secure their data and enhance compliance through robust governance frameworks and tools. 9 | 10 | Over the years, I’ve delved deeply into the realms of Data Loss Prevention (DLP), Microsoft Information Protection (MIP), and the entire suite of Microsoft Purview solutions. Through practical applications and continuous learning, I’ve built expertise in configuring data governance structures, implementing information protection policies, and driving data-centric security initiatives that help teams stay ahead in an ever-evolving digital landscape. 11 | 12 | My work is not only about technical configuration but also about creating practical, adaptable training programs that guide organizations on how to use these tools effectively. From foundational topics like setting up Microsoft Purview Data Maps to advanced sessions on cataloging and labeling sensitive data, I am committed to sharing insights that help others harness the power of data governance technology. 13 | 14 | In addition to providing strategic insights and hands-on guidance through public webinars and custom training labs, I am also invested in helping teams and individuals grow through initiatives like the Armory initiative. This project inspires people to collaborate, share knowledge, and support each other, fostering a culture of teamwork and learning. 15 | 16 | Through my LinkedIn, I actively share updates, insights, and resources related to Microsoft Purview and data protection, keeping my network informed of the latest developments in this dynamic field. You can follow my journey and join the conversation here: [LinkedIn Profile](https://linkedin.com/in/profesorkaz). 17 | 18 | In addition to training and consulting, I have developed a range of resources to help others deepen their knowledge and implement practical solutions in data governance and security. Here are a few of the projects and resources I've created: 19 | 20 | - [My YouTube Channel](https://www.youtube.com/playlist?list=PL6PefKKVENMucWHjv1WyY36qsaPVRb0oH) – A place where I share tutorials, webinars, and deep dives into Microsoft Purview, information protection. MPARR, and data security best practices. 21 | - [Activity Explorer Script Solution](https://github.com/ProfKaz/ActivityExplorerExport/blob/main/README.md) – This PowerShell-based solution streamlines the use of Activity Explorer, providing an automated way to analyze data activity within Microsoft 365. 22 | - [Microsoft Purview Data Map: Starting from Scratch](https://github.com/ProfKaz/AboutPurviewDatamap) – A comprehensive guide to setting up a Microsoft Purview Data Map from scratch, including detailed steps for configuring Azure services, integrating on-premises SQL databases, and managing cloud access via Azure RBAC. 23 | - [MPARR (Microsoft Purview Advanced Rich Reports)](https://github.com/ProfKaz/AboutPurviewDatamap) – A project focused on automating reporting and remediation workflows within Microsoft Purview, providing organizations with actionable insights and streamlined processes for data management. 24 | - [GitHub Repository](https://github.com/ProfKaz) – Explore my GitHub for various scripts, tools, and sample projects that support Microsoft Purview, data protection, and compliance strategies. 25 | 26 | These resources are designed to empower users at every level, from beginners looking to establish foundational skills to advanced users seeking customized solutions for their organizations. You’re welcome to explore and utilize these tools as you journey through the data governance landscape. 27 | 28 | Thank you for stopping by, and I hope my work and resources will be valuable as you navigate your own path in the world of data governance and information security. 29 |

30 | -------------------------------------------------------------------------------- /Blueprints/Add Purview Role Group to a Service Principal.md: -------------------------------------------------------------------------------- 1 | # How to add a Service Principal (Microsoft Entra ID App) to a Purview Role Group 2 | 3 | By default, the Web Interface does not allow a Service Principal to be directly assigned to a Purview Role Group. It is not possible to select the Service Principal as a user, add it to a group, and then assign that group to the Role Group—this approach does not work. 4 | 5 | The only way to achieve this is through PowerShell. You need to connect to Purview and create a new Service Principal under that workload, using the same IDs from Microsoft Entra ID. This process effectively generates a new Service Principal that remains linked to the Microsoft Entra instance. 6 | 7 | The steps to achieve this are: 8 | 9 | ```powershell 10 | $AppClientID = "" 11 | 12 | # Here you need to connect with a user with the right permissions, or a Global Admin account 13 | Connect-MgGraph -Scopes AppRoleAssignment.ReadWrite.All,Application.Read.All -NoWelcome 14 | 15 | #We are adding all the values from the Microsoft Entra Service Principal into a Variable 16 | $MicrosoftEntraApp = Get-MgServicePrincipal -Filter "AppId eq '$AppClientID'" 17 | 18 | #On the same session, under the same console we need to connect using a Compliance Administrator account 19 | Connect-IPPSSession -UseRPSSession:$false -ShowBanner:$false 20 | 21 | #Now we will create the "New" Service Principal under Purview using the same App ID and Object ID from MIcrosoft Entra ID App 22 | New-ServicePrincipal -AppId $MicrosoftEntraApp.AppId -ObjectId $MicrosoftEntraApp.Id -DisplayName "SP for Data Explorer PowerShell" 23 | 24 | #A new variable is created getting the values from the "New" Service Principal 25 | $SP = Get-ServicePrincipal -Identity $MicrosoftEntraApp.AppId 26 | 27 | #Finally, we can assign the Purview Role to the Service Principal, in this case the role assigned is "Content Explorer Content Viewer" 28 | Add-RoleGroupMember -Identity "ContentExplorerContentViewer" -Member $SP.Identity 29 | ``` 30 |

31 | -------------------------------------------------------------------------------- /Lego/Build-Signature.md: -------------------------------------------------------------------------------- 1 | # Function to Build-Signature for Logs Analytics. 2 | 3 | This function is called by the function [WriteToLogsAnalytics](/Lego/WriteToLogsAnalytics.md) and is used to connect to `Logs Analytics` 4 | 5 | ```powershell 6 | function Build-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) 7 | { 8 | # --------------------------------------------------------------- 9 | # Name : Build-Signature 10 | # Value : Creates the authorization signature used in the REST API call to Log Analytics 11 | # --------------------------------------------------------------- 12 | 13 | #Original function to Logs Analytics 14 | $xHeaders = "x-ms-date:" + $date 15 | $stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource 16 | 17 | $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash) 18 | $keyBytes = [Convert]::FromBase64String($sharedKey) 19 | 20 | $sha256 = New-Object System.Security.Cryptography.HMACSHA256 21 | $sha256.Key = $keyBytes 22 | $calculatedHash = $sha256.ComputeHash($bytesToHash) 23 | $encodedHash = [Convert]::ToBase64String($calculatedHash) 24 | $authorization = 'SharedKey {0}:{1}' -f $customerId,$encodedHash 25 | return $authorization 26 | } 27 | ``` 28 |

29 | -------------------------------------------------------------------------------- /Lego/CheckCertificateInstalled.md: -------------------------------------------------------------------------------- 1 | # Function to Check Installed Certificates 2 | 3 | When working with APIs and Service Principals, the connection string often requires a Certificate Thumbprint. For this, the certificate must be installed locally. This function retrieves locally installed certificates and checks if any match the thumbprint stored in a configuration file, which is then used in the connection string. 4 | 5 | By default, the function checks certificates in `Cert:\CurrentUser\My`, which lists certificates installed for the current user. However, certificates can also be installed at the machine level, in which case the location should be `Cert:\LocalMachine\My`. 6 | 7 | Depending on your needs, you may encounter different types of certificates, such as those for SSL authentication or code signing. This function specifically searches for certificates associated with **Client Authentication**. However, this approach has a limitation if the operating system is in a language other than English. 8 | 9 | To ensure compatibility across different OS languages, you can use the following options, replacing the language-specific filter with direct attributes: 10 | 11 | - For **Code Signing** certificates: 12 | 13 | ```powershell 14 | $certificates = @(Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object Thumbprint) 15 | ``` 16 | Instead of: 17 | ```powershell 18 | $certificates = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object {$_.EnhancedKeyUsageList -like "*Code Signing*"} | Select-Object Thumbprint) 19 | ``` 20 | 21 | - For **SSL Server Authentication** certificates: 22 | 23 | ```powershell 24 | $certificates = @(Get-ChildItem Cert:\CurrentUser\My -SSLServerAuthentication | Select-Object Thumbprint) 25 | ``` 26 | Instead of: 27 | ```powershell 28 | $certificates = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object {$_.EnhancedKeyUsageList -like "*Client Authentication"}| Select-Object Thumbprint) 29 | ``` 30 | 31 | Using these options avoids potential language-related issues, ensuring the function works consistently across various OS language settings. 32 | 33 | This function returns `True` if the provided thumbprint matches any locally installed certificates under either `CurrentUser` or `LocalMachine`. Here’s how I use this function: 34 | - $status = CheckCertificateInstalled -thumbprint $CertificateThumb 35 |

36 | 37 | ```powershell 38 | function CheckCertificateInstalled($thumbprint) 39 | { 40 | $var = "False" 41 | $certificates = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object {$_.EnhancedKeyUsageList -like "*Client Authentication*"}| Select-Object Thumbprint) 42 | #$thumbprint -in $certificates 43 | foreach($certificate in $certificates) 44 | { 45 | if($thumbprint -in $certificate.Thumbprint) 46 | { 47 | $var = "True" 48 | } 49 | } 50 | if($var -eq "True") 51 | { 52 | Write-Host "Certificate validation..." -NoNewLine 53 | Write-Host "`t`t`t`tPassed!" -ForegroundColor Green 54 | return $var 55 | }else 56 | { 57 | Write-Host "`nCertificate installed on this machine is missing!!!" -ForeGroundColor Yellow 58 | Write-Host "To execute this script unattended a certificate needs to be installed, the same used under Microsoft Entra App" 59 | Start-Sleep -s 1 60 | return $var 61 | } 62 | } 63 | ``` 64 |

65 | -------------------------------------------------------------------------------- /Lego/CheckConfigurationFileAvailable.md: -------------------------------------------------------------------------------- 1 | # Check if the CSV file used as an input exist 2 | 3 | I use this simple script to check if the required file is available. If the file does not exist, it is created using the [CreateCSVFile](/Lego/CreateCSVFile.md) function. This check is implemented in the [NestedGroupsBasedOnManager script](/Samples/NestedGroupsBasedOnManager.md) in this way: 4 | 5 | ```powershell 6 | $ConfigurationFile = "$PSScriptRoot\ConfigFiles\ManagerGroupsMatrix.csv" 7 | CheckConfigurationFileAvailable 8 | $CSVFile = Import-Csv -Path $ConfigurationFile 9 | 10 | MainScript 11 | ``` 12 | 13 | ```powershell 14 | function CheckConfigurationFileAvailable 15 | { 16 | # Check if the file exists 17 | if (-Not (Test-Path -Path $ConfigurationFile)) 18 | { 19 | CreateCSVFile 20 | Write-Host "`nAn Empty CSV configuration file was created.`n" 21 | Start-Sleep -s 1 22 | Return 23 | } 24 | } 25 | ``` 26 |

27 | -------------------------------------------------------------------------------- /Lego/CheckIfElevated.md: -------------------------------------------------------------------------------- 1 | # Function to check if you PowerShell script is running with Elevate Privileges 2 | 3 | Some times when we execute some scripts we need to run PowerShell with administrator rights to accomplish activities like install a PowerShell module or create a task under task scheduler. 4 | 5 | ```powershell 6 | function CheckIfElevated 7 | { 8 | $IsElevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 9 | if (!$IsElevated) 10 | { 11 | Write-Host "`nPlease start PowerShell as Administrator.`n" -ForegroundColor Yellow 12 | exit(1) 13 | } 14 | } 15 | ``` 16 |

17 | -------------------------------------------------------------------------------- /Lego/CheckPowerShellVersion.md: -------------------------------------------------------------------------------- 1 | # Function to check if you are running PowerShell v7 or higher 2 | 3 | Some functions are using some capabilities that are available only if you are using PowerShell 7, through this function you can validate if you are using that version. 4 | 5 | ```powershell 6 | function CheckPowerShellVersion 7 | { 8 | # Check PowerShell version 9 | Write-Host "`nChecking PowerShell version... " -NoNewline 10 | if ($Host.Version.Major -gt 5) 11 | { 12 | Write-Host "`t`t`t`tPassed!" -ForegroundColor Green 13 | } 14 | else 15 | { 16 | Write-Host "Failed" -ForegroundColor Red 17 | Write-Host "`tCurrent version is $($Host.Version). PowerShell version 7 or newer is required." 18 | exit(1) 19 | } 20 | } 21 | ``` 22 |

23 | -------------------------------------------------------------------------------- /Lego/CheckRequiredModules.md: -------------------------------------------------------------------------------- 1 | # Function to check if you PowerShell have all the PowerShell modules required 2 | 3 | Every time we work with different scripts, we need to load specific PowerShell modules to access the necessary cmdlets. With this in mind, it's essential to ensure that anyone running the script has the correct components installed. 4 | 5 | ```powershell 6 | function CheckRequiredModules 7 | { 8 | # Check PowerShell modules 9 | Write-Host "Checking PowerShell modules..." 10 | 11 | $requiredModules = @( 12 | @{Name="MicrosoftGraph"; MinVersion="0.0"}, 13 | @{Name="Microsoft.Graph.Authentication"; MinVersion="0.0"}, 14 | @{Name="Microsoft.Graph.Users"; MinVersion="0.0"}, 15 | @{Name="Microsoft.Graph.Groups"; MinVersion="0.0"} 16 | ) 17 | 18 | if($CreateEntraApp) 19 | { 20 | $requiredModules += @(@{Name="Microsoft.Graph.Applications"; MinVersion="0.0"}) 21 | } 22 | 23 | $modulesToInstall = @() 24 | foreach ($module in $requiredModules) 25 | { 26 | Write-Host "`t$($module.Name) - " -NoNewline 27 | $installedVersions = Get-Module -ListAvailable $module.Name 28 | if ($installedVersions) 29 | { 30 | if ($installedVersions[0].Version -lt [version]$module.MinVersion) 31 | { 32 | Write-Host "`t`t`tNew version required" -ForegroundColor Red 33 | $modulesToInstall += $module.Name 34 | } 35 | else 36 | { 37 | Write-Host "`t`t`tInstalled" -ForegroundColor Green 38 | } 39 | } 40 | else 41 | { 42 | Write-Host "`t`t`tNot installed" -ForegroundColor Red 43 | $modulesToInstall += $module.Name 44 | } 45 | } 46 | 47 | if ($modulesToInstall.Count -gt 0) 48 | { 49 | CheckIfElevated 50 | $choices = '&Yes', '&No' 51 | 52 | $decision = $Host.UI.PromptForChoice("", "Misisng required modules. Proceed with installation?", $choices, 0) 53 | if ($decision -eq 0) 54 | { 55 | Write-Host "Installing modules..." 56 | foreach ($module in $modulesToInstall) 57 | { 58 | Write-Host "`t$module" 59 | Install-Module $module -ErrorAction Stop 60 | 61 | } 62 | Write-Host "`nModules installed. Please start the script again." 63 | exit(0) 64 | } 65 | else 66 | { 67 | Write-Host "`nExiting setup. Please install required modules and re-run the setup." 68 | exit(1) 69 | } 70 | } 71 | } 72 | ``` 73 |

74 | -------------------------------------------------------------------------------- /Lego/CreateCSVFile.md: -------------------------------------------------------------------------------- 1 | # Function to create a CSV file used later as an input for a script 2 | 3 | The following function creates a CSV file named `ManagerGroupsMatrix.csv` in a folder called `ConfigFiles`. This CSV serves as input for a script that identifies nested direct reports across multiple levels, which can be configured within the same CSV file. If the file does not already exist, the function initializes it with the required structure. The function contains two key sections: one defines the list of fields to be included in the CSV, and the other arranges these fields in a specific order. 4 | 5 | ```powershell 6 | function CreateCSVFile 7 | { 8 | if(-not (Test-Path -Path $PathFolder)) 9 | { 10 | New-Item -ItemType Directory -Force -Path "$PSScriptRoot\ConfigFiles" | Out-Null 11 | } 12 | 13 | # Check if the CSV file already exists 14 | if (-Not (Test-Path $ConfigurationFile)) 15 | { 16 | # Create a CSV structure 17 | $ManagerUPN = "YourManagerUserPrincipalName@yourdomain.com" 18 | $GroupOwner = "OtherGroupOwnerUserPrincipalName@yourdomain.com" 19 | $IncludeManager = "TRUE" 20 | $ManagerAsOwner = "FALSE" 21 | $NewGroup = "Set the name of your new group" 22 | $GroupDescription = "Set your group description" 23 | $GroupType = "Use 'security' or 'microsoft365'" 24 | $data = [pscustomobject][ordered]@{ 25 | ManagerUPN = $ManagerUPN 26 | IncludeManager = $IncludeManager #Include the manager in the same group or not 27 | ManagerAsOwner = $ManagerAsOwner #Set manager as a group Owner 28 | GroupOwner = $GroupOwner #Set a group Owner 29 | NewGroup = $NewGroup 30 | GroupDescription= $GroupDescription 31 | GroupType = $GroupType 32 | ExistingGroup = $ExistingGroup 33 | RecursionDepth = $RecursionDepth 34 | } 35 | # If file does not exist, create it with headers 36 | $data | Export-Csv -Path $ConfigurationFile -NoTypeInformation 37 | Write-Host "Created new CSV file: $ConfigurationFile" 38 | } else 39 | { 40 | # If file exists, append new data 41 | Write-Host "File is existing on path." 42 | exit 43 | } 44 | } 45 | ``` 46 | 47 |

48 | -------------------------------------------------------------------------------- /Lego/CreateCodeSigningCertificate.md: -------------------------------------------------------------------------------- 1 | # Function to create a self-sign certificate for Code Signin 2 | 3 | This function is typically invoked by another function to generate a local certificate, which is then installed under the 'Current User' profile (`Cert:\CurrentUser\My`). However, it can be modified to create certificates under the 'Local Machine' profile (`Cert:\LocalMachine\My`) if needed. 4 | 5 | ```powershell 6 | function CreateCodeSigningCertificate 7 | { 8 | #CMDLET to create certificate 9 | $ScriptingCert = New-SelfSignedCertificate -Subject "CN=Self-Sign Code Signing Cert" -Type "CodeSigning" -CertStoreLocation "Cert:\CurrentUser\My" -HashAlgorithm "sha256" 10 | 11 | ### Add Self Signed certificate as a trusted publisher 12 | 13 | # Add the self-signed Authenticode certificate to the computer's root certificate store. 14 | ## Create an object to represent the CurrentUser\Root certificate store. 15 | $rootStore = [System.Security.Cryptography.X509Certificates.X509Store]::new("Root","CurrentUser") 16 | ## Open the root certificate store for reading and writing. 17 | $rootStore.Open("ReadWrite") 18 | ## Add the certificate stored in the $authenticode variable. 19 | $rootStore.Add($ScriptingCert) 20 | ## Close the root certificate store. 21 | $rootStore.Close() 22 | 23 | # Add the self-signed Authenticode certificate to the computer's trusted publishers certificate store. 24 | ## Create an object to represent the CurrentUser\TrustedPublisher certificate store. 25 | $publisherStore = [System.Security.Cryptography.X509Certificates.X509Store]::new("TrustedPublisher","CurrentUser") 26 | ## Open the TrustedPublisher certificate store for reading and writing. 27 | $publisherStore.Open("ReadWrite") 28 | ## Add the certificate stored in the $authenticode variable. 29 | $publisherStore.Add($ScriptingCert) 30 | ## Close the TrustedPublisher certificate store. 31 | $publisherStore.Close() 32 | } 33 | ``` 34 |

35 | -------------------------------------------------------------------------------- /Lego/CreateConfigFile.md: -------------------------------------------------------------------------------- 1 | # Function to Create a Configuration File for Scripts 2 | 3 | This function creates a configuration file named `Config.json` inside a folder called `ConfigFiles`. If `ConfigFiles` does not exist, the function creates it. The configuration file includes the following attributes: 4 | - `AppClientID`: Initially set as empty, though a default value can be assigned here if needed. 5 | - `TenantGUID`: Set as empty by default; this value can also be populated by another function if necessary. 6 | - `CertificateThumb`: Initially set as empty. 7 | 8 | This function allows you to set multiple attributes, enabling customization based on the requirements of different scripts. 9 | 10 | ```powershell 11 | function CreateConfigFile 12 | { 13 | # Set the path to the config file 14 | $configfile = $PSScriptRoot+"\ConfigFiles\Config.json" 15 | 16 | if(-Not (Test-Path $configfile )) 17 | { 18 | Write-Host "Export data directory is missing, creating a new folder called ConfigFiles" 19 | New-Item -ItemType Directory -Force -Path "$PSScriptRoot\ConfigFiles" | Out-Null 20 | } 21 | 22 | if (-not (Test-Path -Path $configfile)) 23 | { 24 | $config = [ordered]@{ 25 | AppClientID = "" 26 | TenantGUID = "" 27 | CertificateThumb = "" 28 | } 29 | }else 30 | { 31 | Write-Host "Configuration file is available under ConfigFiles folder" 32 | return 33 | } 34 | 35 | $config | ConvertTo-Json | Out-File "$configfile" 36 | Write-Host "New config file was created under ConfigFile folder." -ForegroundColor Yellow 37 | } 38 | ``` 39 |

40 | -------------------------------------------------------------------------------- /Lego/CreateNewEntraApp.md: -------------------------------------------------------------------------------- 1 | # Function to create a Microsoft Entra App 2 | 3 | To automate specific tasks, using a Service Principal is preferred over a user account. With API permissions, this approach allows various activities to be performed securely. The following function creates a Service Principal, commonly referred to as a Microsoft Entra Application, with specified Microsoft Graph API permissions: 4 | - `User.Read` as Delegated (default) 5 | - `User.Read.All` as Application 6 | - `Group.ReadWrite.All` as Application 7 | - `Directory.Read.All` as Application 8 | 9 | 10 |

11 |

12 |

How to identify API Id and Permission Id

13 |
14 | 15 | For unattended access, Microsoft Entra applications typically use one of two authentication methods: a `Secret Key` or a `Certificate Thumbprint`. This function includes steps to generate a certificate, install it locally, associate it with the Microsoft Entra Application, and update the existing configuration file. [Configuration file](CreateConfigFile.md) 16 | 17 | > [!IMPORTANT] 18 | > Permissions granted in Microsoft Entra require final approval from a Global Admin, who must grant access permissions to the APIs specified in the Microsoft Entra Application. 19 | 20 | > [!IMPORTANT] 21 | > If the **configuration file** doesn't exist the same script call the function called `CreateConfigFile` to create the configuration fila that will be used. 22 | 23 | ```powershell 24 | function CreateNewEntraApp 25 | { 26 | Connect-MgGraph -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "Directory.ReadWrite.All", "User.ReadWrite.All" -NoWelcome 27 | 28 | $CONFIGFILE = $PSScriptRoot+"\ConfigFiles\EntraConfig.json" 29 | if(-not (Test-Path -path $CONFIGFILE)) 30 | { 31 | CreateConfigFile 32 | } 33 | 34 | $json = Get-Content -Raw -Path $CONFIGFILE 35 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 36 | 37 | $appName = "Microsoft Entra Application" 38 | Get-MgApplication -ConsistencyLevel eventual -Count appCount -Filter "startsWith(DisplayName, '$appName')" | Out-Null 39 | if ($appCount -gt 0) 40 | { 41 | Write-Host "'$appName' app already exists.`n" 42 | Exit 43 | } 44 | 45 | # app parameters and API permissions definition 46 | $params = @{ 47 | DisplayName = $appName 48 | SignInAudience = "AzureADMyOrg" 49 | RequiredResourceAccess = @( 50 | @{ 51 | # Microsoft Graph API ID 52 | ResourceAppId = "00000003-0000-0000-c000-000000000000" 53 | ResourceAccess = @( 54 | @{ 55 | # This is the default permission added every time that a MIcrosoft Entra App is created 56 | # User.Read - Delegated 57 | Id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" 58 | Type = "Scope" 59 | }, 60 | @{ 61 | # Group.ReadWrite.All - Application 62 | Id = "62a82d76-70ea-41e2-9197-370581804d09" 63 | Type = "Role" 64 | }, 65 | @{ 66 | # User.Read.All 67 | Id = "df021288-bdef-4463-88db-98f22de89214" 68 | Type = "Role" 69 | }, 70 | @{ 71 | # Directory.Read.All 72 | Id = "7ab1d382-f21e-4acd-a863-ba3e13f7da61" 73 | Type = "Role" 74 | } 75 | ) 76 | } 77 | ) 78 | } 79 | 80 | # create application 81 | $app = New-MgApplication @params 82 | $appId = $app.Id 83 | 84 | # assign owner 85 | $userId = (Get-MgUser -UserId (Get-MgContext).Account).Id 86 | $params = @{ 87 | "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$userId" 88 | } 89 | New-MgApplicationOwnerByRef -ApplicationId $appId -BodyParameter $params 90 | 91 | # ask for certificate name 92 | $certName = "Microsoft Entra Groups" 93 | 94 | # certificate life 95 | $validMonths = 24 96 | 97 | # create key 98 | $cert = New-SelfSignedCertificate -DnsName $certName -CertStoreLocation "cert:\CurrentUser\My" -NotAfter (Get-Date).AddMonths($validMonths) 99 | $certBase64 = [System.Convert]::ToBase64String($cert.RawData) 100 | $keyCredential = @{ 101 | type = "AsymmetricX509Cert" 102 | usage = "Verify" 103 | key = [System.Text.Encoding]::ASCII.GetBytes($certBase64) 104 | } 105 | while (-not (Get-MgApplication -ApplicationId $appId -ErrorAction SilentlyContinue)) 106 | { 107 | Write-Host "Waiting while app is being created..." 108 | Start-Sleep -Seconds 5 109 | } 110 | Update-MgApplication -ApplicationId $appId -KeyCredentials $keyCredential -ErrorAction Stop 111 | $TenantID = (Get-MgContext).TenantId 112 | 113 | 114 | Write-Host "`nAzure application was created." 115 | Write-Host "App Name: $appName" 116 | Write-Host "App ID: $($app.AppId)" 117 | Write-Host "Tenant ID: $TenantID" 118 | Write-Host "Certificate thumbprint: $($cert.Thumbprint)" 119 | 120 | Write-Host "`nPlease go to the Azure portal to manually grant admin consent:" 121 | Write-Host "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$($app.AppId)`n" -ForegroundColor Cyan 122 | 123 | $config.TenantGUID = $TenantID 124 | $config.AppClientID = $app.AppId 125 | $config.CertificateThumb = $cert.Thumbprint 126 | 127 | $config | ConvertTo-Json | Out-File $CONFIGFILE 128 | 129 | Remove-Variable cert 130 | Remove-Variable certBase64 131 | } 132 | ``` 133 |

134 | -------------------------------------------------------------------------------- /Lego/CreateTaskSchedulerTask.md: -------------------------------------------------------------------------------- 1 | # Function to create a Task under a new folder on Task Scheduler 2 | 3 | This function creates a scheduled task named **RunMyScript** in Task Scheduler, organizing tasks by creating a folder called **MyScripts** if it doesn’t already exist. The task is configured to run every 30 days, and it will be skipped if a task with the same name already exists. 4 | 5 | > [!IMPORTANT] 6 | > To execute this function, PowerShell must be run with Administrator rights. Use the [CheckIfElevated function](CheckIfElevated.md) to ensure the correct permissions are in place. 7 | 8 | ```powershell 9 | function CreateTaskSchedulerTask 10 | { 11 | # Default folder for Microsoft Entra tasks 12 | $MyScriptFolder = "MyScripts" 13 | $taskFolder = "\"+$MyScriptFolder+"\" 14 | 15 | # Nested Groups Based On Manager script 16 | $taskName = "RunMyScript" 17 | 18 | # Task execution 19 | $validDays = 30 20 | 21 | # calculate date 22 | $dt = Get-Date 23 | $reminder = $dt.Day % $validDays 24 | $dt = $dt.AddDays(-$reminder) 25 | $startTime = [datetime]::new($dt.Year, $dt.Month, $dt.Day, $dt.Hour, $dt.Minute, 0) 26 | 27 | #create task 28 | $trigger = New-ScheduledTaskTrigger -Once -At $startTime -RepetitionInterval (New-TimeSpan -Days $validDays) 29 | $action = New-ScheduledTaskAction -Execute "`"$PSHOME\pwsh.exe`"" -Argument ".\MPARR-MicrosoftEntraRoles.ps1" -WorkingDirectory $PSScriptRoot 30 | $settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -DontStopOnIdleEnd -AllowStartIfOnBatteries ` 31 | -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 1) 32 | 33 | if (Get-ScheduledTask -TaskName $taskName -TaskPath $taskFolder -ErrorAction SilentlyContinue) 34 | { 35 | Write-Host "`nScheduled task named '$taskName' already exists.`n" -ForegroundColor Yellow 36 | exit 37 | } 38 | else 39 | { 40 | Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Settings $settings ` 41 | -RunLevel Highest -TaskPath $taskFolder -ErrorAction Stop | Out-Null 42 | Write-Host "`nScheduled task named '$taskName' was created.`nFor security reasons you have to specify run as account manually.`n`n" -ForegroundColor Yellow 43 | } 44 | } 45 | ``` 46 |

47 | -------------------------------------------------------------------------------- /Lego/HOWTO.md: -------------------------------------------------------------------------------- 1 | # How to use these pieces 2 | 3 |

4 |

5 |

Let's play

6 |
7 | 8 | In this **`Lego`** folder, you'll find various functions that I'll be sharing over time. My goal is to continuously update this folder with any new functions I use in my scripts. 9 | 10 | I’ll create an additional folder containing various scripts, both short and occasionally long. You’ll notice these modular pieces used frequently throughout the scripts. 11 | 12 | As shown below, my scripts typically consist of a collection of modular functions. This approach minimizes the need to write additional code each time, allowing me to focus on the primary objective while reusing previous work. Functions are available for tasks like exporting to CSV, JSON, Log Analytics, or Event Hub, automating connections, setting permissions, and more. 13 | 14 |

15 |

16 |

Script to identify nested direct reports of

17 |

18 | -------------------------------------------------------------------------------- /Lego/HashCredentials.md: -------------------------------------------------------------------------------- 1 | # Function to hash the password 2 | 3 | You can find a simple exercise in this [Hash-Unhash script](/Samples/General/Hash-UnHash.md) where we are able to create a [configuration file](/Lego/CreateConfigFile.md) that is used to store a password, the function read the value in the Json file and update the value `MyPassword` with the hashed one. 4 | You can check the function [UnHashCredentials](/Lego/UnHashCredentials) to work with hashed passwords. 5 | 6 | ```powershell 7 | function HashCredentials 8 | { 9 | # Validate if the password file exists 10 | ValidateConfigurationFile 11 | 12 | $json = Get-Content -Raw -Path $jsonFile 13 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 14 | $HashedPassword = $config.HashedPassword 15 | 16 | # Check if already encrypted 17 | if ($HashedPassword -eq "True") 18 | { 19 | Write-Host "`nAccording to the configuration settings (HashedPassword: True), password is already hashed." -ForegroundColor Yellow 20 | Write-Host "`nNo actions taken.`n" 21 | return 22 | } 23 | 24 | # Encrypt password 25 | $UserPassword = $config.MyPassword 26 | $UserPassword = $UserPassword | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString 27 | 28 | # Write results to the password file 29 | $config.HashedPassword = "True" 30 | $config.MyPassword = $UserPassword 31 | 32 | $date = Get-Date -Format "yyyyMMddHHmmss" 33 | Move-Item "$jsonFile" "$PSScriptRoot\ConfigFiles\MyCredentials_$date.json" 34 | Write-Host "`nPassword hashed." 35 | Write-Host "`nA backup was created with name " -NoNewLine 36 | Write-Host "'MyCredentials_$date.json'`n" -ForegroundColor Green 37 | $config | ConvertTo-Json | Out-File $jsonFile 38 | 39 | Write-Host "Warning!" -ForegroundColor DarkRed 40 | Write-Host "Please note that encrypted keys can be decrypted only on this machine, using the same account.`n" 41 | } 42 | ``` 43 |

44 | -------------------------------------------------------------------------------- /Lego/ScriptVariables.md: -------------------------------------------------------------------------------- 1 | # Function to set global variables 2 | 3 | When scripting, I often use the param() option at the beginning of my scripts to define environment variables. However, this approach has a drawback: these variables appear as attributes when the script is executed, which might not be ideal. 4 | 5 | Placing variables outside of functions is another option, but in lengthy scripts, some variables can become difficult to track or might inadvertently be overwritten. 6 | 7 | To address this issue, a better approach is to encapsulate all environment variables within a dedicated function. This method keeps the script organized and minimizes the risk of losing or misusing variables. Here's how it can be implemented: 8 | 9 | ```powershell 10 | function ScriptVariables 11 | { 12 | # Log Analytics table where the data is written to. Log Analytics will add an _CL to this name. 13 | $script:TableName = "CopilotActivities" 14 | $script:ConfigPath = $PSScriptRoot + "\ConfigFiles\Config.json" 15 | $script:QueryPath = $PSScriptRoot + "\ConfigFiles\Query.txt" 16 | $script:ExportFolderName = "ExportedData" 17 | $script:ExportPath = $PSScriptRoot + "\" + $ExportFolderName 18 | $script:GraphEndpoint = "https://graph.microsoft.com/v1.0/security/microsoft.graph.security.runHuntingQuery" 19 | } 20 | ``` 21 | 22 | > [!NOTE] 23 | > Here you need to replace the common variable set as $Variable by $script:Variable that permit to reach this goal. 24 | 25 |

26 | -------------------------------------------------------------------------------- /Lego/SelfSign.md: -------------------------------------------------------------------------------- 1 | # Function to sign the scripts with a self-sign certificate 2 | 3 | This is a simple version of a function that search for certificates preciously installed used to sign code and uses the latest one available. 4 | 5 | To ensure compatibility across different OS languages, you can use the following options, replacing the language-specific filter with direct attributes: 6 | 7 | - For **Code Signing** certificates: 8 | 9 | ```powershell 10 | $certificates = @(Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Select-Object Thumbprint) 11 | ``` 12 | Instead of: 13 | ```powershell 14 | $certificates = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object {$_.EnhancedKeyUsageList -like "*Code Signing*"} | Select-Object Thumbprint) 15 | ``` 16 | 17 | - For **SSL Server Authentication** certificates: 18 | 19 | ```powershell 20 | $certificates = @(Get-ChildItem Cert:\CurrentUser\My -SSLServerAuthentication | Select-Object Thumbprint) 21 | ``` 22 | Instead of: 23 | ```powershell 24 | $certificates = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object {$_.EnhancedKeyUsageList -like "*Client Authentication"}| Select-Object Thumbprint) 25 | ``` 26 | >[!IMPORTANT] 27 | >By default, the function checks certificates in `Cert:\CurrentUser\My`, which lists certificates installed for the current user. However, certificates can also be installed at the machine level, in which case the location should be `Cert:\LocalMachine\My`. 28 | 29 | >[!NOTE] 30 | >In the code we can see that the function will sign the file called `MyMainScript.ps1`, nevertheless, we can use wildcards like as `*.ps1` to get all the script files to be signed. 31 | 32 | ```powershell 33 | function SelfSign 34 | { 35 | $certificates = @(Get-ChildItem Cert:\CurrentUser\My | Where-Object {$_.EnhancedKeyUsageList -like "*Code Signing*"}| Sort-Object NotBefore -Descending | Select-Object Subject, Thumbprint, NotBefore, NotAfter) 36 | $cert = Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert | Where-Object {$_.Thumbprint -eq $certificates[0].Thumbprint} 37 | $MainScript = Get-ChildItem -Path .\MyMainScript.ps1 38 | Set-AuthenticodeSignature -FilePath ".\$($MainScript.Name)" -Certificate $cert 39 | } 40 | ``` 41 |

42 | -------------------------------------------------------------------------------- /Lego/Services Connections/Connect2EDM.md: -------------------------------------------------------------------------------- 1 | # Function to Connect to EDM service 2 | 3 | You can find a complete detailed way to use Microsoft Purview Exact Data Match in this [link](https://github.com/ProfKaz/EDM-Post-Tasks), from another of my projects. 4 | To accomplish this connection an application is required to pre-install first and set other variables, based on that installation. 5 | 6 | ```powershell 7 | function Connect2EDM 8 | { 9 | $CONFIGFILE = "$PSScriptRoot\EDMConfig.json" 10 | if (-not (Test-Path -Path $CONFIGFILE)) 11 | { 12 | $CONFIGFILE = "$PSScriptRoot\EDM_RemoteConfig.json" 13 | } 14 | 15 | $json = Get-Content -Raw -Path $CONFIGFILE 16 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 17 | $EncryptedKeys = $config.EncryptedKeys 18 | $EDMFolder = $config.EDMAppFolder 19 | $user = $config.User 20 | $SharedKey = $config.Password 21 | 22 | if ($EncryptedKeys -eq "True") 23 | { 24 | $SharedKey = DecryptSharedKey $SharedKey 25 | Set-Location $EDMFolder | cmd 26 | Clear-Host 27 | cls 28 | Write-Host "Validating connection to EDM..." -ForegroundColor Green 29 | .\EdmUploadAgent.exe /Authorize /Username $user /Password $SharedKey 30 | }else{ 31 | Set-Location $EDMFolder | cmd 32 | Clear-Host 33 | cls 34 | Write-Host "Validating connection to EDM..." -ForegroundColor Green 35 | .\EdmUploadAgent.exe /Authorize /Username $user /Password $SharedKey 36 | } 37 | } 38 | ``` 39 |

40 | -------------------------------------------------------------------------------- /Lego/Services Connections/Connect2MicrosoftGraphService.md: -------------------------------------------------------------------------------- 1 | # Function to connect to Microsoft Graph API manually or automatically 2 | 3 | Use this function to connect to the Microsoft Graph API, either via a `ManualConnection` or through a [Microsoft Entra App](CreateNewEntraApp.md), where connection details are stored in a [Config File](CreateConfigFile.md). For `ManualConnection`, ensure the necessary scopes are specified for your tasks; this function currently uses the following scopes: 4 | - `Group.ReadWrite.All` 5 | - `Directory.ReadWrite.All` 6 | - `User.Read.All` 7 | 8 | If connecting via a [Microsoft Entra Application](CreateNewEntraApp.md), set the required API permissions within the app configuration. 9 | 10 | > [!NOTE] 11 | > When running the script with `.\MyScript.ps1` to establish an automatic connection, if the configuration file is missing, you’ll receive a message indicating that you can run the script with an attribute to create a [Microsoft Entra Application](CreateNewEntraApp.md). Remember set this function in your script. 12 | 13 | ```powershell 14 | function Connect2MicrosoftGraphService 15 | { 16 | 17 | <# 18 | .NOTES 19 | Special permissions to Microsoft Graph can be required, check the initial notes in each script 20 | #> 21 | if($ManualConnection) 22 | { 23 | Write-Host "`nAuthentication is required, please check your browser" -ForegroundColor Green 24 | Write-Host "Please note that manual connection might not work because some additional permissions may be required." -ForegroundColor DarkYellow 25 | Connect-MgGraph -Scopes "Group.ReadWrite.All", "Directory.ReadWrite.All", "User.Read.All" -NoWelcome 26 | }else 27 | { 28 | $ConfigFile = $PSScriptRoot+"\ConfigFiles\Config.json" 29 | 30 | #Check if the configuration file exist or not 31 | if(-Not (Test-Path -Path $ConfigFile)) 32 | { 33 | Write-Host "`nConfiguration file not available, you have these options:" 34 | Write-Host "You can use for a manual connection : " -NoNewLine 35 | Write-Host "`t.\MyScript.ps1 -ManualConnection" -ForeGroundColor Green 36 | Write-Host "You can configure a Microsoft Entra App to automate the connection using : " -NoNewLine 37 | Write-host "`t.\MyScript.ps1 -CreateEntraApp`n`n" -ForeGroundColor Green 38 | exit 39 | } 40 | 41 | $json = Get-Content -Raw -Path $ConfigFile 42 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 43 | 44 | $EncryptedKeys = $config.EncryptedKeys 45 | $AppClientID = $config.AppClientID 46 | $CertificateThumb = $config.CertificateThumb 47 | $TenantGUID = $config.TenantGUID 48 | 49 | $status = CheckCertificateInstalled -thumbprint $CertificateThumb 50 | 51 | if($status -eq "True") 52 | { 53 | Connect-MgGraph -CertificateThumbPrint $CertificateThumb -AppID $AppClientID -TenantId $TenantGUID -NoWelcome 54 | }else 55 | { 56 | Write-Host "`nThe Certificate set in EntraConfig.json don't match with the certificates installed on this machine, you can try to execute using manual connection, to do that extecute: " 57 | Write-Host ".\NestedGroupsBasedOnManager.ps1 -ManualConnection" -ForeGroundColor Green 58 | exit 59 | } 60 | 61 | } 62 | } 63 | ``` 64 | 65 | To use this function you need to set at the begin of the script a `param` variables like this: 66 | ```powershell 67 | param( 68 | [Parameter()] 69 | [switch]$ManualConnection 70 | ) 71 | ``` 72 | 73 | Having that parameter set you can call your script in this way to use `ManualConnection`: 74 | ```powershell 75 | .\MyScript.ps1 -ManualConnection 76 | ``` 77 |

78 | -------------------------------------------------------------------------------- /Lego/Services Connections/Connect2MicrosoftGraphWithCertificate.md: -------------------------------------------------------------------------------- 1 | # Function to connect to Microsoft Graph using a certificate using invoke method 2 | 3 | To connect to Microsoft Graph API exist 3 common methods: 4 | - Through cmdlet `Connect-IPPSSession` 5 | - Through Invoke using a Secret Key 6 | - Theough Invoke using a certificate 7 | 8 | This function show how to reach the 3rd option using a certificate, this certificate needs to be installed locally and added in a [Service Principal](../CreateNewEntraApp.md) previously created. 9 | The script uses the variable `$CertThumbprint` that needs to be previosuly set, through a [general variable](../ScriptVariables.md) or read it from a [Configuration File](../CreateConfigFile.md). 10 |
11 | 12 | ```powershell 13 | function Connect2MicrosoftGraph 14 | { 15 | Write-Output "Testing connection using certificate..." 16 | try { 17 | # Retrieve the certificate from CurrentUser\My 18 | $Certificate = Get-Item -Path Cert:\CurrentUser\My\$CertThumbprint 19 | if (-not $Certificate) { 20 | throw "Certificate with thumbprint $CertThumbprint not found in CurrentUser store." 21 | } 22 | 23 | # Ensure the certificate has a private key 24 | if (-not $Certificate.HasPrivateKey) { 25 | throw "The certificate does not have an associated private key." 26 | } 27 | Write-Output "Certificate found and has a private key." 28 | 29 | # Step 1: Create JWT client assertion 30 | Write-Output "Creating JWT client assertion..." 31 | $JwtHeader = @{ 32 | #alg: Algorithm 33 | #typ: Type 34 | #x5t: X.509 Thumbprint 35 | alg = "RS256" 36 | typ = "JWT" 37 | x5t = [Convert]::ToBase64String([System.Convert]::FromHexString($CertThumbprint)) 38 | } | ConvertTo-Json -Depth 10 -Compress 39 | 40 | $JwtPayload = @{ 41 | #aud: Audience 42 | #iss: Issuer 43 | #sub: Subject 44 | #jti: JWT ID 45 | #nbf: Not Before (time in Unix seconds) 46 | #exp: Expiration (time in Unix seconds) 47 | aud = $TokenEndpoint 48 | iss = $ClientId 49 | sub = $ClientId 50 | jti = [guid]::NewGuid().ToString() 51 | nbf = [int][System.DateTimeOffset]::UtcNow.AddMinutes(-5).ToUnixTimeSeconds() 52 | exp = [int][System.DateTimeOffset]::UtcNow.AddMinutes(55).ToUnixTimeSeconds() 53 | } | ConvertTo-Json -Depth 10 -Compress 54 | 55 | $EncodedHeader = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($JwtHeader)) 56 | $EncodedPayload = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($JwtPayload)) 57 | 58 | # Sign the JWT with the certificate's private key 59 | $CryptoProvider = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate) 60 | $DataToSign = [System.Text.Encoding]::UTF8.GetBytes("$EncodedHeader.$EncodedPayload") 61 | $SignatureBytes = $CryptoProvider.SignData($DataToSign, [System.Security.Cryptography.HashAlgorithmName]::SHA256, [System.Security.Cryptography.RSASignaturePadding]::Pkcs1) 62 | $Signature = [Convert]::ToBase64String($SignatureBytes) 63 | 64 | $ClientAssertion = "$EncodedHeader.$EncodedPayload.$Signature" 65 | 66 | # Step 2: Acquire Access Token using the client assertion 67 | Write-Output "Acquiring Access Token using client assertion..." 68 | $TokenResponse = Invoke-RestMethod -Method Post -Uri $TokenEndpoint -ContentType "application/x-www-form-urlencoded" -Body @{ 69 | client_id = $ClientId 70 | client_assertion = $ClientAssertion 71 | client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 72 | grant_type = "client_credentials" 73 | scope = $Scope 74 | } 75 | Write-Host "Access Token Acquired... " -NoNewLine 76 | Write-Host "`tSuccessfully:" -ForeGroundColor Green 77 | #Write-Output $TokenResponse.access_token 78 | $script:AccessToken = $TokenResponse.access_token 79 | } catch { 80 | Write-Output "Error during connection test: $($_.Exception.Message)" 81 | if ($_.Exception.Response -ne $null) { 82 | try { 83 | $ErrorContent = $_.Exception.Response.Content.ReadAsStringAsync().Result 84 | Write-Output "HTTP Response Content: $ErrorContent" 85 | } catch { 86 | Write-Output "Unable to capture response content: $($_.Exception.Message)" 87 | } 88 | } 89 | throw $_ 90 | } 91 | } 92 | ``` 93 | 94 | 95 |

96 | -------------------------------------------------------------------------------- /Lego/Services Connections/Connect2Purview.md: -------------------------------------------------------------------------------- 1 | # Function to connect to Purview Services 2 | 3 | This function utilizes the `Office 365 Exchange Online API`, enabling the execution of cmdlets typically available to admin users. It relies on a [Configuration File](/Lego/CreateConfigFile.md) containing details for the [Microsoft Entra Application](/Lego/CreateNewEntraApp.md) created for this purpose. To establish an automatic connection, you’ll need the application’s ID, the tenant’s OnMicrosoft URL, and a certificate thumbprint generated by the previous function. 4 | 5 | > [!WARNING] 6 | > To use this API, the service principal must have the necessary admin permissions for the cmdlets being executed. 7 | 8 | ```powershell 9 | function connect2PurviewService 10 | { 11 | ValidateConfigurationFile 12 | <# 13 | .NOTES 14 | If you cannot add the "Compliance Administrator" role to the Microsoft Entra App, for security reasons, you can execute with "Compliance Administrator" role 15 | this script using .\YourScript.ps1 -ManualConnection 16 | #> 17 | if($ManualConnection) 18 | { 19 | Write-Host "`nAuthentication is required, please check your browser" -ForegroundColor Green 20 | Connect-IPPSSession -UseRPSSession:$false -ShowBanner:$false 21 | }else 22 | { 23 | $CONFIGFILE = "$PSScriptRoot\ConfigFiles\laconfig.json" 24 | $json = Get-Content -Raw -Path $CONFIGFILE 25 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 26 | 27 | $EncryptedKeys = $config.EncryptedKeys 28 | $AppClientID = $config.AppClientID 29 | $CertificateThumb = $config.CertificateThumb 30 | $OnmicrosoftTenant = $config.OnmicrosoftURL 31 | if ($EncryptedKeys -eq "True") 32 | { 33 | $CertificateThumb = DecryptSharedKey $CertificateThumb 34 | } 35 | $status = CheckCertificateInstalled -thumbprint $CertificateThumb 36 | 37 | if($status -eq "True") 38 | { 39 | Connect-IPPSSession -CertificateThumbPrint $CertificateThumb -AppID $AppClientID -Organization $OnmicrosoftTenant -ShowBanner:$false 40 | }else 41 | { 42 | Write-Host "`nThe Certificate set in laconfig.json don't match with the certificates installed on this machine, you can try to execute using manual connection, to do that extecute: " 43 | Write-Host ".\MyScript.ps1 -ManualConnection" -ForeGroundColor Green 44 | exit 45 | } 46 | 47 | } 48 | } 49 | ``` 50 |

51 | -------------------------------------------------------------------------------- /Lego/UnHashCredentials.md: -------------------------------------------------------------------------------- 1 | # Function to hash the password 2 | 3 | You can find a simple exercise in this [Hash-Unhash script](/Samples/General/Hash-UnHash.md) where we are able to create a [configuration file](/Lego/CreateConfigFile.md) that is used to store a password, the function read the value in the Json file and update the value `MyPassword` with the Unhashed password and change the attribute `HashedPassword`to `False` this attrbute works as a flag. 4 | 5 | You can check the function [UnHashCredentials](/Lego/HashCredentials.md) to work with hashed passwords. 6 | 7 | To use this function you need to pass the hashed password and the function return the value unhashed, something like this: 8 | 9 | ```powershell 10 | if ($HashedPassword -eq "True") 11 | { 12 | $UserPassword = UnHashCredentials $UserPassword 13 | } 14 | ``` 15 | 16 | Now you can start using this function to UnHash your password. 17 | 18 | ```powershell 19 | function UnHashCredentials 20 | { 21 | param( 22 | [string] $encryptedKey 23 | ) 24 | 25 | try { 26 | $secureKey = $encryptedKey | ConvertTo-SecureString -ErrorAction Stop 27 | } 28 | catch { 29 | Write-Error "Workspace key: $($_.Exception.Message)" 30 | exit(1) 31 | } 32 | $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureKey) 33 | $plainKey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 34 | $plainKey 35 | } 36 | ``` 37 |

38 | -------------------------------------------------------------------------------- /Lego/UpdateEntraApp.md: -------------------------------------------------------------------------------- 1 | # Function to update a Microsoft Entra App 2 | 3 | Occasionally, we release new versions of our scripts that include enhancements or new features. These updates may require additional API permissions. Providing clear instructions to end users on how to configure these permissions is crucial to prevent potential issues or unexpected behavior, especially for users with limited experience. By simplifying and clearly documenting the required steps, we can help ensure a smoother implementation process. 4 | 5 | ```powershell 6 | function UpdateMicrosoftEntraApp 7 | { 8 | Connect-MgGraph -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All" -NoWelcome 9 | Clear-Host 10 | 11 | Write-Host "`n`n----------------------------------------------------------------------------------------" 12 | Write-Host "`nMicrosoft Entra App update!" -ForegroundColor DarkGreen 13 | Write-Host "This menu helps to validate that the Microsoft Entra App previously created have all the API permissions required." -ForegroundColor DarkGreen 14 | Write-Host "You will need to consent permissions Under Microsoft Entra portal to the app and the new permissions." -ForegroundColor DarkGreen 15 | Write-Host "`n----------------------------------------------------------------------------------------" 16 | 17 | $json = Get-Content -Raw -Path $ConfigFile 18 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 19 | $AppID = $config.AppClientID 20 | 21 | $filter = "AppId eq '$AppId'" 22 | $servicePrincipal = Get-MgServicePrincipal -All -Filter $filter 23 | $roles = Get-MgServicePrincipalAppRoleAssignment -ServicePrincipalId ($servicePrincipal.Id) 24 | if ($roles.AppRoleId -notcontains "dc50a0fb-09a3-484d-be87-e023b12c6440") 25 | { 26 | Write-Host "Office 365 Exchange Online API permission 'Exchange.ManageAsApp'" -NoNewLine 27 | Write-Host "`tNot Found!" -ForegroundColor Red 28 | Write-Host "App ID used:" $AppId 29 | Write-Host "Press any key to continue..." 30 | $key = ([System.Console]::ReadKey($true)) 31 | Write-Host "`nAdding permission...`n" 32 | # app parameters and API permissions definition 33 | $params = @{ 34 | AppId = $AppID 35 | RequiredResourceAccess = @( 36 | @{ 37 | # Office 365 Exchange Online API ID 38 | ResourceAppId = "00000002-0000-0ff1-ce00-000000000000" 39 | ResourceAccess = @( 40 | @{ 41 | # Permission used to execute ExchangeOnline cmdlets 42 | # Exchange.ManageAsApp - Application 43 | Id = "dc50a0fb-09a3-484d-be87-e023b12c6440" 44 | Type = "Role" 45 | } 46 | ) 47 | } 48 | 49 | ) 50 | } 51 | Update-MgApplicationByAppId @params 52 | Write-Host "Permission added." -ForegroundColor Green 53 | Write-Host "`nPlease go to the Azure portal to manually grant admin consent:" 54 | Write-Host "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$($AppId)`n" -ForegroundColor Cyan 55 | } 56 | else 57 | { 58 | Write-Host "Office 365 Exchange Online API permission..." -NoNewLine 59 | Write-Host "`t'Exchange.ManageAsApp'" -NoNewLine -ForegroundColor Green 60 | Write-Host "`tpermission already in place." 61 | Start-Sleep -s 3 62 | } 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /Lego/ValidateCmdlet.md: -------------------------------------------------------------------------------- 1 | # Function to validate if the cmdlet can be executed 2 | 3 | The following function is designed to determine whether the `Export-ContentExplorerData` cmdlet is available for execution. By performing this check, potential errors are avoided when attempting to run the cmdlet. 4 | 5 | ```powershell 6 | function CheckContentExplorerPermissions 7 | { 8 | if (-not (Get-Command -Name Export-ContentExplorerData -ErrorAction SilentlyContinue)) 9 | { 10 | Write-Host "You don´t have the permissions required to execute the cmdlet Export-ContentExplorerData" 11 | Write-Host "Please sign-in again with an account with these permissions assigned :" 12 | Write-Host "`t* Content Explorer Content Viewer" 13 | Write-Host "`t* Content Explorer List Viewer" 14 | Write-Host "`nYou can connect manually running " -NoNewline 15 | Write-Host "PS C:\>Connect-IPPSSession -UseRPSSession:$false -ShowBanner:$false" 16 | exit 17 | } 18 | } 19 | ``` 20 | 21 | Here another example to validate if you have the cmdlets required to Connecto to Microsoft Graph and Microsoft Exhange Online. 22 | 23 | ```powershell 24 | function ValidateConnectsCmdlets 25 | { 26 | $NotPassed = 0 27 | Write-Host "`n" 28 | if (-not (Get-Command -Name Connect-MgGraph -ErrorAction SilentlyContinue)) 29 | { 30 | Write-Host "Check connection to Microsoft Graph API..." -NoNewline 31 | Write-Host "`tFailed" -ForeGroundColor DarkRed 32 | NotPassed++ 33 | }else 34 | { 35 | Write-Host "Check connection to Microsoft Graph API..." -NoNewline 36 | Write-Host "`tPassed" -ForeGroundColor Green 37 | } 38 | 39 | if (-not (Get-Command -Name Connect-ExchangeOnline -ErrorAction SilentlyContinue)) 40 | { 41 | Write-Host "Check connection to Microsoft Exchange..." -NoNewline 42 | Write-Host "`tFailed" -ForeGroundColor DarkRed 43 | NotPassed++ 44 | }else 45 | { 46 | Write-Host "Check connection to Microsoft Exchange..." -NoNewline 47 | Write-Host "`tPassed" -ForeGroundColor Green 48 | } 49 | 50 | Start-Sleep -s 10 51 | 52 | if($NotPassed -gt 1) 53 | { 54 | Write-Host "`nYou don´t have the PowerShell module required to Connect to the services required." 55 | Write-Host "Please execute this script using :" 56 | Write-Host "`t* .\YourScript.ps2.ps1 -CheckDependencies" 57 | exit 58 | } 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /Lego/ValidateExistingGroupField.md: -------------------------------------------------------------------------------- 1 | # Function to validate Group ID format in a column in a CSV file 2 | 3 | This script reads the `ExistingGroup` column from a CSV file, validating each record to ensure that the Group ID matches with the right ID format. If any values do not conform to the Group ID format, a counter increments for each error. If the counter is greater than 0 at the end, the script exits and displays the total number of errors. 4 | 5 | ```powershell 6 | function ValidateExistingGroupField 7 | { 8 | # Regular expression to match GUID format 9 | $guidPattern = '^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$' 10 | $CountGUIDError = 0 11 | 12 | Write-Host "`n########## Validating Group ID format ##########`n" -ForeGroundColor DarkYellow 13 | # Iterate over each record in the CSV 14 | foreach ($record in $CSVFile) 15 | { 16 | # Validate the ExistingGroup field 17 | if ($record.ExistingGroup -match $guidPattern) 18 | { 19 | Write-Host "ExistingGroup valid: $($record.ExistingGroup)" 20 | }elseif($record.ExistingGroup -eq "") 21 | { 22 | Write-Host "Missing ExistingGroup: Not Set" 23 | }else 24 | { 25 | Write-Host "Invalid value set in ExistingGroup: $($record.ExistingGroup)" 26 | $CountGUIDError++ 27 | } 28 | } 29 | if($CountGUIDError -gt 0) 30 | { 31 | Write-Host "`nTotal of Group ID errors found : " -NoNewline 32 | Write-Host $CountGUIDError -ForegroundColor Green 33 | Write-Host "Please review the file located at $ConfigurationFile and validate the Group IDs added to the file." 34 | Write-Host "`n#####################################################`n" -ForeGroundColor DarkYellow 35 | exit 36 | } 37 | Write-Host "`n#####################################################`n" -ForeGroundColor DarkYellow 38 | } 39 | ``` 40 |

41 | -------------------------------------------------------------------------------- /Lego/ValidateIfCSVisOpenByAnotherApp.md: -------------------------------------------------------------------------------- 1 | # Function to validate if a critical file is open by another application 2 | 3 | While working with scripts that read or write configuration files during execution, errors often occur if a file is open in another application. This function detects if a file is open and pauses script execution until the file is closed, ensuring smooth processing. 4 | 5 | ```powershell 6 | function ValidateIfCSVisOpenByAnotherApp 7 | { 8 | # Keep checking until the file is available 9 | while ($true) { 10 | try { 11 | # Try to open the file with exclusive access 12 | $fileStream = [System.IO.File]::Open($ConfigurationFile, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::None) 13 | $fileStream.Close() 14 | Write-Host "File is now available." -ForegroundColor Green 15 | break 16 | } 17 | catch { 18 | # If the file is locked, show a blinking message 19 | Write-Host "`r[WARNING] The file is currently open by another application. Please close it to proceed..." -ForegroundColor Red -NoNewline 20 | Start-Sleep -Milliseconds 1000 21 | Write-Host "`r " -NoNewline 22 | Start-Sleep -Milliseconds 500 23 | } 24 | } 25 | } 26 | ``` 27 |

28 | -------------------------------------------------------------------------------- /Lego/ValidateUPNInCSVFIle.md: -------------------------------------------------------------------------------- 1 | # Function to validate email format in a column in a CSV file 2 | 3 | This script reads the `ManagerUPN` and `GroupOwner` columns from a CSV file, validating each record to ensure that the UPN matches an email format. If any values do not conform to the email format, a counter increments for each error. If the counter is greater than 0 at the end, the script exits and displays the total number of errors. 4 | 5 | ```powershell 6 | function ValidateUPNInCSVFIle 7 | { 8 | # Regular expression to match email format 9 | $emailPattern = '^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$' 10 | $CountUPNError = 0 11 | 12 | Write-Host "`n########## Validating UPN format ##########`n" -ForeGroundColor DarkYellow 13 | # Iterate over each record in the CSV 14 | foreach ($record in $CSVFile) 15 | { 16 | # Validate the ManagerUPN field 17 | if ($record.ManagerUPN -match $emailPattern) 18 | { 19 | Write-Host "ManagerUPN valid: $($record.ManagerUPN)" 20 | }else 21 | { 22 | Write-Host "Invalid or missing ManagerUPN: $($record.ManagerUPN)" 23 | $CountUPNError++ 24 | } 25 | 26 | # Validate the GroupOwner field 27 | if ($record.GroupOwner -match $emailPattern) 28 | { 29 | Write-Host "GroupOwner valid: $($record.GroupOwner)" 30 | }elseif($record.GroupOwner -eq "") 31 | { 32 | Write-Host "Missing GroupOwner: Not Set" 33 | }else 34 | { 35 | Write-Host "Invalid format GroupOwner: $($record.GroupOwner)" 36 | $CountUPNError++ 37 | } 38 | } 39 | if($CountUPNError -gt 0) 40 | { 41 | Write-Host "`nTotal of UPN errors found : " -NoNewline 42 | Write-Host $CountUPNError -ForegroundColor Green 43 | Write-Host "Please review the file located at $ConfigurationFile and validate the UPNs added to the file." 44 | Write-Host "`n###########################################`n" -ForeGroundColor DarkYellow 45 | exit 46 | } 47 | Write-Host "`n###########################################`n" -ForeGroundColor DarkYellow 48 | } 49 | ``` 50 | -------------------------------------------------------------------------------- /Lego/WriteToLogsAnalytics.md: -------------------------------------------------------------------------------- 1 | # Function used to export data to Logs Analytics 2 | 3 | This function exports data stored in a variable in JSON format and sends it to a Log Analytics workspace. It leverages [Build-Signature](/Lego/Build-Signature.md) to establish the connection to Log Analytics. 4 | To use this function, simply pass a `TableName` and an array containing the data in JSON format. 5 | Sample way to use: 6 | ```powershell 7 | WriteToLogsAnalytics -LogAnalyticsTableName $TableName -body $TotalResults 8 | ``` 9 | 10 | ```powershell 11 | function WriteToLogsAnalytics($body, $LogAnalyticsTableName) 12 | { 13 | # --------------------------------------------------------------- 14 | # Name : Post-LogAnalyticsData 15 | # Value : Writes the data to Log Analytics using a REST API 16 | # Input : 1) PSObject with the data 17 | # 2) Table name in Log Analytics 18 | # Return : None 19 | # --------------------------------------------------------------- 20 | 21 | #Read configuration file 22 | $CONFIGFILE = "$PSScriptRoot\ConfigFiles\MSPurviewDLPConfiguration.json" 23 | $json = Get-Content -Raw -Path $CONFIGFILE 24 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 25 | 26 | $EncryptedKeys = $config.EncryptedKeys 27 | $WLA_CustomerID = $config.Workspace_ID 28 | $WLA_SharedKey = $config.WorkspacePrimaryKey 29 | if ($EncryptedKeys -eq "True") 30 | { 31 | $WLA_SharedKey = DecryptSharedKey $WLA_SharedKey 32 | } 33 | 34 | # Your Log Analytics workspace ID 35 | $LogAnalyticsWorkspaceId = $WLA_CustomerID 36 | 37 | # Use either the primary or the secondary Connected Sources client authentication key 38 | $LogAnalyticsPrimaryKey = $WLA_SharedKey 39 | 40 | #Step 0: sanity checks 41 | if($body -isnot [array]) {return} 42 | if($body.Count -eq 0) {return} 43 | 44 | #Step 1: convert the body.ResultData to JSON 45 | $json_array = @() 46 | $parse_array = @() 47 | $parse_array = $body #| ConvertFrom-Json 48 | foreach($item in $parse_array) 49 | { 50 | $json_array += $item 51 | } 52 | $json = $json_array | ConvertTo-Json -Depth 12 53 | 54 | #Step 2: convert the PSObject to JSON 55 | $bodyJson = $json 56 | #Step 2.5: sanity checks 57 | if($bodyJson.Count -eq 0) {return} 58 | 59 | #Step 3: get the UTF8 bytestream for the JSON 60 | $bodyJsonUTF8 = ([System.Text.Encoding]::UTF8.GetBytes($bodyJson)) 61 | 62 | #Step 4: build the signature 63 | $method = "POST" 64 | $contentType = "application/json" 65 | $resource = "/api/logs" 66 | $rfc1123date = [DateTime]::UtcNow.ToString("r") 67 | $contentLength = $bodyJsonUTF8.Length 68 | $signature = Build-Signature -customerId $LogAnalyticsWorkspaceId -sharedKey $LogAnalyticsPrimaryKey -date $rfc1123date -contentLength $contentLength -method $method -contentType $contentType -resource $resource 69 | 70 | #Step 5: create the header 71 | $headers = @{ 72 | "Authorization" = $signature; 73 | "Log-Type" = $LogAnalyticsTableName; 74 | "x-ms-date" = $rfc1123date; 75 | }; 76 | 77 | #Step 6: REST API call 78 | $uri = 'https://' + $LogAnalyticsWorkspaceId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01" 79 | $response = Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -ContentType $contentType -Body $bodyJsonUTF8 -UseBasicParsing 80 | 81 | if ($Response.StatusCode -eq 200) { 82 | $rows = $bodyJsonUTF8.Count 83 | Write-Information -MessageData "$rows rows written to Log Analytics workspace $uri" -InformationAction Continue 84 | } 85 | } 86 | ``` 87 |

88 | -------------------------------------------------------------------------------- /LegoPlus/AboutLegoPlus.md: -------------------------------------------------------------------------------- 1 | # About these Lego Plus Pieces. 2 | 3 |

4 |

5 |

PowerShell Special LEGO pieces

6 |
7 | 8 | Just as there are specialized Lego sets with unique pieces for specific models, this repository includes two categories of PowerShell functions: [Lego](/Lego/HOWTO.md) and [LegoPlus](/LegoPlus/AboutLegoPlus.md). The Lego functions represent versatile, commonly-used components applicable in almost any script, helping streamline repetitive tasks and improve efficiency. 9 | 10 | On the other hand, `LegoPlus` functions contain specialized pieces designed for specific scenarios and often require additional services, such as the **Microsoft Graph API**. These functions allow for deeper integration and enhanced functionality by connecting to various Microsoft services, enabling solutions for more complex cases. Additionally, other APIs are available within `LegoPlus`, expanding the range of tasks these functions can accomplish. 11 | 12 | This repository provides a robust toolkit(I'll be working to growth this part), making it easy to build both standard and advanced PowerShell scripts to meet a wide range of needs. 13 |

14 | -------------------------------------------------------------------------------- /LegoPlus/Microsoft Entra/CreateMicrosoftEntraGroup.md: -------------------------------------------------------------------------------- 1 | # Function to create groups in Microsoft Entra 2 | 3 | Using Microsoft Graph API, this function permit to update an create a `Security` or `Microsoft365` group under Microsoft Entra using a new name set in a CSV file, adding users from an Array that contains Users Ids. 4 | 5 | > [!WARNING] 6 | > This function uses `Microsoft Graph API` 7 | 8 | ```powershell 9 | function CreateMicrosoftEntraGroup([array]$MembershipGroup, $GroupName, $GroupType, $GroupDescription, $GroupOwner, $ManagerAsOwner) 10 | { 11 | $groups = get-mggroup -All 12 | $GroupExists = $groups | Where-Object { $_.DisplayName -eq $GroupName } 13 | if($GroupExists) 14 | { 15 | Write-Host "Group '$GroupName' exists." -ForeGroundColor DarkYellow 16 | Return 17 | }else 18 | { 19 | # Case: Create a new group 20 | if ($GroupType -eq "Microsoft365") { 21 | # Create Microsoft 365 Group 22 | $newGroup = New-MgGroup -DisplayName $GroupName ` 23 | -Description $GroupDescription ` 24 | -MailEnabled:$true ` 25 | -SecurityEnabled:$false ` 26 | -MailNickname ($GroupName -replace " ", "") ` 27 | -GroupTypes @("Unified") 28 | } 29 | elseif ($GroupType -eq "Security") { 30 | # Create Security Group 31 | $newGroup = New-MgGroup -DisplayName $GroupName ` 32 | -Description $GroupDescription ` 33 | -MailNickname ($GroupName -replace " ", "") ` 34 | -MailEnabled:$false ` 35 | -SecurityEnabled:$true 36 | } 37 | 38 | # If the group was successfully created 39 | if ($newGroup) 40 | { 41 | # Update the CSV: Clear NewGroup, populate ExistingGroup with Group ID 42 | # Return an array to reeplace values 43 | #$group.ExistingGroup = $newGroup.Id 44 | #$group.NewGroup = "" # Clear the NewGroup field 45 | 46 | Write-Host "Group created: $($newGroup.DisplayName), ID: $($newGroup.Id)" -ForegroundColor Green 47 | $currentMembers = Get-MgGroupMember -GroupId $newGroup.Id | Select-Object -ExpandProperty Id 48 | $currentOwners = Get-MgGroupOwner -GroupId $newGroup.Id | Select-Object -ExpandProperty Id 49 | 50 | # Optionally add Manager as Owner 51 | if ($ManagerAsOwner) 52 | { 53 | $manager = Get-MgUser -Filter "userPrincipalName eq '$($ManagerAsOwner)'" 54 | $ManagerId = $manager.Id 55 | if ($currentOwners -contains $ManagerId) 56 | { 57 | Write-Host "User $userId is already a owner of the group $($newGroup.DisplayName). Skipping..." 58 | }else 59 | { 60 | New-MgGroupOwner -GroupId $newGroup.Id -DirectoryObjectId $manager.Id 61 | Write-Host "Added manager $($ManagerAsOwner) as owner." 62 | } 63 | } 64 | 65 | if ($GroupOwner) 66 | { 67 | $owner = Get-MgUser -Filter "userPrincipalName eq '$($GroupOwner)'" 68 | $OwnerId = $owner.Id 69 | if ($currentOwners -contains $OwnerId) 70 | { 71 | Write-Host "User $userId is already a owner of the group $($newGroup.DisplayName). Skipping..." 72 | }else 73 | { 74 | New-MgGroupOwner -GroupId $newGroup.Id -DirectoryObjectId $owner.Id 75 | Write-Host "Added additional user $($GroupOwner) as owner." 76 | } 77 | 78 | } 79 | 80 | # Add members from the MembershipGroup array to the newly created group 81 | foreach ($userId in $MembershipGroup) 82 | { 83 | if ($currentMembers -contains $userId) 84 | { 85 | Write-Host "User $userId is already a member of the group $($newGroup.DisplayName). Skipping..." 86 | }else 87 | { 88 | New-MgGroupMember -GroupId $newGroup.Id -DirectoryObjectId $userId 89 | Write-Host "Added user $userId to new group $($newGroup.DisplayName)" 90 | } 91 | } 92 | }else 93 | { 94 | Write-Host "Failed to create group: $($group.NewGroup)" -ForegroundColor Red 95 | } 96 | 97 | $GroupID = $newGroup.Id 98 | Return $GroupID 99 | } 100 | } 101 | 102 | ``` 103 |

104 | -------------------------------------------------------------------------------- /LegoPlus/Microsoft Entra/Update-ExistingGroup.md: -------------------------------------------------------------------------------- 1 | # Function to update groups in Microsoft Entra 2 | 3 | Using Microsoft Graph API, this function permit to update an existing group under Microsoft Entra using the Group ID as identity, adding users from an Array that contains Users Ids. 4 | 5 | > [!WARNING] 6 | > This function uses `Microsoft Graph API` 7 | 8 | ```powershell 9 | function Update-ExistingGroup($GroupId, $GroupDescription, $GroupType, [array]$MembershipGroup) 10 | { 11 | ValidateExistingGroupField 12 | 13 | # Fetch the group to validate its existence 14 | $existingGroup = Get-MgGroup -GroupId $ 15 | $currentMembers = Get-MgGroupMember -GroupId $GroupId | Select-Object -ExpandProperty Id 16 | 17 | Write-Host "Updating an existing group: $($GroupId)`n" -ForegroundColor Yellow 18 | 19 | if ($existingGroup) { 20 | Write-Host "Updating group: $($existingGroup.DisplayName), ID: $GroupId" -ForegroundColor Yellow 21 | 22 | # Determine the group type based on MailEnabled and SecurityEnabled 23 | if ($GroupType -eq "Microsoft365") { 24 | # Microsoft 365 Group 25 | Update-MgGroup -GroupId $GroupId -Description $GroupDescription 26 | Write-Host "Updated Microsoft 365 group: $($existingGroup.DisplayName)" -ForegroundColor Green 27 | 28 | }elseif ($GroupType -eq "Security" ) 29 | { 30 | # Security Group 31 | Update-MgGroup -GroupId $GroupId -Description $GroupDescription 32 | Write-Host "Updated Security group: $($existingGroup.DisplayName)" -ForegroundColor Green 33 | 34 | }else 35 | { 36 | Write-Host "Unknown group type. Unable to update." -ForegroundColor Red 37 | } 38 | 39 | # Add members to the existing group from the MembershipGroup array 40 | foreach ($userId in $MembershipGroup) { 41 | if ($currentMembers -contains $userId) 42 | { 43 | Write-Host "User $userId is already a member of the group $($newGroup.DisplayName). Skipping..." 44 | }else 45 | { 46 | New-MgGroupMember -GroupId $GroupId -DirectoryObjectId $userId 47 | Write-Host "Added user $userId to new group $($newGroup.DisplayName)" 48 | } 49 | } 50 | 51 | # Remove members from currentMembers that are not in MembershipGroup 52 | $membersToRemove = $currentMembers | Where-Object { $MembershipGroup -notcontains $_ } 53 | foreach ($userId in $membersToRemove) { 54 | #Remove-MgGroupMember -GroupId $GroupId -DirectoryObjectId $userId 55 | Remove-MgGroupMemberByRef -GroupId $GroupId -DirectoryObjectId $userId 56 | Write-Host "Removed user $userId from group $($existingGroup.DisplayName)" 57 | } 58 | 59 | } else { 60 | Write-Host "Group with ID $GroupId does not exist." -ForegroundColor Red 61 | } 62 | } 63 | ``` 64 |

65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerShell "Lego" Learning Approach 2 | 3 |

4 |

5 |

PowerShell as LEGO

6 |
7 | 8 | Throughout my journey in mastering PowerShell, I’ve developed a unique approach to streamline my workflow by breaking down complex tasks into modular, reusable functions. Much like assembling LEGO pieces, this method allows me to easily combine specific functions to construct any solution I need. This strategy not only accelerates development but also fosters a structured approach that scales well with diverse project requirements. 9 |

10 | 11 | Every function I create is tested in PowerShell 7, ensuring they perform reliably across various environments. I’ve made these functions available to you, welcoming you to explore, adapt, and integrate them into your own projects. Your feedback and insights are valuable, so feel free to share any ideas or experiences! 12 |

13 | 14 | Here are some PowerShell-specific activity samples based on my expertise and focus areas: 15 |
16 | 1. Function-Based PowerShell Development 17 | - Creating reusable, modular functions to streamline task automation 18 | - Designing PowerShell functions as “LEGO pieces” that can be assembled for complex workflows 19 | - Testing and validating functions in PowerShell 7 for reliability and cross-platform support 20 | 21 | 2. PowerShell for Microsoft Entra Management 22 | - Automating user role assignments and management in Microsoft Entra 23 | - Scripting to identify and manage users with direct reports 24 | - Integrating PowerShell with Microsoft Entra to retrieve organizational insights 25 | 26 | 3. RBAC Role Assignments with PowerShell 27 | - Assigning Purview-specific RBAC roles via PowerShell scripts 28 | - Automating Microsoft 365 role assignments, including Content Explorer roles for compliance needs 29 | - Managing access and permissions efficiently through role-based assignments 30 | 31 | 4. Data Loss Prevention (DLP) Automation in PowerShell 32 | - Creating and managing DLP policies and rules in PowerShell for compliance reporting 33 | - Automating condition-based measures, like counting specific conditions in DLP policy data 34 | - Scripting Power BI integrations with DLP data for reporting and policy adjustments through Logs Analytics integration 35 | 36 | 5. PowerShell for Microsoft Purview Information Protection 37 | - Developing PowerShell scripts to integrate with the Purview Information Protection SDK 38 | - Automating labeling, data classification, and sensitivity policies across data environments 39 | - Implementing custom sensitivity labels and rules via PowerShell for standardized data protection 40 | 41 | 6. PowerShell Error Handling and Logging Best Practices 42 | - Building scripts with robust error handling mechanisms and logging for audit trails 43 | - Implementing retry logic and detailed logging in long-running PowerShell jobs 44 | - Structuring scripts to capture detailed logs for compliance and troubleshooting 45 | 46 | Each of these activities highlights how you use PowerShell to achieve efficient automation, data governance, and compliance in enterprise environments. Let me know if you'd like any additional detail on these! 47 |
48 | 49 | You can start browsing these LEGO pieces [here!](/Lego) 50 |

51 | -------------------------------------------------------------------------------- /Samples/Defender/AdvanceHunting.md: -------------------------------------------------------------------------------- 1 | # Script that permit to run Advance Hunting queries 2 | 3 | This script permit to run any Advance Hunting KQL query, by default creates a file called `Query.txt` that request activities from Copilot, nevertheless, the KQL can be changed on the mentioned fila and any kind of query can be added. 4 | 5 | The following functions from the 'Lego' folder were utilized: 6 | - [ScriptVariables](/Lego/ScriptVariables.md) 7 | - [CheckIfElevated](/Lego/CheckIfElevated.md) 8 | - [CheckPowerShellVersion](/Lego/CheckPowerShellVersion.md) 9 | - [CheckRequiredModules](/Lego/CheckRequiredModules.md) 10 | - [CreateConfigFile](/Lego/CreateConfigFile.md) 11 | - [CreateNewEntraApp](/Lego/CreateNewEntraApp.md) 12 | - [Build-Signature](/Lego/Build-Signature.md) 13 | - [WriteToLogsAnalytics](/Lego/WriteToLogsAnalytics.md) 14 | 15 |
16 | You can find the complete script here 17 | 18 | ```powershell 19 | #this script is thought to get Copilot Activities through an Advanced Hunting query 20 | 21 | param ( 22 | [Parameter()] 23 | [switch]$ExportToJSONFile, 24 | [Parameter()] 25 | [switch]$CreateEntraApp 26 | ) 27 | 28 | function ScriptVariables 29 | { 30 | # Log Analytics table where the data is written to. Log Analytics will add an _CL to this name. 31 | $script:TableName = "CopilotActivities" 32 | $script:ConfigPath = $PSScriptRoot + "\ConfigFiles\Config.json" 33 | $script:QueryPath = $PSScriptRoot + "\ConfigFiles\Query.txt" 34 | $script:ExportFolderName = "ExportedData" 35 | $script:ExportPath = $PSScriptRoot + "\" + $ExportFolderName 36 | $script:GraphEndpoint = "https://graph.microsoft.com/v1.0/security/microsoft.graph.security.runHuntingQuery" 37 | } 38 | 39 | function CheckPowerShellVersion 40 | { 41 | # Check PowerShell version 42 | Write-Host "`nChecking PowerShell version... " -NoNewline 43 | if ($Host.Version.Major -gt 5) 44 | { 45 | Write-Host "`t`t`t`tPassed!" -ForegroundColor Green 46 | } 47 | else 48 | { 49 | Write-Host "Failed" -ForegroundColor Red 50 | Write-Host "`tCurrent version is $($Host.Version). PowerShell version 7 or newer is required." 51 | exit(1) 52 | } 53 | } 54 | 55 | function CheckIfElevated 56 | { 57 | $IsElevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 58 | if (!$IsElevated) 59 | { 60 | Write-Host "`nPlease start PowerShell as Administrator.`n" -ForegroundColor Yellow 61 | exit(1) 62 | } 63 | } 64 | 65 | function CheckRequiredModules 66 | { 67 | # Check PowerShell modules 68 | Write-Host "Checking PowerShell modules..." 69 | $requiredModules = @( 70 | @{Name="Microsoft.Graph.Authentication"; MinVersion="0.0"}, 71 | @{Name="Microsoft.Graph.Applications"; MinVersion="0.0"}, 72 | @{Name="Microsoft.Graph.Users"; MinVersion="0.0"}, 73 | @{Name="Microsoft.Graph.Identity.DirectoryManagement"; MinVersion="0.0"} 74 | ) 75 | 76 | $modulesToInstall = @() 77 | foreach ($module in $requiredModules) 78 | { 79 | Write-Host "`t$($module.Name) - " -NoNewline 80 | $installedVersions = Get-Module -ListAvailable $module.Name 81 | if ($installedVersions) 82 | { 83 | if ($installedVersions[0].Version -lt [version]$module.MinVersion) 84 | { 85 | Write-Host "`t`t`tNew version required" -ForegroundColor Red 86 | $modulesToInstall += $module.Name 87 | } 88 | else 89 | { 90 | Write-Host "`t`t`tInstalled" -ForegroundColor Green 91 | } 92 | } 93 | else 94 | { 95 | Write-Host "`t`t`tNot installed" -ForegroundColor Red 96 | $modulesToInstall += $module.Name 97 | } 98 | } 99 | 100 | if ($modulesToInstall.Count -gt 0) 101 | { 102 | CheckIfElevated 103 | $choices = '&Yes', '&No' 104 | 105 | $decision = $Host.UI.PromptForChoice("", "Misisng required modules. Proceed with installation?", $choices, 0) 106 | if ($decision -eq 0) 107 | { 108 | Write-Host "Installing modules..." 109 | foreach ($module in $modulesToInstall) 110 | { 111 | Write-Host "`t$module" 112 | Install-Module $module -ErrorAction Stop 113 | 114 | } 115 | Write-Host "`nModules installed. Please start the script again." 116 | exit(0) 117 | } 118 | else 119 | { 120 | Write-Host "`nExiting setup. Please install required modules and re-run the setup." 121 | exit(1) 122 | } 123 | } 124 | } 125 | 126 | function CreateCopilotQueryToFile 127 | { 128 | $Query = @" 129 | CloudAppEvents 130 | | where Timestamp >= now(-180d) 131 | | where Application contains 'Microsoft Copilot for Microsoft 365' 132 | "@ 133 | 134 | Write-Output "Writing query to $QueryPath" 135 | $Query | Out-File -FilePath $QueryPath -Encoding UTF8 136 | Write-Output "Query written successfully to $QueryPath" 137 | } 138 | 139 | function CreateConfigFile 140 | { 141 | if(-Not (Test-Path $ConfigPath )) 142 | { 143 | Write-Host "Export data directory is missing, creating a new folder called ConfigFiles" 144 | New-Item -ItemType Directory -Force -Path "$PSScriptRoot\ConfigFiles" | Out-Null 145 | } 146 | 147 | if (-not (Test-Path -Path $ConfigPath)) 148 | { 149 | $config = [ordered]@{ 150 | ClientId = "" 151 | TenantId = "" 152 | ClientSecret = "" 153 | Buffer = "1000" 154 | WorkspaceID = "" 155 | WorkspacePrimaryKey = "" 156 | } 157 | }else 158 | { 159 | Write-Host "Configuration file is available under ConfigFiles folder" 160 | return 161 | } 162 | 163 | $config | ConvertTo-Json | Out-File "$ConfigPath" 164 | Write-Host "New config file was created under ConfigFile folder." -ForegroundColor Yellow 165 | } 166 | 167 | function CreateNewEntraApp 168 | { 169 | cls 170 | Write-Host "'nYou will be prompted to add your Global Administrator credentials to login to Microsoft Entra and create a Microsoft Entra App..." 171 | Connect-MgGraph -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "Directory.ReadWrite.All", "User.ReadWrite.All" -NoWelcome 172 | 173 | if(-not (Test-Path -path $ConfigPath)) 174 | { 175 | CreateConfigFile 176 | } 177 | 178 | $json = Get-Content -Raw -Path $ConfigPath 179 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 180 | 181 | $appName = "Get Advanced Hunting" 182 | Get-MgApplication -ConsistencyLevel eventual -Count appCount -Filter "startsWith(DisplayName, '$appName')" | Out-Null 183 | if ($appCount -gt 0) 184 | { 185 | Write-Host "'$appName' app already exists.`n" 186 | Exit 187 | } 188 | 189 | # app parameters and API permissions definition 190 | $params = @{ 191 | DisplayName = $appName 192 | SignInAudience = "AzureADMyOrg" 193 | RequiredResourceAccess = @( 194 | @{ 195 | # Microsoft Graph API ID 196 | ResourceAppId = "00000003-0000-0000-c000-000000000000" 197 | ResourceAccess = @( 198 | @{ 199 | # This is the default permission added every time that a MIcrosoft Entra App is created 200 | # User.Read - Delegated 201 | Id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" 202 | Type = "Scope" 203 | }, 204 | @{ 205 | # ThreatHunting.Read.All - Application 206 | Id = "dd98c7f5-2d42-42d3-a0e4-633161547251" 207 | Type = "Role" 208 | } 209 | ) 210 | } 211 | ) 212 | } 213 | 214 | # create application 215 | $app = New-MgApplication @params 216 | $appId = $app.Id 217 | 218 | # assign owner 219 | $userId = (Get-MgUser -UserId (Get-MgContext).Account).Id 220 | $params = @{ 221 | "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$userId" 222 | } 223 | New-MgApplicationOwnerByRef -ApplicationId $appId -BodyParameter $params 224 | 225 | # ask for client secret name 226 | $keyName = "Advanced Hunting App Secret key" 227 | 228 | # create client secret 229 | $passwordCred = @{ 230 | displayName = $keyName 231 | endDateTime = (Get-Date).AddMonths(24) 232 | } 233 | 234 | $secret = Add-MgApplicationPassword -applicationId $appId -PasswordCredential $passwordCred 235 | 236 | $TenantID = (Get-MgContext).TenantId 237 | 238 | Write-Host "`nAzure application was created." 239 | Write-Host "App Name: $appName" 240 | Write-Host "App ID: $($app.AppId)" 241 | Write-Host "Tenant ID: $TenantID" 242 | Write-Host "Secret password: $($secret.SecretText)" 243 | Write-Host "`nPlease go to the Azure portal to manually grant admin consent:" 244 | Write-Host "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$($app.AppId)`n" -ForegroundColor Cyan 245 | 246 | $config.TenantId = $TenantID 247 | $config.ClientId = $app.AppId 248 | $config.ClientSecret = $secret.SecretText 249 | 250 | $config | ConvertTo-Json | Out-File $ConfigPath 251 | } 252 | 253 | function Build-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) 254 | { 255 | # --------------------------------------------------------------- 256 | # Name : Build-Signature 257 | # Value : Creates the authorization signature used in the REST API call to Log Analytics 258 | # --------------------------------------------------------------- 259 | 260 | #Original function to Logs Analytics 261 | $xHeaders = "x-ms-date:" + $date 262 | $stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource 263 | 264 | $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash) 265 | $keyBytes = [Convert]::FromBase64String($sharedKey) 266 | 267 | $sha256 = New-Object System.Security.Cryptography.HMACSHA256 268 | $sha256.Key = $keyBytes 269 | $calculatedHash = $sha256.ComputeHash($bytesToHash) 270 | $encodedHash = [Convert]::ToBase64String($calculatedHash) 271 | $authorization = 'SharedKey {0}:{1}' -f $customerId,$encodedHash 272 | return $authorization 273 | } 274 | 275 | function WriteToLogsAnalytics($body, $LogAnalyticsTableName) 276 | { 277 | # --------------------------------------------------------------- 278 | # Name : Post-LogAnalyticsData 279 | # Value : Writes the data to Log Analytics using a REST API 280 | # Input : 1) PSObject with the data 281 | # 2) Table name in Log Analytics 282 | # Return : None 283 | # --------------------------------------------------------------- 284 | 285 | #Read configuration file 286 | $json = Get-Content -Raw -Path $ConfigPath 287 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 288 | 289 | #$EncryptedKeys = $config.EncryptedKeys 290 | $WLA_CustomerID = $config.WorkspaceID 291 | $WLA_SharedKey = $config.WorkspacePrimaryKey 292 | 293 | <#if ($EncryptedKeys -eq "True") 294 | { 295 | $WLA_SharedKey = DecryptSharedKey $WLA_SharedKey 296 | }#> 297 | 298 | # Your Log Analytics workspace ID 299 | $LogAnalyticsWorkspaceId = $WLA_CustomerID 300 | 301 | # Use either the primary or the secondary Connected Sources client authentication key 302 | $LogAnalyticsPrimaryKey = $WLA_SharedKey 303 | 304 | #Step 0: sanity checks 305 | if($body -isnot [array]) {return} 306 | if($body.Count -eq 0) {return} 307 | 308 | #Step 1: convert the body.ResultData to JSON 309 | $json_array = @() 310 | $parse_array = @() 311 | $parse_array = $body #| ConvertFrom-Json 312 | foreach($item in $parse_array) 313 | { 314 | $json_array += $item 315 | } 316 | $json = $json_array | ConvertTo-Json -Depth 12 317 | 318 | #Step 2: convert the PSObject to JSON 319 | $bodyJson = $json 320 | #Step 2.5: sanity checks 321 | if($bodyJson.Count -eq 0) {return} 322 | $TotalRows = $bodyJson.Count 323 | 324 | #Step 3: get the UTF8 bytestream for the JSON 325 | $bodyJsonUTF8 = ([System.Text.Encoding]::UTF8.GetBytes($bodyJson)) 326 | 327 | #Step 4: build the signature 328 | $method = "POST" 329 | $contentType = "application/json" 330 | $resource = "/api/logs" 331 | $rfc1123date = [DateTime]::UtcNow.ToString("r") 332 | $contentLength = $bodyJsonUTF8.Length 333 | $signature = Build-Signature -customerId $LogAnalyticsWorkspaceId -sharedKey $LogAnalyticsPrimaryKey -date $rfc1123date -contentLength $contentLength -method $method -contentType $contentType -resource $resource 334 | 335 | #Step 5: create the header 336 | $headers = @{ 337 | "Authorization" = $signature; 338 | "Log-Type" = $LogAnalyticsTableName; 339 | "x-ms-date" = $rfc1123date; 340 | }; 341 | 342 | #Step 6: REST API call 343 | $uri = 'https://' + $LogAnalyticsWorkspaceId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01" 344 | $response = Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -ContentType $contentType -Body $bodyJsonUTF8 -UseBasicParsing 345 | 346 | if ($Response.StatusCode -eq 200) { 347 | Write-Information -MessageData "$TotalRows rows written to Log Analytics workspace $uri" -InformationAction Continue 348 | } 349 | } 350 | 351 | function MainFunction 352 | { 353 | $Config = Get-Content -Path $ConfigPath | ConvertFrom-Json 354 | 355 | if(-Not(Test-Path -Path $QueryPath)) 356 | { 357 | CreateCopilotQueryToFile 358 | } 359 | $Query = Get-Content -Path $QueryPath -Raw 360 | # Extract configuration values 361 | $TenantId = $Config.TenantId 362 | $ClientId = $Config.ClientId 363 | $ClientSecret = $Config.ClientSecret 364 | $Buffer = $Config.Buffer 365 | $TokenEndpoint = "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" 366 | 367 | # Step 1: Acquire Access Token using Secret Key for Microsoft Graph 368 | Write-Output "Acquiring Access Token for Microsoft Graph..." 369 | try { 370 | $TokenResponse = Invoke-RestMethod -Method Post -Uri $TokenEndpoint -ContentType "application/x-www-form-urlencoded" -Body @{ 371 | client_id = $ClientId 372 | client_secret = $ClientSecret 373 | grant_type = "client_credentials" 374 | scope = "https://graph.microsoft.com/.default" 375 | } 376 | $AccessToken = $TokenResponse.access_token 377 | Write-Output "Access Token Acquired Successfully." 378 | } catch { 379 | Write-Output "Error Acquiring Access Token: $($_.Exception.Message)" 380 | throw $_ 381 | } 382 | 383 | # Step 2: Query Execution using Microsoft Graph API with Pagination 384 | $Body = @{ 385 | "Query" = $Query 386 | } | ConvertTo-Json -Depth 10 387 | 388 | $OutputFilePath = $PSScriptRoot + "\ConfigFiles\QueryResults.json" 389 | $Headers = @{ 390 | "Authorization" = "Bearer $AccessToken" 391 | "Content-Type" = "application/json" 392 | } 393 | 394 | $AllResults = @() # Array to store all results 395 | Write-Output "Executing query via Microsoft Graph API..." 396 | try { 397 | $NextLink = $GraphEndpoint 398 | do { 399 | # Make the request 400 | $Response = Invoke-RestMethod -Method Post -Uri $NextLink -Headers $Headers -Body $Body 401 | $Results = $Response.results 402 | 403 | # Append results to the main array 404 | $AllResults += $Results 405 | 406 | # Check for next page 407 | $NextLink = $Response.'@odata.nextLink' 408 | Write-Output "Fetched $($Results.Count) records. Total so far: $($AllResults.Count)." 409 | } while ($NextLink -ne $null) 410 | 411 | # Save all results to JSON file 412 | if($ExportToJSONFile) 413 | { 414 | 415 | if(-Not (Test-Path $ExportPath)) 416 | { 417 | New-Item -ItemType Directory -Force -Path "$PSScriptRoot\$ExportFolderName" | Out-Null 418 | } 419 | $date = (Get-Date).ToString("yyyy-MM-dd HHmm") 420 | $OutputFilePath = $ExportPath + "\QueryResults - " + $date + ".json" 421 | $AllResults | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputFilePath -Encoding UTF8 422 | }else 423 | { 424 | WriteToLogsAnalytics -LogAnalyticsTableName $TableName -body $AllResults 425 | } 426 | 427 | Write-Output "Query results saved to $OutputFilePath." 428 | } catch { 429 | Write-Output "Error Executing Query. HTTP Status Code: $($_.Exception.Response.StatusCode)" 430 | Write-Output "Reason Phrase: $($_.Exception.Response.ReasonPhrase)" 431 | Write-Output "Error Content: $($_.Exception.Response.Content.ReadAsStringAsync().Result)" 432 | throw $_ 433 | } 434 | 435 | } 436 | 437 | ScriptVariables 438 | 439 | if($CreateEntraApp) 440 | { 441 | CheckPowerShellVersion 442 | CheckRequiredModules 443 | CreateNewEntraApp 444 | CreateCopilotQueryToFile 445 | exit 446 | } 447 | 448 | CheckPowerShellVersion 449 | MainFunction 450 | ``` 451 | 452 |
453 | 454 |

455 | -------------------------------------------------------------------------------- /Samples/Exchange/CreateInboxRules.md: -------------------------------------------------------------------------------- 1 | # Script to create an inbox folder and set an inboox rule by subject or sender 2 | 3 | This script allows you to create a new folder under a user's inbox if it doesn't already exist. Users can be loaded either from a CSV file or directly through the Microsoft Graph API. The script can also be automated by creating a Microsoft Entra Application with the necessary permissions pre-configured. 4 | 5 | In its current version, the script utilizes the Exchange Online PowerShell module. It requires the Exchange Administrator role to grant the necessary permissions for creating folders and inbox rules. 6 | 7 | ```powershell 8 | # Function to create a mail folder under inbox and then an Inbox rule to send certain emails to that folder 9 | 10 | [CmdletBinding(DefaultParameterSetName = "None")] 11 | param( 12 | [Parameter()] 13 | [switch]$CreateEntraApp, 14 | [Parameter()] 15 | [switch]$CreateConfigurationFile, 16 | [Parameter()] 17 | [switch]$ChangeInput, 18 | [Parameter()] 19 | [switch]$CheckDependencies 20 | ) 21 | 22 | function CheckPowerShellVersion 23 | { 24 | # Check PowerShell version 25 | Write-Host "`nChecking PowerShell version... " -NoNewline 26 | if ($Host.Version.Major -gt 5) 27 | { 28 | Write-Host "`t`t`t`tPassed!" -ForegroundColor Green 29 | } 30 | else 31 | { 32 | Write-Host "Failed" -ForegroundColor Red 33 | Write-Host "`tCurrent version is $($Host.Version). PowerShell version 7 or newer is required." 34 | exit(1) 35 | } 36 | } 37 | 38 | function CheckIfElevated 39 | { 40 | $IsElevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 41 | if (!$IsElevated) 42 | { 43 | Write-Host "`nPlease start PowerShell as Administrator.`n" -ForegroundColor Yellow 44 | exit(1) 45 | } 46 | } 47 | 48 | function CheckRequiredModules 49 | { 50 | # Check PowerShell modules 51 | Write-Host "Checking PowerShell modules..." 52 | $requiredModules = @( 53 | @{Name="ExchangeOnlineManagement"; MinVersion="0.0"}, 54 | @{Name="Microsoft.Graph.Mail"; MinVersion="0.0"}, 55 | @{Name="Microsoft.Graph.Applications"; MinVersion="0.0"}, 56 | @{Name="Microsoft.Graph.Users"; MinVersion="0.0"}, 57 | @{Name="Microsoft.Graph.Identity.DirectoryManagement"; MinVersion="0.0"} 58 | ) 59 | 60 | $modulesToInstall = @() 61 | foreach ($module in $requiredModules) 62 | { 63 | Write-Host "`t$($module.Name) - " -NoNewline 64 | $installedVersions = Get-Module -ListAvailable $module.Name 65 | if ($installedVersions) 66 | { 67 | if ($installedVersions[0].Version -lt [version]$module.MinVersion) 68 | { 69 | Write-Host "`t`t`tNew version required" -ForegroundColor Red 70 | $modulesToInstall += $module.Name 71 | } 72 | else 73 | { 74 | Write-Host "`t`t`tInstalled" -ForegroundColor Green 75 | } 76 | } 77 | else 78 | { 79 | Write-Host "`t`t`tNot installed" -ForegroundColor Red 80 | $modulesToInstall += $module.Name 81 | } 82 | } 83 | 84 | if ($modulesToInstall.Count -gt 0) 85 | { 86 | CheckIfElevated 87 | $choices = '&Yes', '&No' 88 | 89 | $decision = $Host.UI.PromptForChoice("", "Misisng required modules. Proceed with installation?", $choices, 0) 90 | if ($decision -eq 0) 91 | { 92 | Write-Host "Installing modules..." 93 | foreach ($module in $modulesToInstall) 94 | { 95 | Write-Host "`t$module" 96 | Install-Module $module -ErrorAction Stop 97 | 98 | } 99 | Write-Host "`nModules installed. Please start the script again." 100 | exit(0) 101 | } 102 | else 103 | { 104 | Write-Host "`nExiting setup. Please install required modules and re-run the setup." 105 | exit(1) 106 | } 107 | } 108 | } 109 | 110 | function UnHashCredentials 111 | { 112 | param( 113 | [string] $encryptedKey 114 | ) 115 | 116 | try { 117 | $secureKey = $encryptedKey | ConvertTo-SecureString -ErrorAction Stop 118 | } 119 | catch { 120 | Write-Error "Workspace key: $($_.Exception.Message)" 121 | exit(1) 122 | } 123 | $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureKey) 124 | $plainKey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 125 | $plainKey 126 | } 127 | 128 | function CreateConfigFile 129 | { 130 | if(-Not (Test-Path $Configfile )) 131 | { 132 | Write-Host "Export data directory is missing, creating a new folder called ConfigFiles" 133 | New-Item -ItemType Directory -Force -Path "$PSScriptRoot\ConfigFiles" | Out-Null 134 | } 135 | 136 | if (-not (Test-Path -Path $configfile)) 137 | { 138 | $config = [ordered]@{ 139 | EncryptedKeys = "False" 140 | AppClientID = "" 141 | TenantGUID = "" 142 | CertificateThumb = "" 143 | TenantDomain = "" 144 | OnmicrosoftTenant = "" 145 | InputMethod = "CSV" 146 | RuleType = "Subject" 147 | RuleValue = "[E-Migrator]" 148 | } 149 | }else 150 | { 151 | Write-Host "Configuration file is available under ConfigFiles folder" 152 | return 153 | } 154 | 155 | $config | ConvertTo-Json | Out-File "$configfile" 156 | Write-Host "New config file was created under ConfigFile folder." -ForegroundColor Yellow 157 | } 158 | 159 | #Creates the CSV file used as main an unique input to create groups 160 | function CreateCSVFile 161 | { 162 | if(-not (Test-Path -Path $PathFolder)) 163 | { 164 | New-Item -ItemType Directory -Force -Path "$PSScriptRoot\ConfigFiles" | Out-Null 165 | } 166 | 167 | # Check if the CSV file already exists 168 | if (-Not (Test-Path $csvFilePath)) 169 | { 170 | # Create a CSV structure 171 | $UPN = "UserPrincipalName@yourdomain.com" 172 | $data = @{ 173 | AccountUPN = $UPN 174 | } 175 | # If file does not exist, create it with headers 176 | $data | Export-Csv -Path $csvFilePath -NoTypeInformation 177 | Write-Host "Created new CSV file: $csvFilePath" 178 | Write-Host "`nPlease complete the file with the right UPNs.`n" -ForegroundColor Blue 179 | Write-host "You can change the input method to use Microsoft Graph API instead of CSV file executing:" 180 | Write-host ".\ResolveMailAccount.ps1 -ChangeInput `n" -ForeGroundColor Green 181 | exit 182 | } else 183 | { 184 | # If file exists, append new data 185 | Write-Host "File is existing on path." 186 | return 187 | } 188 | } 189 | 190 | function CheckCertificateInstalled($thumbprint) 191 | { 192 | $var = "False" 193 | $certificates = @(Get-ChildItem Cert:\CurrentUser\My -SSLServerAuthentication | Select-Object Thumbprint) 194 | #$thumbprint -in $certificates 195 | foreach($certificate in $certificates) 196 | { 197 | if($thumbprint -in $certificate.Thumbprint) 198 | { 199 | $var = "True" 200 | } 201 | } 202 | if($var -eq "True") 203 | { 204 | Write-Host "Certificate validation..." -NoNewLine 205 | Write-Host "`t`tPassed!" -ForegroundColor Green 206 | return $var 207 | }else 208 | { 209 | Write-Host "`nCertificate installed on this machine is missing!!!" -ForeGroundColor Yellow 210 | Write-Host "To execute this script unattended a certificate needs to be installed, the same used under Microsoft Entra App" 211 | Start-Sleep -s 1 212 | return $var 213 | } 214 | } 215 | 216 | function CreateNewEntraApp 217 | { 218 | $appName = "E-Migrator Mail resolver" 219 | 220 | if (Get-MgContext) 221 | { 222 | Write-Host "Disconnecting from previous session opened..." 223 | disconnect-MgGraph 224 | } 225 | 226 | Write-Host "Connecting to Microsoft Graph API" 227 | Connect-MgGraph -Scopes "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All", "Directory.ReadWrite.All", "User.ReadWrite.All", "Domain.Read.All" -NoWelcome 228 | 229 | if(-not (Test-Path -path $Configfile)) 230 | { 231 | CreateConfigFile 232 | } 233 | 234 | $json = Get-Content -Raw -Path $Configfile 235 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 236 | 237 | Get-MgApplication -ConsistencyLevel eventual -Count appCount -Filter "startsWith(DisplayName, '$appName')" | Out-Null 238 | if ($appCount -gt 0) 239 | { 240 | cls 241 | Write-Host "`n`n'$appName' app already exists.`n" 242 | Write-Host "You can run now the script as:" -NoNewline 243 | Write-Host "`t.\ResolveMailAccount.ps1`n`n" -ForeGroundColor Green 244 | Exit 245 | } 246 | 247 | # app parameters and API permissions definition 248 | $params = @{ 249 | DisplayName = $appName 250 | SignInAudience = "AzureADMyOrg" 251 | RequiredResourceAccess = @( 252 | @{ 253 | # Microsoft Graph API ID 254 | ResourceAppId = "00000003-0000-0000-c000-000000000000" 255 | ResourceAccess = @( 256 | @{ 257 | # This is the default permission added every time that a MIcrosoft Entra App is created 258 | # User.Read - Delegated 259 | Id = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" 260 | Type = "Scope" 261 | }, 262 | @{ 263 | # This permission is required to create the new folder on Inbox and create then the inbox rule 264 | # Mail.ReadWrite - Application 265 | Id = "e2a3a72e-5f79-4c64-b1b1-878b674786c9" 266 | Type = "Role" 267 | }, 268 | @{ 269 | # This permission permit get a list of EXO licensed users 270 | # User.Read.All - Application 271 | Id = "df021288-bdef-4463-88db-98f22de89214" 272 | Type = "Role" 273 | } 274 | ) 275 | }, 276 | @{ 277 | # Office 365 Exchange Online API 278 | ResourceAppId = "00000002-0000-0ff1-ce00-000000000000" 279 | ResourceAccess = @( 280 | @{ 281 | # This permission is required to create the new folder on Inbox and create then the inbox rule 282 | # Exchange.ManageAsApp - Application 283 | Id = "dc50a0fb-09a3-484d-be87-e023b12c6440" 284 | Type = "Role" 285 | } 286 | ) 287 | } 288 | ) 289 | } 290 | 291 | # create application 292 | $app = New-MgApplication @params 293 | $appId = $app.Id 294 | 295 | # assign owner 296 | $userId = (Get-MgUser -UserId (Get-MgContext).Account).Id 297 | $params = @{ 298 | "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$userId" 299 | } 300 | New-MgApplicationOwnerByRef -ApplicationId $appId -BodyParameter $params 301 | 302 | # ask for certificate name 303 | $certName = "$appName"+" Certificate" 304 | 305 | # certificate life 306 | $validMonths = 24 307 | 308 | # create key 309 | $cert = New-SelfSignedCertificate -DnsName $certName -CertStoreLocation "cert:\CurrentUser\My" -NotAfter (Get-Date).AddMonths($validMonths) 310 | $certBase64 = [System.Convert]::ToBase64String($cert.RawData) 311 | $keyCredential = @{ 312 | type = "AsymmetricX509Cert" 313 | usage = "Verify" 314 | key = [System.Text.Encoding]::ASCII.GetBytes($certBase64) 315 | } 316 | while (-not (Get-MgApplication -ApplicationId $appId -ErrorAction SilentlyContinue)) 317 | { 318 | Write-Host "Waiting while app is being created..." 319 | Start-Sleep -Seconds 5 320 | } 321 | Update-MgApplication -ApplicationId $appId -KeyCredentials $keyCredential -ErrorAction Stop 322 | $TenantID = (Get-MgContext).TenantId 323 | 324 | #Get main domains 325 | $Domains = Get-MgDomain -All 326 | $OnMicrosoftDomain = $Domains | Where-Object { $_.isInitial -eq $true } 327 | $PrimaryDomain = $Domains | Where-Object { $_.IsDefault -eq $true } 328 | 329 | Write-Host "`nAzure application was created." 330 | Write-Host "App Name: $appName" 331 | Write-Host "App ID: $($app.AppId)" 332 | Write-Host "Tenant ID: $TenantID" 333 | Write-Host "Certificate thumbprint: $($cert.Thumbprint)" 334 | Write-Host "Tenant default domain: $($PrimaryDomain.Id)" 335 | Write-Host "Tenant onmicrosoft domain: $($OnMicrosoftDomain.Id)" 336 | 337 | Write-Host "`nPlease go to the Azure portal to manually grant admin consent:" 338 | Write-Host "https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/CallAnAPI/appId/$($app.AppId)`n" -ForegroundColor Cyan 339 | 340 | $config.TenantGUID = $TenantID 341 | $config.AppClientID = $app.AppId 342 | $config.CertificateThumb = $cert.Thumbprint 343 | $config.TenantDomain = $PrimaryDomain.Id 344 | $config.OnmicrosoftTenant = $OnMicrosoftDomain.Id 345 | 346 | $config | ConvertTo-Json | Out-File $Configfile 347 | 348 | Remove-Variable cert 349 | Remove-Variable certBase64 350 | } 351 | 352 | function Connect2MicrosoftGraphService 353 | { 354 | $json = Get-Content -Raw -Path $ConfigFile 355 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 356 | 357 | $EncryptedKeys = $config.EncryptedKeys 358 | $AppClientID = $config.AppClientID 359 | $CertificateThumb = $config.CertificateThumb 360 | $TenantGUID = $config.TenantGUID 361 | 362 | $status = CheckCertificateInstalled -thumbprint $CertificateThumb 363 | 364 | if($status -eq "True") 365 | { 366 | Connect-MgGraph -CertificateThumbPrint $CertificateThumb -AppID $AppClientID -TenantId $TenantGUID -NoWelcome 367 | } 368 | 369 | if (!(Get-MgContext)) { 370 | throw "Failed to connect to Microsoft Graph." 371 | exit 372 | } 373 | } 374 | 375 | function connect2ExchangeOnline 376 | { 377 | $json = Get-Content -Raw -Path $Configfile 378 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 379 | 380 | $EncryptedKeys = $config.EncryptedKeys 381 | $AppClientID = $config.AppClientID 382 | $CertificateThumb = $config.CertificateThumb 383 | $OnmicrosoftTenant = $config.OnmicrosoftTenant 384 | if ($EncryptedKeys -eq "True") 385 | { 386 | $CertificateThumb = UnHashCredentials $CertificateThumb 387 | } 388 | $status = "True" 389 | 390 | if($status -eq "True") 391 | { 392 | Connect-ExchangeOnline -CertificateThumbPrint $CertificateThumb -AppID $AppClientID -Organization $OnmicrosoftTenant -ShowBanner:$false 393 | }else 394 | { 395 | Write-Host "`nThe Certificate set in config.json don't match with the certificates installed on this machine, you can try to execute using manual connection, to do that extecute: " 396 | Write-Host ".\GetDataExplorer2.ps1 -ManualConnection" -ForeGroundColor Green 397 | exit 398 | } 399 | } 400 | 401 | function GetM365Accounts 402 | { 403 | $mailEnabledUser = Get-MgUser -Filter "assignedLicenses/`$count ne 0 and userType eq 'Member'" -ConsistencyLevel eventual -CountVariable licensedUserCount -All 404 | Write-Host "`nTotal active users : "$mailEnabledUser.count 405 | Write-Host "Identifying users with email enabled.`n" 406 | return $mailEnabledUser 407 | } 408 | 409 | function ValidateUPNInCSVFIle 410 | { 411 | $CSVFile = Import-Csv -Path $csvFilePath 412 | # Regular expression to match email format 413 | $emailPattern = '^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$' 414 | $CountUPNError = 0 415 | 416 | Write-Host "`n########## Validating UPN format ##########`n" -ForeGroundColor DarkYellow 417 | # Iterate over each record in the CSV 418 | foreach ($record in $CSVFile) 419 | { 420 | # Validate the ManagerUPN field 421 | if ($record.AccountUPN -match $emailPattern) 422 | { 423 | Write-Host "Account UPN valid: $($record.AccountUPN)" 424 | }else 425 | { 426 | Write-Host "Invalid or missing Account UPN: $($record.AccountUPN)" 427 | $CountUPNError++ 428 | } 429 | } 430 | if($CountUPNError -gt 0) 431 | { 432 | Write-Host "`nTotal of UPN errors found : " -NoNewline 433 | Write-Host $CountUPNError -ForegroundColor Green 434 | Write-Host "Please review the file located at $ConfigurationFile and validate the UPNs added to the file." 435 | Write-Host "`n###########################################`n" -ForeGroundColor DarkYellow 436 | exit 437 | } 438 | Write-Host "`n###########################################`n" -ForeGroundColor DarkYellow 439 | } 440 | 441 | function Create-MailFolder($UPN) 442 | { 443 | #Validate if the folder previously exist 444 | try 445 | { 446 | # Check if the folder already exists 447 | $existingFolder = Get-MgUserMailFolderChildFolder -UserId $UPN -MailFolderId "inbox" | Where-Object { $_.DisplayName -eq $FolderName } 448 | 449 | if ($null -ne $existingFolder) { 450 | Write-Host "Folder '$FolderName' already exists under Inbox for $UPN. Skipping creation." 451 | #return $existingFolder.Id 452 | } 453 | 454 | # Create the folder if it doesn't exist 455 | $newFolder = New-MgUserMailFolderChildFolder -UserId $UPN -MailFolderId "inbox" -BodyParameter @{ 456 | DisplayName = $FolderName 457 | } 458 | 459 | Write-Host "Folder '$FolderName' created successfully under Inbox for $UPN." 460 | return $newFolder.Id 461 | }catch 462 | { 463 | # Log the error to the error collector CSV 464 | $errorMessage = $_.Exception.Message 465 | $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" 466 | $errorEntry = "$UPN,$errorMessage,$timestamp" 467 | Add-Content -Path $ErrorFolderCreation -Value $errorEntry 468 | Write-Error "Failed to create folder for $UPN. Error logged to $ErrorFolderCreation." 469 | } 470 | } 471 | 472 | function Create-InboxRuleUsingExchangeOnline($UPN) 473 | { 474 | $json = Get-Content -Raw -Path $Configfile 475 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 476 | $RuleType = $config.RuleType 477 | $RuleValue = $config.RuleValue 478 | $TargetFolder = "$($UPN):\Inbox\"+$FolderName 479 | 480 | $mailbox = Get-Mailbox -Identity $UPN -ErrorAction SilentlyContinue 481 | 482 | if ($mailbox -eq $null) 483 | { 484 | Write-Output "Mailbox is inactive or not provisioned for user $UPN." 485 | return 486 | }else 487 | { 488 | Write-Output "Mailbox is active for user: $($mailbox.DisplayName)" 489 | } 490 | 491 | $folder = Get-MgUserMailFolderChildFolder -UserId $UPN -MailFolderId "inbox" | Where-Object { $_.DisplayName -eq $FolderName } 492 | 493 | if ($null -eq $folder) 494 | { 495 | Write-Host "Folder '$FolderName' not found under Inbox for $UPN." 496 | Create-MailFolder -UPN $UPN 497 | } 498 | 499 | #Get all the inbox rules per user 500 | $inboxRules = Get-InboxRule -Mailbox $UPN 501 | 502 | # Check if the rule already exists 503 | if($RuleType -eq "Sender") 504 | { 505 | $matchingRules = $inboxRules | Where-Object {$_.Name -eq "Move Emails From $RuleValue"} 506 | }elseif($RuleType -eq "Subject") 507 | { 508 | $matchingRules = $inboxRules | Where-Object {$_.Name -eq "Move Emails With Subject $RuleValue"} 509 | } 510 | 511 | if ($matchingRules.count -gt 0) 512 | { 513 | Write-Host "Inbox rule for '$RuleValue' already exists for $UPN. Skipping creation." 514 | return 515 | } 516 | 517 | try 518 | { 519 | # Create the rule 520 | Write-Host "Target folder " $TargetFolder 521 | if ($RuleType -eq "Sender") 522 | { 523 | New-InboxRule -Mailbox $UPN -Name "Move Emails From $RuleValue" ` 524 | -From $RuleValue -MoveToFolder $TargetFolder 525 | } elseif ($RuleType -eq "Subject") 526 | { 527 | New-InboxRule -Mailbox $UPN -Name "Move Emails With Subject $RuleValue" ` 528 | -SubjectContainsWords @($RuleValue) -MoveToFolder $TargetFolder 529 | } 530 | 531 | Write-Host "Inbox rule created to move emails by $RuleType is '$RuleValue' to folder '$FolderName' for $UPN." 532 | $FolderID = $TargetFolder 533 | $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" 534 | $TrackEntry = "$UPN,$FolderName,$RuleType,$RuleValue,$timestamp" 535 | Add-Content -Path $TrackFile -Value $TrackEntry 536 | }catch 537 | { 538 | # Log the error to the error collector CSV 539 | $errorMessage = $_.Exception.Message 540 | $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" 541 | $errorEntry = "$UPN,$FolderName,$RuleType,$RuleValue,$errorMessage,$timestamp" 542 | 543 | Add-Content -Path $ErrorRuleCreation -Value $errorEntry 544 | Write-Error "Failed to create Inbox rule for $UPN. Error logged to $ErrorRuleCreation." 545 | } 546 | 547 | } 548 | 549 | #All the changes related to group are set in the CSV file, if it's the file is open can drop the script 550 | function ValidateIfCSVisOpenByAnotherApp 551 | { 552 | # Keep checking until the file is available 553 | while ($true) { 554 | try { 555 | # Try to open the file with exclusive access 556 | $fileStream = [System.IO.File]::Open($csvFilePath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::None) 557 | $fileStream.Close() 558 | Write-Host "File is now available." -ForegroundColor Green 559 | break 560 | } 561 | catch { 562 | # If the file is locked, show a blinking message 563 | Write-Host "`r[WARNING] The file is currently open by another application. Please close it to proceed..." -ForegroundColor Red -NoNewline 564 | Start-Sleep -Milliseconds 1000 565 | Write-Host "`r " -NoNewline 566 | Start-Sleep -Milliseconds 500 567 | } 568 | } 569 | } 570 | 571 | function MainFunction 572 | { 573 | cls 574 | 575 | if(-Not (Test-Path -Path $Configfile )) 576 | { 577 | Write-Host "`nIf you need to validate that you have the right PowerShell modules you can execute:`n" 578 | Write-host ".\ResolveMailAccount.ps1 -CheckDependencies" -ForeGroundColor Green 579 | Write-Host "`nConfiguration file not available, you need to execute:`n" 580 | Write-host ".\ResolveMailAccount.ps1 -CreateEntraApp `n`n" -ForeGroundColor Green 581 | exit 582 | } 583 | 584 | Connect2MicrosoftGraphService 585 | 586 | if (-Not (Test-Path -Path $ErrorFolderCreation)) 587 | { 588 | # Create the file with headers if it doesn't exist 589 | "UPN,ErrorMessage,TimeStamp" | Out-File -FilePath $ErrorFolderCreation -Encoding UTF8 590 | } 591 | 592 | # Ensure the error collector file exists 593 | if (!(Test-Path -Path $ErrorRuleCreation)) { 594 | # Create the file with headers if it doesn't exist 595 | "UPN,FolderName,RuleType,RuleValue,ErrorMessage,TimeStamp" | Out-File -FilePath $ErrorRuleCreation -Encoding UTF8 596 | } 597 | 598 | if (-Not (Test-Path -Path $TrackFile)) 599 | { 600 | # Create the file with headers if it doesn't exist 601 | "UPN,FolderName,RuleType,$uleValue,timestamp" | Out-File -FilePath $TrackFile -Encoding UTF8 602 | } 603 | 604 | if($ChangeInput) 605 | { 606 | $json = Get-Content -Raw -Path $Configfile 607 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 608 | $method = $config.InputMethod 609 | 610 | if($method -eq "CSV") 611 | { 612 | $config.InputMethod = "GRAPH" 613 | }elseif($method -eq "GRAPH") 614 | { 615 | $config.InputMethod = "CSV" 616 | } 617 | 618 | $config | ConvertTo-Json | Out-File "$configfile" 619 | } 620 | 621 | $json = Get-Content -Raw -Path $Configfile 622 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 623 | $InputMethodToUse = $config.InputMethod 624 | if($InputMethodToUse -eq "CSV") 625 | { 626 | CreateCSVFile 627 | ValidateIfCSVisOpenByAnotherApp 628 | $CSVFile = Import-Csv -Path $csvFilePath 629 | ValidateUPNInCSVFIle 630 | $TotalRows = $CSVFile.count 631 | if($TotalRows -eq "1") 632 | { 633 | if($CSVFile.AccountUPN -eq "UserPrincipalName@yourdomain.com") 634 | { 635 | Write-Host "You are using the sample data in the file, please replace with the right one." 636 | Write-Host "Exiting...`n`n" 637 | exit 638 | } 639 | } 640 | 641 | Write-Host "`nConnecting to Exchange Online...`n" -ForeGroundColor Green 642 | connect2ExchangeOnline 643 | 644 | foreach($account in $CSVFile) 645 | { 646 | Create-InboxRuleUsingExchangeOnline -UPN $account.AccountUPN 647 | } 648 | Write-Host "`nProcess finished...`n" 649 | Write-Host "################################################################################`n`n" -ForeGroundColor DarkYellow 650 | } 651 | if($InputMethodToUse -eq "GRAPH") 652 | { 653 | Write-Host "`nConnecting to Exchange Online...`n" -ForeGroundColor Green 654 | connect2ExchangeOnline 655 | 656 | $Accounts = GetM365Accounts 657 | foreach($account in $Accounts) 658 | { 659 | Create-InboxRuleUsingExchangeOnline -UPN $account.UserPrincipalName 660 | } 661 | Write-Host "`nProcess finished...`n" 662 | Write-Host "################################################################################`n`n" -ForeGroundColor DarkYellow 663 | } 664 | } 665 | 666 | # Here global variables are set 667 | $ConfigFile = $PSScriptRoot+"\ConfigFiles\configurationFile.json" 668 | $ErrorFolderCreation = $PSScriptRoot+"\ConfigFiles\ErrorFolderCreation.Csv" 669 | $ErrorRuleCreation = $PSScriptRoot+"\ConfigFiles\ErrorRuleCreation.Csv" 670 | $TrackFile = $PSScriptRoot+"\ConfigFiles\TrackFile.Csv" 671 | $csvFilePath = "$PSScriptRoot\ConfigFiles\InputMailAccounts.csv" 672 | $PathFolder = $PSScriptRoot+"\ConfigFiles" 673 | $FolderName = "E-Migrator" 674 | 675 | # Only to create the Microsoft Entra App to automate the proecess 676 | if($CreateEntraApp) 677 | { 678 | CreateNewEntraApp 679 | exit 680 | } 681 | 682 | # Validate if all the minimal requirements are available 683 | if($CheckDependencies) 684 | { 685 | cls 686 | Write-Host "`nValidating dependencies...`n" -ForeGroundColor Green 687 | CheckPowerShellVersion 688 | CheckIfElevated 689 | CheckRequiredModules 690 | Write-Host "`n`n" 691 | return 692 | } 693 | 694 | if($CreateConfigurationFile) 695 | { 696 | CreateConfigFile 697 | exit 698 | } 699 | 700 | # Main script 701 | MainFunction 702 | ``` 703 | -------------------------------------------------------------------------------- /Samples/General/Hash-UnHash.md: -------------------------------------------------------------------------------- 1 | # Solution to learn about hash passwords 2 | 3 | Often, there’s a need to store `credentials` locally, which can pose a **security risk** if stored in **plain text**. In this example, a configuration file in JSON format is created, where you can manually add a username and password. This setup demonstrates how only the password value can be hashed, while the rest of the file remains in plain text. An attribute called `HashedPasswords` acts as a flag to indicate if the password is hashed. 4 | 5 | The same script can also unhash the stored password, providing a simple solution for secure password storage. This process uses PowerShell cmdlets that rely on both the machine ID and the logged-in user. Consequently, only the original user on the same machine can unhash the password, adding an extra layer of security if the file is moved to another machine or accessed by a different user." 6 | 7 | 8 | ```powershell 9 | # Script to explain how to hash passwords using PowerShell 10 | 11 | # Attributes to be used with the script 12 | param( 13 | [Parameter()] 14 | [switch]$Hash, 15 | [Parameter()] 16 | [switch]$UnHash, 17 | [Parameter()] 18 | [switch]$CredentialsFile 19 | ) 20 | 21 | # Define paths 22 | $configFolder = $PSScriptRoot+"\ConfigFiles" 23 | $jsonFile = "$configFolder\MyCredentials.json" 24 | 25 | function ConfigFile 26 | { 27 | # Validate if the directory exist 28 | if(-Not (Test-Path $configFolder )) 29 | { 30 | Write-Host "`nExport data directory is missing, creating a new folder called ConfigFiles" 31 | New-Item -ItemType Directory -Force -Path $configFolder | Out-Null 32 | } 33 | 34 | # Validate if the configuration file exists 35 | if (-Not (Test-Path -Path $jsonFile)) 36 | { 37 | $config = [ordered]@{ 38 | HashedPassword = "False" 39 | MyUser = "" 40 | MyPassword = "" 41 | } 42 | }else 43 | { 44 | Write-Host "`nConfiguration file is available under ConfigFiles folder.`n" 45 | exit 46 | } 47 | 48 | $config | ConvertTo-Json | Out-File $jsonFile 49 | Write-Host "`nNew config file was created under ConfigFile folder.`n" -ForegroundColor Yellow 50 | } 51 | 52 | function ValidateConfigurationFile 53 | { 54 | if (-not (Test-Path -Path $jsonFile)) 55 | { 56 | Write-Host "`nMissing config file '$jsonFile'." -ForegroundColor Yellow 57 | Write-Host "`nTo create the missing file please execute " -NoNewLine 58 | Write-Host ".\Hash-UnHash.ps1 -CredentialsFile`n" -ForegroundColor Green 59 | exit 60 | } 61 | } 62 | 63 | function HashCredentials 64 | { 65 | # Validate if the password file exists 66 | ValidateConfigurationFile 67 | 68 | $json = Get-Content -Raw -Path $jsonFile 69 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 70 | $HashedPassword = $config.HashedPassword 71 | 72 | # Check if already encrypted 73 | if ($HashedPassword -eq "True") 74 | { 75 | Write-Host "`nAccording to the configuration settings (HashedPassword: True), password is already hashed." -ForegroundColor Yellow 76 | Write-Host "`nNo actions taken.`n" 77 | return 78 | } 79 | 80 | # Encrypt password 81 | $UserPassword = $config.MyPassword 82 | $UserPassword = $UserPassword | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString 83 | 84 | # Write results to the password file 85 | $config.HashedPassword = "True" 86 | $config.MyPassword = $UserPassword 87 | 88 | $date = Get-Date -Format "yyyyMMddHHmmss" 89 | Move-Item "$jsonFile" "$PSScriptRoot\ConfigFiles\MyCredentials_$date.json" 90 | Write-Host "`nPassword hashed." 91 | Write-Host "`nA backup was created with name " -NoNewLine 92 | Write-Host "'MyCredentials_$date.json'`n" -ForegroundColor Green 93 | $config | ConvertTo-Json | Out-File $jsonFile 94 | 95 | Write-Host "Warning!" -ForegroundColor DarkRed 96 | Write-Host "Please note that encrypted keys can be decrypted only on this machine, using the same account.`n" 97 | } 98 | 99 | function UnHashCredentials 100 | { 101 | param( 102 | [string] $encryptedKey 103 | ) 104 | 105 | try { 106 | $secureKey = $encryptedKey | ConvertTo-SecureString -ErrorAction Stop 107 | } 108 | catch { 109 | Write-Error "Workspace key: $($_.Exception.Message)" 110 | exit(1) 111 | } 112 | $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureKey) 113 | $plainKey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 114 | $plainKey 115 | } 116 | 117 | function MainScript 118 | { 119 | # Script is executed without any attribute 120 | if(-Not $Hash -AND -Not $UnHash -AND -Not $CredentialsFile) 121 | { 122 | cls 123 | Write-Host "`n`nThis script demonstrates how to hash and unhash passwords. Follow the steps below:" 124 | Write-Host "`n`t 1. Run" -NoNewLine 125 | Write-Host ".\Hash-UnHash.ps1 -CredentialsFile " -NoNewLine -ForegroundColor Green 126 | Write-Host "to generate a JSON file where you can input your username and password. The file, named MyCredentials.json, will be created in a folder called ConfigFiles." 127 | Write-Host "`n`t 2. Open the MyCredentials.json file located in the ConfigFiles folder, and enter a username and password inside the quotes for each attribute." 128 | Write-Host "`n`t 3. Run" -NoNewLine 129 | Write-Host ".\Hash-UnHash.ps1 -Hash " -NoNewLine -ForegroundColor Green 130 | Write-Host "to hash the password stored in the JSON file." 131 | Write-Host "`n`t 4. Run" -NoNewLine 132 | Write-Host ".\Hash-UnHash.ps1 -Unhash " -NoNewLine -ForegroundColor Green 133 | Write-Host "to unhash the password stored in the JSON file.`n`n" 134 | exit 135 | } 136 | 137 | if($CredentialsFile) 138 | { 139 | ConfigFile 140 | exit 141 | } 142 | 143 | if($Hash) 144 | { 145 | HashCredentials 146 | exit 147 | } 148 | 149 | if($UnHash) 150 | { 151 | # Validate if the configuration file exists 152 | ValidateConfigurationFile 153 | $json = Get-Content -Raw -Path $jsonFile 154 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 155 | $HashedPassword = $config.HashedPassword 156 | $UserPassword = $config.MyPassword 157 | if ($HashedPassword -eq "True") 158 | { 159 | $UserPassword = UnHashCredentials $UserPassword 160 | } 161 | 162 | $config.HashedPassword = "False" 163 | $config.MyPassword = $UserPassword 164 | $config | ConvertTo-Json | Out-File $jsonFile 165 | Write-Host "`nYour password inside MyCredentials.json is unhash.`n" 166 | exit 167 | } 168 | } 169 | 170 | MainScript 171 | ``` 172 |

173 | -------------------------------------------------------------------------------- /Samples/MDCA/GetMDCAMatchingFiles.md: -------------------------------------------------------------------------------- 1 | # Script: Solution to identify matching files with a MDCA Policy. 2 | 3 | I developed this script based on a customer requirement, the main objective get a list of all files matching in a MDCA file policy. 4 | 5 | The following functions from the 'Lego' folder were utilized: 6 | - [ScriptVariables](../../Lego/ScriptVariables.md) 7 | - Set-ApiConfiguration 8 | - Build-FilterBodyByPolicyId 9 | - Fetch-FilesFromApi 10 | - Retrieve-AllFiles 11 | - Export-FilesToCsv 12 | 13 | To configure this script, you need to obtain your `` and ``. These can be found in [Microsoft Defender](https://security.microsoft.com) under **Settings** > **MDCA Settings** > **API Tokens**. 14 | 15 |

16 |

17 |

Create a MDCA API token

18 |
19 | 20 | Obtaining the Policy ID is slightly more complex. You'll need to locate the File Policy, then extract the Policy ID directly from the URL, as demonstrated in the image below. 21 | 22 |

23 |

24 |

Identify Policy ID

25 |
26 | 27 |
28 | You can find the complete script here 29 | 30 | ```powershell 31 | # Script to get all files matching in a File Policy under MDCA 32 | 33 | #Function to set global variables 34 | function ScriptVariables 35 | { 36 | $script:apiToken = "" 37 | $script:tenantUrl = "" 38 | $script:policyID = "651daa212e00000347cccc0b1" # Replace with the desired policy Id 39 | } 40 | 41 | # Function to Set API Token and URL 42 | function Set-ApiConfiguration { 43 | param ( 44 | [string]$ApiToken, 45 | [string]$TenantUrl 46 | ) 47 | @{ 48 | "Headers" = @{ 49 | "Authorization" = "Token $ApiToken" 50 | "Content-Type" = "application/json" 51 | } 52 | "ApiUrl" = "$TenantUrl/api/v1/files/" 53 | } 54 | } 55 | 56 | # Function to Build the Filter Body by Policy 57 | function Build-FilterBodyByPolicy { 58 | param ( 59 | [string]$PolicyID, 60 | [int]$Limit = 1000 61 | ) 62 | @{ 63 | "filters" = @{ 64 | "policy" = @{ 65 | "cabinetmatchedrulesequals" = @($PolicyID) 66 | } 67 | } 68 | "limit" = $Limit 69 | } | ConvertTo-Json -Depth 10 70 | } 71 | 72 | # Function to Fetch Files from API 73 | function Fetch-FilesFromApi { 74 | param ( 75 | [string]$ApiUrl, 76 | [hashtable]$Headers, 77 | [string]$Body 78 | ) 79 | $response = Invoke-RestMethod -Uri $ApiUrl -Method Post -Headers $Headers -Body $Body 80 | $response 81 | } 82 | 83 | # Function to Handle Pagination 84 | function Retrieve-AllFiles { 85 | param ( 86 | [string]$ApiUrl, 87 | [hashtable]$Headers, 88 | [string]$Body 89 | ) 90 | $allFiles = @() 91 | do { 92 | $response = Invoke-RestMethod -Uri $ApiUrl -Method Post -Headers $Headers -Body $Body 93 | $allFiles += $response.data 94 | $nextLink = $response."@odata.nextLink" 95 | if ($nextLink) { 96 | $ApiUrl = $nextLink 97 | } 98 | } while ($nextLink) 99 | $allFiles 100 | } 101 | 102 | # Function to Export Data to CSV 103 | function Export-FilesToCsv { 104 | param ( 105 | [array]$Files, 106 | [string]$FilePath 107 | ) 108 | $Files | Export-Csv -Path $FilePath -NoTypeInformation 109 | } 110 | 111 | # Main Script Execution 112 | function Main { 113 | $config = Set-ApiConfiguration -ApiToken $apiToken -TenantUrl $tenantUrl 114 | 115 | # Build Filter for Files by Policy Name 116 | $filterBody = Build-FilterBodyByPolicy -PolicyID $policyID 117 | 118 | # Retrieve All Files 119 | $allFiles = Retrieve-AllFiles -ApiUrl $config.ApiUrl -Headers $config.Headers -Body $filterBody 120 | 121 | # Export to CSV 122 | $outputPath = "FilteredFilesByPolicy.csv" 123 | if ($allFiles) { 124 | Export-FilesToCsv -Files $allFiles -FilePath $outputPath 125 | Write-Host "Export completed. File saved to $outputPath" 126 | } else { 127 | Write-Host "No matching files found for the specified policy." 128 | } 129 | } 130 | 131 | # Set global variables 132 | ScriptVariables 133 | # Run the Main Function 134 | Main 135 | ``` 136 | 137 |
138 | 139 |

140 | -------------------------------------------------------------------------------- /Samples/Purview/MSPurviewDLPCollector.md: -------------------------------------------------------------------------------- 1 | # Script solution to get Microsoft Purview Data Loss Prevention configuration 2 | 3 |

4 |

5 |

Power BI report based on data collected in Logs Analytics

6 |
7 | 8 | I developed this script to simplify the way to delivery a Microsoft Purview Information Protection Assessment. This script permit to collect Sensitivity Labels and Labes Policies, the information is exported automatically in Json format, nevertheless can be exported to CSV or Logs Analytics. 9 | 10 | The following functions from the 'Lego' folder were utilized: 11 | - [CheckIfElevated](/Lego/CheckIfElevated.md) 12 | - [CheckPowerShellVersion](/Lego/CheckPowerShellVersion.md) 13 | - [CheckRequiredModules](/Lego/CheckRequiredModules.md) 14 | 15 | Other modules used on this script will be shared soon, nevertheless, next you can find all the code. 16 | 17 |
18 | You can find the complete script here 19 | 20 | Additional helper functions are available, such as `Help`, which provides guidance on using the complete script. 21 | 22 | ```powershell 23 | <#PSScriptInfo 24 | 25 | .VERSION 2.0.5 26 | 27 | .GUID 883af802-166c-4708-f4d1-352686c02f01 28 | 29 | .AUTHOR 30 | https://www.linkedin.com/in/profesorkaz/; Sebastian Zamorano 31 | 32 | .COMPANYNAME 33 | Microsoft Purview Advanced Rich Reports 34 | 35 | .TAGS 36 | #Microsoft365 #M365 #MPARR #MicrosoftPurview #ActivityExplorer 37 | 38 | .PROJECTURI 39 | https://aka.ms/MPARR-YouTube 40 | 41 | .RELEASENOTES 42 | The MIT License (MIT) 43 | Copyright (c) 2015 Microsoft Corporation 44 | Permission is hereby granted, free of charge, to any person obtaining a copy 45 | of this software and associated documentation files (the "Software"), to deal 46 | in the Software without restriction, including without limitation the rights 47 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 48 | copies of the Software, and to permit persons to whom the Software is 49 | furnished to do so, subject to the following conditions: 50 | The above copyright notice and this permission notice shall be included in all 51 | copies or substantial portions of the Software. 52 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 53 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 54 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 55 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 56 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 57 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 58 | SOFTWARE. 59 | 60 | #> 61 | 62 | <# 63 | 64 | .DESCRIPTION 65 | This script permit to export Information Protection configuration 66 | 67 | #> 68 | 69 | <# 70 | HISTORY 71 | Script : MSPurviewDLPCollector.ps1 72 | Author : S. Zamorano 73 | Version : 2.0.5 74 | Description : Export DLP policies and rules to CSV or Json format. 75 | 17-04-2024 S. Zamorano - Public release 76 | 12-08-2024 S. Zamorano - Version 2 Public release 77 | 16-08-2024 S. Zamorano - Conditions field added to the query 78 | 19-08-2024 S. Zamorano - Added field to identify users scope for policies 79 | 20-08-2024 S. Zamorano - Fix export name 80 | #> 81 | 82 | [CmdletBinding(DefaultParameterSetName = "None")] 83 | param( 84 | [string]$DLPRuleTableName = "MSPurviewDLPRulesDetailed", 85 | [string]$DLPPoliciesTableName = "MSPurviewDLPPoliciesDetailed", 86 | [Parameter()] 87 | [switch]$Help, 88 | [Parameter()] 89 | [switch]$ExportToCsv, 90 | [Parameter()] 91 | [switch]$ExportToLogsAnalytics, 92 | [Parameter()] 93 | [switch]$OnlyRules, 94 | [Parameter()] 95 | [switch]$OnlyPolicies 96 | ) 97 | 98 | function CheckPowerShellVersion 99 | { 100 | # Check PowerShell version 101 | Write-Host "`nChecking PowerShell version... " -NoNewline 102 | if ($Host.Version.Major -gt 5) 103 | { 104 | Write-Host "`t`t`t`tPassed!" -ForegroundColor Green 105 | } 106 | else 107 | { 108 | Write-Host "Failed" -ForegroundColor Red 109 | Write-Host "`tCurrent version is $($Host.Version). PowerShell version 7 or newer is required." 110 | exit(1) 111 | } 112 | } 113 | 114 | function CheckIfElevated 115 | { 116 | $IsElevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 117 | if (!$IsElevated) 118 | { 119 | Write-Host "`nPlease start PowerShell as Administrator.`n" -ForegroundColor Yellow 120 | exit(1) 121 | } 122 | } 123 | 124 | function CheckRequiredModules 125 | { 126 | # Check PowerShell modules 127 | Write-Host "Checking PowerShell modules..." 128 | $requiredModules = @( 129 | @{Name="ExchangeOnlineManagement"; MinVersion="0.0"} 130 | ) 131 | 132 | $modulesToInstall = @() 133 | foreach ($module in $requiredModules) 134 | { 135 | Write-Host "`t$($module.Name) - " -NoNewline 136 | $installedVersions = Get-Module -ListAvailable $module.Name 137 | if ($installedVersions) 138 | { 139 | if ($installedVersions[0].Version -lt [version]$module.MinVersion) 140 | { 141 | Write-Host "`t`t`tNew version required" -ForegroundColor Red 142 | $modulesToInstall += $module.Name 143 | } 144 | else 145 | { 146 | Write-Host "`t`t`tInstalled" -ForegroundColor Green 147 | } 148 | } 149 | else 150 | { 151 | Write-Host "`t`t`tNot installed" -ForegroundColor Red 152 | $modulesToInstall += $module.Name 153 | } 154 | } 155 | 156 | if ($modulesToInstall.Count -gt 0) 157 | { 158 | CheckIfElevated 159 | $choices = '&Yes', '&No' 160 | 161 | $decision = $Host.UI.PromptForChoice("", "Misisng required modules. Proceed with installation?", $choices, 0) 162 | if ($decision -eq 0) 163 | { 164 | Write-Host "Installing modules..." 165 | foreach ($module in $modulesToInstall) 166 | { 167 | Write-Host "`t$module" 168 | Install-Module $module -ErrorAction Stop 169 | 170 | } 171 | Write-Host "`nModules installed. Please start the script again." 172 | exit(0) 173 | } 174 | else 175 | { 176 | Write-Host "`nExiting setup. Please install required modules and re-run the setup." 177 | exit(1) 178 | } 179 | } 180 | } 181 | 182 | function CheckPrerequisites 183 | { 184 | CheckPowerShellVersion 185 | CheckRequiredModules 186 | } 187 | 188 | function connect2service 189 | { 190 | Write-Host "`nAuthentication is required, please check your browser" -ForegroundColor DarkYellow 191 | Connect-IPPSSession -UseRPSSession:$false -ShowBanner:$false 192 | } 193 | 194 | function DecryptSharedKey 195 | { 196 | param( 197 | [string] $encryptedKey 198 | ) 199 | 200 | try { 201 | $secureKey = $encryptedKey | ConvertTo-SecureString -ErrorAction Stop 202 | } 203 | catch { 204 | Write-Error "Workspace key: $($_.Exception.Message)" 205 | exit(1) 206 | } 207 | $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureKey) 208 | $plainKey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 209 | $plainKey 210 | } 211 | 212 | function Build-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) 213 | { 214 | # --------------------------------------------------------------- 215 | # Name : Build-Signature 216 | # Value : Creates the authorization signature used in the REST API call to Log Analytics 217 | # --------------------------------------------------------------- 218 | 219 | #Original function to Logs Analytics 220 | $xHeaders = "x-ms-date:" + $date 221 | $stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource 222 | 223 | $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash) 224 | $keyBytes = [Convert]::FromBase64String($sharedKey) 225 | 226 | $sha256 = New-Object System.Security.Cryptography.HMACSHA256 227 | $sha256.Key = $keyBytes 228 | $calculatedHash = $sha256.ComputeHash($bytesToHash) 229 | $encodedHash = [Convert]::ToBase64String($calculatedHash) 230 | $authorization = 'SharedKey {0}:{1}' -f $customerId,$encodedHash 231 | return $authorization 232 | } 233 | 234 | function WriteToLogsAnalytics($body, $LogAnalyticsTableName) 235 | { 236 | # --------------------------------------------------------------- 237 | # Name : Post-LogAnalyticsData 238 | # Value : Writes the data to Log Analytics using a REST API 239 | # Input : 1) PSObject with the data 240 | # 2) Table name in Log Analytics 241 | # Return : None 242 | # --------------------------------------------------------------- 243 | 244 | #Read configuration file 245 | $CONFIGFILE = "$PSScriptRoot\ConfigFiles\MSPurviewDLPConfiguration.json" 246 | $json = Get-Content -Raw -Path $CONFIGFILE 247 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 248 | 249 | $EncryptedKeys = $config.EncryptedKeys 250 | $WLA_CustomerID = $config.Workspace_ID 251 | $WLA_SharedKey = $config.WorkspacePrimaryKey 252 | if ($EncryptedKeys -eq "True") 253 | { 254 | $WLA_SharedKey = DecryptSharedKey $WLA_SharedKey 255 | } 256 | 257 | # Your Log Analytics workspace ID 258 | $LogAnalyticsWorkspaceId = $WLA_CustomerID 259 | 260 | # Use either the primary or the secondary Connected Sources client authentication key 261 | $LogAnalyticsPrimaryKey = $WLA_SharedKey 262 | 263 | #Step 0: sanity checks 264 | if($body -isnot [array]) {return} 265 | if($body.Count -eq 0) {return} 266 | 267 | #Step 1: convert the body.ResultData to JSON 268 | $json_array = @() 269 | $parse_array = @() 270 | $parse_array = $body #| ConvertFrom-Json 271 | foreach($item in $parse_array) 272 | { 273 | $json_array += $item 274 | } 275 | $json = $json_array | ConvertTo-Json -Depth 12 276 | 277 | #Step 2: convert the PSObject to JSON 278 | $bodyJson = $json 279 | #Step 2.5: sanity checks 280 | if($bodyJson.Count -eq 0) {return} 281 | 282 | #Step 3: get the UTF8 bytestream for the JSON 283 | $bodyJsonUTF8 = ([System.Text.Encoding]::UTF8.GetBytes($bodyJson)) 284 | 285 | #Step 4: build the signature 286 | $method = "POST" 287 | $contentType = "application/json" 288 | $resource = "/api/logs" 289 | $rfc1123date = [DateTime]::UtcNow.ToString("r") 290 | $contentLength = $bodyJsonUTF8.Length 291 | $signature = Build-Signature -customerId $LogAnalyticsWorkspaceId -sharedKey $LogAnalyticsPrimaryKey -date $rfc1123date -contentLength $contentLength -method $method -contentType $contentType -resource $resource 292 | 293 | #Step 5: create the header 294 | $headers = @{ 295 | "Authorization" = $signature; 296 | "Log-Type" = $LogAnalyticsTableName; 297 | "x-ms-date" = $rfc1123date; 298 | }; 299 | 300 | #Step 6: REST API call 301 | $uri = 'https://' + $LogAnalyticsWorkspaceId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01" 302 | $response = Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -ContentType $contentType -Body $bodyJsonUTF8 -UseBasicParsing 303 | 304 | if ($Response.StatusCode -eq 200) { 305 | $rows = $bodyJsonUTF8.Count 306 | Write-Information -MessageData "$rows rows written to Log Analytics workspace $uri" -InformationAction Continue 307 | } 308 | } 309 | 310 | function WriteToJson($results, $ExportFolder, $QueryType, $date) 311 | { 312 | $json_array = @() 313 | $parse_array = @() 314 | $parse_array = $results 315 | foreach($item in $parse_array) 316 | { 317 | $json_array += $item 318 | } 319 | $json = $json_array | ConvertTo-Json -Depth 6 320 | $FileName = "Microsoft Purview DLP export - "+"$QueryType"+" - "+"$date"+".Json" 321 | $pathJson = $PSScriptRoot+"\"+$ExportFolder+"\"+$FileName 322 | $path = $pathJson 323 | $json | Add-Content -Path $path 324 | Write-Host "`nData exported to... :" -NoNewLine 325 | Write-Host $pathJson -ForeGroundColor Cyan 326 | Write-Host "`n----------------------------------------------------------------------------------------`n`n" -ForeGroundColor DarkBlue 327 | } 328 | 329 | function WriteToCsv($results, $ExportFolder, $QueryType, $date) 330 | { 331 | $parse_array = @() 332 | $nextpages_array = @() 333 | $TotalResults = @() 334 | $TotalResults = $results 335 | foreach($item in $TotalResults) 336 | { 337 | $FileName = "Microsoft Purview DLP export - "+"$QueryType"+" - "+"$date"+".Csv" 338 | $pathCsv = $PSScriptRoot+"\"+$ExportFolder+"\"+$FileName 339 | $path = $pathCsv 340 | $parse_array = $item 341 | $values = $parse_array[0].psobject.properties.name 342 | $parse_array | Export-Csv -Path $path -NTI -Force -Append | Out-Null 343 | } 344 | Write-Host "Total results $($results.count)" 345 | Write-Host "`nData exported to..." -NoNewline 346 | Write-Host "`n$pathCsv" -ForeGroundColor Cyan 347 | Write-Host "`n----------------------------------------------------------------------------------------`n`n" -ForeGroundColor DarkBlue 348 | } 349 | 350 | function MSPuviewIPCollectorHelp 351 | { 352 | cls 353 | Write-Host "`n" 354 | Write-Host "################################################################################" -ForegroundColor Green 355 | Write-Host "`n How to use this script `n" -ForegroundColor Green 356 | Write-Host "################################################################################" -ForegroundColor Green 357 | Write-Host "`nDescription: " -ForegroundColor Blue -NoNewLine 358 | Write-Host "This menu" 359 | Write-Host ".\MSPurviewDLPCollector.ps1 -Help" -ForeGroundColor DarkYellow 360 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 361 | Write-Host "Using only the script by default, you'll be able to get your DLP Rules and Policies in Json format." 362 | Write-Host ".\MSPurviewDLPCollector.ps1" -ForeGroundColor DarkYellow 363 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 364 | Write-Host "Using the attribute '-OnlyRules' you will be able only to export DLP information" 365 | Write-Host ".\MSPurviewDLPCollector.ps1 -OnlyRules" -ForeGroundColor DarkYellow 366 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 367 | Write-Host "Using the attribute '-OnlyPolicies' you will be able only to export DLP Policies information" 368 | Write-Host ".\MSPurviewDLPCollector.ps1 -OnlyPolicies" -ForeGroundColor DarkYellow 369 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 370 | Write-Host "Using the attribute '-ExportToLogsAnalytics' you will be able only to export all the data to a Logs Analytics workspace" 371 | Write-Host ".\MSPurviewDLPCollector.ps1 -ExportToLogsAnalytics" -ForeGroundColor DarkYellow 372 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 373 | Write-Host "If you are not comfortable working with JSON format, you can use the attribute '-ExportToCsv' to export the data in CSV format." 374 | Write-Host ".\MSPurviewDLPCollector.ps1 -ExportToCsv" -ForeGroundColor DarkYellow 375 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 376 | Write-Host "You can combine different attributes available in the script to customize its functionality. For example:" 377 | Write-Host ".\MSPurviewDLPCollector.ps1 -OnlyRules -ExportToLogsAnalytics" -ForeGroundColor DarkYellow 378 | Write-Host "`n" 379 | Write-Host "### You can now proceed using any of the options listed in the Help menu. ###" -ForegroundColor Green 380 | Write-Host "`n" 381 | return 382 | } 383 | 384 | function GetDataLossPreventionData($ExportFormat, $ExportFolder, $ExportOption) 385 | { 386 | Write-Host "`nExecuting Get cmdlet for your selection..." -ForeGroundColor Blue 387 | 388 | $date = (Get-Date).ToString("yyyy-MM-dd HHmm") 389 | $ExportExtension = $ExportFormat 390 | if($ExportFormat -eq "LA") 391 | { 392 | $ExportExtension="Json" 393 | } 394 | if($ExportOption -eq "All") 395 | { 396 | #Request DLP Rules 397 | $results = New-Object PSObject 398 | $TotalResults = @() 399 | $Query = "DLPRules" 400 | $results = Get-DlpComplianceRule 401 | $TotalResults += $results 402 | if($results.TotalResultCount -eq "0") 403 | { 404 | Write-Host "The previous combination does not return any values." 405 | Write-Host "Exiting...`n" 406 | }else 407 | { 408 | Write-Host "`nCollecting data..." -ForegroundColor DarkBlue -NoNewLine 409 | Write-Host $TotalResults.Count -ForegroundColor Blue -NoNewLine 410 | Write-Host " records returned" 411 | #Run the below steps in loop until all results are fetched 412 | 413 | if($ExportFormat -eq "Csv") 414 | { 415 | $CSVresults = $TotalResults 416 | WriteToCsv -results $CSVresults -ExportFolder $ExportFolder -QueryType $Query -date $date 417 | }elseif($ExportFormat -eq "LA") 418 | { 419 | WriteToLogsAnalytics -LogAnalyticsTableName $DLPRuleTableName -body $TotalResults 420 | }else 421 | { 422 | WriteToJson -results $TotalResults -ExportFolder $ExportFolder -QueryType $Query -date $date 423 | } 424 | } 425 | #Request DLP policies 426 | $results = New-Object PSObject 427 | $TotalResults = @() 428 | $Query = "DLPPolicies" 429 | $results = Get-DlpCompliancePolicy 430 | $TotalResults += $results 431 | if($results.TotalResultCount -eq "0") 432 | { 433 | Write-Host "The previous combination does not return any values." 434 | Write-Host "Exiting...`n" 435 | }else 436 | { 437 | Write-Host "`nCollecting data..." -ForegroundColor DarkBlue -NoNewLine 438 | Write-Host $TotalResults.Count -ForegroundColor Blue -NoNewLine 439 | Write-Host " records returned" 440 | #Run the below steps in loop until all results are fetched 441 | 442 | if($ExportFormat -eq "Csv") 443 | { 444 | $CSVresults = $TotalResults 445 | WriteToCsv -results $CSVresults -ExportFolder $ExportFolder -QueryType $Query -date $date 446 | }elseif($ExportFormat -eq "LA") 447 | { 448 | WriteToLogsAnalytics -LogAnalyticsTableName $DLPPoliciesTableName -body $TotalResults 449 | }else 450 | { 451 | WriteToJson -results $TotalResults -ExportFolder $ExportFolder -QueryType $Query -date $date 452 | } 453 | } 454 | }elseif($ExportOption -eq "OnlyRules") 455 | { 456 | $results = New-Object PSObject 457 | $TotalResults = @() 458 | $Query = "DLPRules" 459 | $results = Get-DlpComplianceRule 460 | $TotalResults += $results 461 | if($results.TotalResultCount -eq "0") 462 | { 463 | Write-Host "The previous combination does not return any values." 464 | Write-Host "Exiting...`n" 465 | }else 466 | { 467 | Write-Host "`nCollecting data..." -ForegroundColor DarkBlue -NoNewLine 468 | Write-Host $TotalResults.count -ForegroundColor Blue -NoNewLine 469 | Write-Host " records returned" 470 | #Run the below steps in loop until all results are fetched 471 | 472 | if($ExportFormat -eq "Csv") 473 | { 474 | $CSVresults = $TotalResults 475 | WriteToCsv -results $CSVresults -ExportFolder $ExportFolder -QueryType $Query -date $date 476 | }elseif($ExportFormat -eq "LA") 477 | { 478 | WriteToLogsAnalytics -LogAnalyticsTableName $DLPRuleTableName -body $TotalResults 479 | }else 480 | { 481 | WriteToJson -results $TotalResults -ExportFolder $ExportFolder -QueryType $Query -date $date 482 | } 483 | } 484 | }elseif($ExportOption -eq "OnlyPolicies") 485 | { 486 | $results = New-Object PSObject 487 | $TotalResults = @() 488 | $Query = "DLPPolicies" 489 | $results = Get-DlpCompliancePolicy 490 | $TotalResults += $results 491 | if($results.TotalResultCount -eq "0") 492 | { 493 | Write-Host "The previous combination does not return any values." 494 | Write-Host "Exiting...`n" 495 | }else 496 | { 497 | Write-Host "`nCollecting data..." -ForegroundColor DarkBlue -NoNewLine 498 | Write-Host $results.TotalResultCount -ForegroundColor Blue -NoNewLine 499 | Write-Host " records returned" 500 | #Run the below steps in loop until all results are fetched 501 | 502 | if($ExportFormat -eq "Csv") 503 | { 504 | $CSVresults = $TotalResults 505 | WriteToCsv -results $CSVresults -ExportFolder $ExportFolder -QueryType $Query -date $date 506 | }elseif($ExportFormat -eq "LA") 507 | { 508 | WriteToLogsAnalytics -LogAnalyticsTableName $DLPPoliciesTableName -body $TotalResults 509 | }else 510 | { 511 | WriteToJson -results $TotalRFesults -ExportFolder $ExportFolder -QueryType $Query -date $date 512 | } 513 | } 514 | } 515 | } 516 | 517 | function MainFunction 518 | { 519 | #Welcome header 520 | cls 521 | Clear-Host 522 | 523 | Write-Host "`n`n----------------------------------------------------------------------------------------" 524 | Write-Host "`nWelcome to Data Loss Prevention Export script!" -ForegroundColor Green 525 | Write-Host "This script will permit to collect data from DLP Rules and Policies related" 526 | Write-Host "`n----------------------------------------------------------------------------------------" 527 | 528 | 529 | #Initiate variables 530 | 531 | $ExportOption = "All" 532 | 533 | ##List only DLP 534 | if($OnlyRules) 535 | { 536 | $ExportOption = "OnlyRules" 537 | } 538 | if($OnlyPolicies) 539 | { 540 | $ExportOption = "OnlyPolicies" 541 | } 542 | 543 | ##Export format 544 | $ExportFormat = "Json" 545 | if($ExportToCsv) 546 | { 547 | $ExportFormat = "Csv" 548 | } 549 | if($ExportToLogsAnalytics) 550 | { 551 | $ExportFormat = "LA" 552 | $LogsAnalyticsConfigurationFile = "$PSScriptRoot\ConfigFiles\MSPurviewDLPConfiguration.json" 553 | if(-not (Test-Path -Path $LogsAnalyticsConfigurationFile)) 554 | { 555 | Write-Host "`nConfiguration file is not present" -ForegroundColor DarkYellow 556 | Write-Host "Please download the configuration file from http://activityexplorer.kaznets.com and save inside of the ConfigFiles folder.`n" 557 | Write-Host "Press any key to continue..." 558 | $key = ([System.Console]::ReadKey($true)) 559 | exit 560 | } 561 | } 562 | 563 | ##Export folder Name 564 | $ExportFolderName = "ExportedData" 565 | $ExportPath = "$PSScriptRoot\$ExportFolderName" 566 | if(-Not (Test-Path $ExportPath)) 567 | { 568 | New-Item -ItemType Directory -Force -Path "$PSScriptRoot\$ExportFolderName" | Out-Null 569 | $StatusFolder = "Created" 570 | }else 571 | { 572 | $StatusFolder = "Available" 573 | } 574 | 575 | ##Show variables set 576 | Write-Host "Export format set to`t`t`t:" -NoNewline 577 | Write-Host "`t$ExportFormat" -ForegroundColor Green 578 | Write-Host "Export folder set to`t`t`t:" -NoNewline 579 | Write-Host "`t$ExportFolderName ($StatusFolder)" -ForegroundColor Green 580 | Write-Host "Export Option selected`t`t`t:" -NoNewline 581 | Write-Host "`t$ExportOption" -ForegroundColor Green 582 | if($ExportToLogsAnalytics) 583 | { 584 | if($OnlyRules) 585 | { 586 | Write-Host "Table name for DLP Rules`t:" -NoNewline 587 | Write-Host "`t$DLPRuleTableName" -ForegroundColor Green 588 | }elseif($OnlyPolicies) 589 | { 590 | Write-Host "Table name for Policies DLP`t`t:" -NoNewline 591 | Write-Host "`t$DLPPoliciesTableName" -ForegroundColor Green 592 | }else 593 | { 594 | Write-Host "Table name for DLP Rules`t:" -NoNewline 595 | Write-Host "`t$DLPRuleTableName" -ForegroundColor Green 596 | Write-Host "Table name for Policies DLP`t`t:" -NoNewline 597 | Write-Host "`t$DLPPoliciesTableName" -ForegroundColor Green 598 | } 599 | } 600 | Write-Host "`n`nYou will be prompted for your credentials, remember that you need Compliance Administrator role" 601 | Write-Host "Press any key to continue..." 602 | $key = ([System.Console]::ReadKey($true)) 603 | connect2service 604 | 605 | Write-Host "Calling script..." 606 | 607 | #Call function to export data from Activity Explorer 608 | GetDataLossPreventionData -ExportFormat $ExportFormat -ExportFolder $ExportFolderName -ExportOption $ExportOption 609 | } 610 | 611 | if($Help) 612 | { 613 | MSPuviewIPCollectorHelp 614 | exit 615 | } 616 | 617 | CheckPrerequisites 618 | MainFunction 619 | ``` 620 | 621 |
622 |

623 | -------------------------------------------------------------------------------- /Samples/Purview/MSPurviewIPCollector.md: -------------------------------------------------------------------------------- 1 | # Script solution to get Microsoft Purview Information Protection configuration 2 | 3 |

4 |

5 |

Power BI report based on data collected in Logs Analytics

6 |
7 | 8 | I developed this script to simplify the way to delivery a Microsoft Purview Information Protection Assessment. This script permit to collect Sensitivity Labels and Labes Policies, the information is exported automatically in Json format, nevertheless can be exported to CSV or Logs Analytics. 9 | 10 | The following functions from the 'Lego' folder were utilized: 11 | - [CheckIfElevated](/Lego/CheckIfElevated.md) 12 | - [CheckPowerShellVersion](/Lego/CheckPowerShellVersion.md) 13 | - [CheckRequiredModules](/Lego/CheckRequiredModules.md) 14 | 15 | Other modules used on this script will be shared soon, nevertheless, next you can find all the code. 16 | 17 |
18 | You can find the complete script here 19 | 20 | Additional helper functions are available, such as `Help`, which provides guidance on using the complete script. 21 | 22 | ```powershell 23 | <#PSScriptInfo 24 | 25 | .VERSION 2.0.5 26 | 27 | .GUID 883af802-166c-4708-f4d1-352686c02f01 28 | 29 | .AUTHOR 30 | https://www.linkedin.com/in/profesorkaz/; Sebastian Zamorano 31 | 32 | .COMPANYNAME 33 | Microsoft Purview Advanced Rich Reports 34 | 35 | .TAGS 36 | #Microsoft365 #M365 #MPARR #MicrosoftPurview #ActivityExplorer 37 | 38 | .PROJECTURI 39 | https://aka.ms/MPARR-YouTube 40 | 41 | .RELEASENOTES 42 | The MIT License (MIT) 43 | Copyright (c) 2015 Microsoft Corporation 44 | Permission is hereby granted, free of charge, to any person obtaining a copy 45 | of this software and associated documentation files (the "Software"), to deal 46 | in the Software without restriction, including without limitation the rights 47 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 48 | copies of the Software, and to permit persons to whom the Software is 49 | furnished to do so, subject to the following conditions: 50 | The above copyright notice and this permission notice shall be included in all 51 | copies or substantial portions of the Software. 52 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 53 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 54 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 55 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 56 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 57 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 58 | SOFTWARE. 59 | 60 | #> 61 | 62 | <# 63 | 64 | .DESCRIPTION 65 | This script permit to export Information Protection configuration 66 | 67 | #> 68 | 69 | <# 70 | HISTORY 71 | Script : MSPurviewIPCollector.ps1 72 | Author : S. Zamorano 73 | Version : 2.0.5 74 | Description : Export Activity Explorer activities to CSV or Json format. 75 | 17-04-2024 S. Zamorano - Public release 76 | 12-08-2024 S. Zamorano - Version 2 Public release 77 | 16-08-2024 S. Zamorano - Conditions field added to the query 78 | 19-08-2024 S. Zamorano - Added field to identify users scope for policies 79 | 20-08-2024 S. Zamorano - Fix export name 80 | #> 81 | 82 | [CmdletBinding(DefaultParameterSetName = "None")] 83 | param( 84 | [string]$SensitivityLabelTableName = "MSPurviewIPSensitivityLabelsDetailed", 85 | [string]$PoliciesLabelTableName = "MSPurviewIPPoliciesDetailed", 86 | [Parameter()] 87 | [switch]$Help, 88 | [Parameter()] 89 | [switch]$ExportToCsv, 90 | [Parameter()] 91 | [switch]$ExportToLogsAnalytics, 92 | [Parameter()] 93 | [switch]$OnlyLabels, 94 | [Parameter()] 95 | [switch]$OnlyPolicies 96 | ) 97 | 98 | function CheckPowerShellVersion 99 | { 100 | # Check PowerShell version 101 | Write-Host "`nChecking PowerShell version... " -NoNewline 102 | if ($Host.Version.Major -gt 5) 103 | { 104 | Write-Host "`t`t`t`tPassed!" -ForegroundColor Green 105 | } 106 | else 107 | { 108 | Write-Host "Failed" -ForegroundColor Red 109 | Write-Host "`tCurrent version is $($Host.Version). PowerShell version 7 or newer is required." 110 | exit(1) 111 | } 112 | } 113 | 114 | function CheckIfElevated 115 | { 116 | $IsElevated = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) 117 | if (!$IsElevated) 118 | { 119 | Write-Host "`nPlease start PowerShell as Administrator.`n" -ForegroundColor Yellow 120 | exit(1) 121 | } 122 | } 123 | 124 | function CheckRequiredModules 125 | { 126 | # Check PowerShell modules 127 | Write-Host "Checking PowerShell modules..." 128 | $requiredModules = @( 129 | @{Name="ExchangeOnlineManagement"; MinVersion="0.0"} 130 | ) 131 | 132 | $modulesToInstall = @() 133 | foreach ($module in $requiredModules) 134 | { 135 | Write-Host "`t$($module.Name) - " -NoNewline 136 | $installedVersions = Get-Module -ListAvailable $module.Name 137 | if ($installedVersions) 138 | { 139 | if ($installedVersions[0].Version -lt [version]$module.MinVersion) 140 | { 141 | Write-Host "`t`t`tNew version required" -ForegroundColor Red 142 | $modulesToInstall += $module.Name 143 | } 144 | else 145 | { 146 | Write-Host "`t`t`tInstalled" -ForegroundColor Green 147 | } 148 | } 149 | else 150 | { 151 | Write-Host "`t`t`tNot installed" -ForegroundColor Red 152 | $modulesToInstall += $module.Name 153 | } 154 | } 155 | 156 | if ($modulesToInstall.Count -gt 0) 157 | { 158 | CheckIfElevated 159 | $choices = '&Yes', '&No' 160 | 161 | $decision = $Host.UI.PromptForChoice("", "Misisng required modules. Proceed with installation?", $choices, 0) 162 | if ($decision -eq 0) 163 | { 164 | Write-Host "Installing modules..." 165 | foreach ($module in $modulesToInstall) 166 | { 167 | Write-Host "`t$module" 168 | Install-Module $module -ErrorAction Stop 169 | 170 | } 171 | Write-Host "`nModules installed. Please start the script again." 172 | exit(0) 173 | } 174 | else 175 | { 176 | Write-Host "`nExiting setup. Please install required modules and re-run the setup." 177 | exit(1) 178 | } 179 | } 180 | } 181 | 182 | function CheckPrerequisites 183 | { 184 | CheckPowerShellVersion 185 | CheckRequiredModules 186 | } 187 | 188 | function connect2service 189 | { 190 | Write-Host "`nAuthentication is required, please check your browser" -ForegroundColor DarkYellow 191 | Connect-IPPSSession -UseRPSSession:$false -ShowBanner:$false 192 | } 193 | 194 | function DecryptSharedKey 195 | { 196 | param( 197 | [string] $encryptedKey 198 | ) 199 | 200 | try { 201 | $secureKey = $encryptedKey | ConvertTo-SecureString -ErrorAction Stop 202 | } 203 | catch { 204 | Write-Error "Workspace key: $($_.Exception.Message)" 205 | exit(1) 206 | } 207 | $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureKey) 208 | $plainKey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) 209 | $plainKey 210 | } 211 | 212 | function Build-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) 213 | { 214 | # --------------------------------------------------------------- 215 | # Name : Build-Signature 216 | # Value : Creates the authorization signature used in the REST API call to Log Analytics 217 | # --------------------------------------------------------------- 218 | 219 | #Original function to Logs Analytics 220 | $xHeaders = "x-ms-date:" + $date 221 | $stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource 222 | 223 | $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash) 224 | $keyBytes = [Convert]::FromBase64String($sharedKey) 225 | 226 | $sha256 = New-Object System.Security.Cryptography.HMACSHA256 227 | $sha256.Key = $keyBytes 228 | $calculatedHash = $sha256.ComputeHash($bytesToHash) 229 | $encodedHash = [Convert]::ToBase64String($calculatedHash) 230 | $authorization = 'SharedKey {0}:{1}' -f $customerId,$encodedHash 231 | return $authorization 232 | } 233 | 234 | function WriteToLogsAnalytics($body, $LogAnalyticsTableName) 235 | { 236 | # --------------------------------------------------------------- 237 | # Name : Post-LogAnalyticsData 238 | # Value : Writes the data to Log Analytics using a REST API 239 | # Input : 1) PSObject with the data 240 | # 2) Table name in Log Analytics 241 | # Return : None 242 | # --------------------------------------------------------------- 243 | 244 | #Read configuration file 245 | $CONFIGFILE = "$PSScriptRoot\ConfigFiles\MSPurviewIPConfiguration.json" 246 | $json = Get-Content -Raw -Path $CONFIGFILE 247 | [PSCustomObject]$config = ConvertFrom-Json -InputObject $json 248 | 249 | $EncryptedKeys = $config.EncryptedKeys 250 | $WLA_CustomerID = $config.Workspace_ID 251 | $WLA_SharedKey = $config.WorkspacePrimaryKey 252 | if ($EncryptedKeys -eq "True") 253 | { 254 | $WLA_SharedKey = DecryptSharedKey $WLA_SharedKey 255 | } 256 | 257 | # Your Log Analytics workspace ID 258 | $LogAnalyticsWorkspaceId = $WLA_CustomerID 259 | 260 | # Use either the primary or the secondary Connected Sources client authentication key 261 | $LogAnalyticsPrimaryKey = $WLA_SharedKey 262 | 263 | #Step 0: sanity checks 264 | if($body -isnot [array]) {return} 265 | if($body.Count -eq 0) {return} 266 | 267 | #Step 1: convert the body.ResultData to JSON 268 | $json_array = @() 269 | $parse_array = @() 270 | $parse_array = $body #| ConvertFrom-Json 271 | foreach($item in $parse_array) 272 | { 273 | $json_array += $item 274 | } 275 | $json = $json_array | ConvertTo-Json -Depth 6 276 | 277 | #Step 2: convert the PSObject to JSON 278 | $bodyJson = $json 279 | #Step 2.5: sanity checks 280 | if($bodyJson.Count -eq 0) {return} 281 | 282 | #Step 3: get the UTF8 bytestream for the JSON 283 | $bodyJsonUTF8 = ([System.Text.Encoding]::UTF8.GetBytes($bodyJson)) 284 | 285 | #Step 4: build the signature 286 | $method = "POST" 287 | $contentType = "application/json" 288 | $resource = "/api/logs" 289 | $rfc1123date = [DateTime]::UtcNow.ToString("r") 290 | $contentLength = $bodyJsonUTF8.Length 291 | $signature = Build-Signature -customerId $LogAnalyticsWorkspaceId -sharedKey $LogAnalyticsPrimaryKey -date $rfc1123date -contentLength $contentLength -method $method -contentType $contentType -resource $resource 292 | 293 | #Step 5: create the header 294 | $headers = @{ 295 | "Authorization" = $signature; 296 | "Log-Type" = $LogAnalyticsTableName; 297 | "x-ms-date" = $rfc1123date; 298 | }; 299 | 300 | #Step 6: REST API call 301 | $uri = 'https://' + $LogAnalyticsWorkspaceId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01" 302 | $response = Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -ContentType $contentType -Body $bodyJsonUTF8 -UseBasicParsing 303 | 304 | if ($Response.StatusCode -eq 200) { 305 | $rows = $bodyJson.Count 306 | Write-Information -MessageData "$rows rows written to Log Analytics workspace $uri" -InformationAction Continue 307 | } 308 | } 309 | 310 | function WriteToJson($results, $ExportFolder, $QueryType, $date) 311 | { 312 | $json_array = @() 313 | $parse_array = @() 314 | $parse_array = $results 315 | foreach($item in $parse_array) 316 | { 317 | $json_array += $item 318 | } 319 | $json = $json_array | ConvertTo-Json -Depth 6 320 | $FileName = "Microsoft Purview IP export - "+"$QueryType"+" - "+"$date"+".Json" 321 | $pathJson = $PSScriptRoot+"\"+$ExportFolder+"\"+$FileName 322 | $path = $pathJson 323 | $json | Add-Content -Path $path 324 | Write-Host "`nData exported to... :" -NoNewLine 325 | Write-Host $pathJson -ForeGroundColor Cyan 326 | Write-Host "`n----------------------------------------------------------------------------------------`n`n" -ForeGroundColor DarkBlue 327 | } 328 | 329 | function WriteToCsv($results, $ExportFolder, $QueryType, $date) 330 | { 331 | $parse_array = @() 332 | $nextpages_array = @() 333 | $TotalResults = @() 334 | $TotalResults = $results 335 | foreach($item in $TotalResults) 336 | { 337 | $FileName = "Microsoft Purview IP export - "+"$QueryType"+" - "+"$date"+".Csv" 338 | $pathCsv = $PSScriptRoot+"\"+$ExportFolder+"\"+$FileName 339 | $path = $pathCsv 340 | $parse_array = $item 341 | $values = $parse_array[0].psobject.properties.name 342 | $parse_array | Export-Csv -Path $path -NTI -Force -Append | Out-Null 343 | } 344 | Write-Host "Total results $($results.count)" 345 | Write-Host "`nData exported to..." -NoNewline 346 | Write-Host "`n$pathCsv" -ForeGroundColor Cyan 347 | Write-Host "`n----------------------------------------------------------------------------------------`n`n" -ForeGroundColor DarkBlue 348 | } 349 | 350 | function MSPuviewIPCollectorHelp 351 | { 352 | cls 353 | Write-Host "`n" 354 | Write-Host "################################################################################" -ForegroundColor Green 355 | Write-Host "`n How to use this script `n" -ForegroundColor Green 356 | Write-Host "################################################################################" -ForegroundColor Green 357 | Write-Host "`nDescription: " -ForegroundColor Blue -NoNewLine 358 | Write-Host "This menu" 359 | Write-Host ".\MSPurviewIPCollector.ps1 -Help" -ForeGroundColor DarkYellow 360 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 361 | Write-Host "Using only the script by default, you'll be able to get your Sensitivity Labels and Policies in Json format." 362 | Write-Host ".\MSPurviewIPCollector.ps1" -ForeGroundColor DarkYellow 363 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 364 | Write-Host "Using the attribute '-OnlyLabels' you will be able only to export Sensitivity Labels information" 365 | Write-Host ".\MSPurviewIPCollector.ps1 -OnlyLabels" -ForeGroundColor DarkYellow 366 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 367 | Write-Host "Using the attribute '-OnlyPolicies' you will be able only to export Sensitivity Labels Policies information" 368 | Write-Host ".\MSPurviewIPCollector.ps1 -OnlyPolicies" -ForeGroundColor DarkYellow 369 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 370 | Write-Host "Using the attribute '-ExportToLogsAnalytics' you will be able only to export all the data to a Logs Analytics workspace" 371 | Write-Host ".\MSPurviewIPCollector.ps1 -ExportToLogsAnalytics" -ForeGroundColor DarkYellow 372 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 373 | Write-Host "If you are not comfortable working with JSON format, you can use the attribute '-ExportToCsv' to export the data in CSV format." 374 | Write-Host ".\MSPurviewIPCollector.ps1 -ExportToCsv" -ForeGroundColor DarkYellow 375 | Write-Host "`n`nDescription: " -ForegroundColor Blue -NoNewLine 376 | Write-Host "You can combine different attributes available in the script to customize its functionality. For example:" 377 | Write-Host ".\MSPurviewIPCollector.ps1 -OnlyLabels -ExportToLogsAnalytics" -ForeGroundColor DarkYellow 378 | Write-Host "`n" 379 | Write-Host "### You can now proceed using any of the options listed in the Help menu. ###" -ForegroundColor Green 380 | Write-Host "`n" 381 | return 382 | } 383 | 384 | function GetInformationProtectionData($ExportFormat, $ExportFolder, $ExportOption) 385 | { 386 | Write-Host "`nExecuting Get cmdlet for your selection..." -ForeGroundColor Blue 387 | 388 | $date = (Get-Date).ToString("yyyy-MM-dd HHmm") 389 | $ExportExtension = $ExportFormat 390 | if($ExportFormat -eq "LA") 391 | { 392 | $ExportExtension="Json" 393 | } 394 | if($ExportOption -eq "All") 395 | { 396 | #Request Sensitivity Labels 397 | $results = New-Object PSObject 398 | $TotalResults = @() 399 | $Query = "SensitivityLabels" 400 | $results = Get-Label | select DisplayName,Name,Guid,ParentLabelDisplayName,ParentId,IsParent,IsLabelGroup,Tooltip,DefaultContentLabel,ContentType,LocaleSettings,SchematizedDataCondition,ColumnAssetCondition,LabelActions,Settings,Priority,Workload,Policy,CreatedBy,LastModifiedBy,WhenChangedUTC,WhenCreatedUTC,Comment,Conditions 401 | $TotalResults += $results 402 | if($results.TotalResultCount -eq "0") 403 | { 404 | Write-Host "The previous combination does not return any values." 405 | Write-Host "Exiting...`n" 406 | }else 407 | { 408 | Write-Host "`nCollecting data..." -ForegroundColor DarkBlue -NoNewLine 409 | Write-Host $TotalResults.Count -ForegroundColor Blue -NoNewLine 410 | Write-Host " records returned" 411 | #Run the below steps in loop until all results are fetched 412 | 413 | if($ExportFormat -eq "Csv") 414 | { 415 | $CSVresults = $TotalResults 416 | WriteToCsv -results $CSVresults -ExportFolder $ExportFolder -QueryType $Query -date $date 417 | }elseif($ExportFormat -eq "LA") 418 | { 419 | WriteToLogsAnalytics -LogAnalyticsTableName $SensitivityLabelTableName -body $TotalResults 420 | }else 421 | { 422 | WriteToJson -results $TotalResults -ExportFolder $ExportFolder -QueryType $Query -date $date 423 | } 424 | } 425 | #Request Labels policies 426 | $results = New-Object PSObject 427 | $TotalResults = @() 428 | $Query = "LabelsPolicies" 429 | $results = Get-LabelPolicy | select Name,Guid,WhenChangedUTC,WhenCreatedUTC,Enabled,Mode,DistributionStatus,Type,Settings,Labels,ScopedLabels,PolicySettingsBlob,Workload,CreatedBy,LastModifiedBy,ExchangeLocation 430 | $TotalResults += $results 431 | if($results.TotalResultCount -eq "0") 432 | { 433 | Write-Host "The previous combination does not return any values." 434 | Write-Host "Exiting...`n" 435 | }else 436 | { 437 | Write-Host "`nCollecting data..." -ForegroundColor DarkBlue -NoNewLine 438 | Write-Host $TotalResults.Count -ForegroundColor Blue -NoNewLine 439 | Write-Host " records returned" 440 | #Run the below steps in loop until all results are fetched 441 | 442 | if($ExportFormat -eq "Csv") 443 | { 444 | $CSVresults = $TotalResults 445 | WriteToCsv -results $CSVresults -ExportFolder $ExportFolder -QueryType $Query -date $date 446 | }elseif($ExportFormat -eq "LA") 447 | { 448 | WriteToLogsAnalytics -LogAnalyticsTableName $PoliciesLabelTableName -body $TotalResults 449 | }else 450 | { 451 | WriteToJson -results $TotalResults -ExportFolder $ExportFolder -QueryType $Query -date $date 452 | } 453 | } 454 | }elseif($ExportOption -eq "OnlyLabels") 455 | { 456 | $results = New-Object PSObject 457 | $TotalResults = @() 458 | $Query = "SensitivityLabels" 459 | $results = Get-Label | select DisplayName,Name,Guid,ParentLabelDisplayName,ParentId,IsParent,IsLabelGroup,Tooltip,DefaultContentLabel,ContentType,LocaleSettings,SchematizedDataCondition,ColumnAssetCondition,LabelActions,Settings,Priority,Workload,Policy,CreatedBy,LastModifiedBy,WhenChangedUTC,WhenCreatedUTC,Comment,Conditions 460 | $TotalResults += $results 461 | if($results.TotalResultCount -eq "0") 462 | { 463 | Write-Host "The previous combination does not return any values." 464 | Write-Host "Exiting...`n" 465 | }else 466 | { 467 | Write-Host "`nCollecting data..." -ForegroundColor DarkBlue -NoNewLine 468 | Write-Host $TotalResults.count -ForegroundColor Blue -NoNewLine 469 | Write-Host " records returned" 470 | #Run the below steps in loop until all results are fetched 471 | 472 | if($ExportFormat -eq "Csv") 473 | { 474 | $CSVresults = $TotalResults 475 | WriteToCsv -results $CSVresults -ExportFolder $ExportFolder -QueryType $Query -date $date 476 | }elseif($ExportFormat -eq "LA") 477 | { 478 | WriteToLogsAnalytics -LogAnalyticsTableName $SensitivityLabelTableName -body $TotalResults 479 | }else 480 | { 481 | WriteToJson -results $TotalResults -ExportFolder $ExportFolder -QueryType $Query -date $date 482 | } 483 | } 484 | }elseif($ExportOption -eq "OnlyPolicies") 485 | { 486 | $results = New-Object PSObject 487 | $TotalResults = @() 488 | $Query = "LabelsPolicies" 489 | $results = Get-LabelPolicy | select Name,Guid,WhenChangedUTC,WhenCreatedUTC,Enabled,Mode,DistributionStatus,Type,Settings,Labels,ScopedLabels,PolicySettingsBlob,Workload,CreatedBy,LastModifiedBy,ExchangeLocation 490 | $TotalResults += $results 491 | if($results.TotalResultCount -eq "0") 492 | { 493 | Write-Host "The previous combination does not return any values." 494 | Write-Host "Exiting...`n" 495 | }else 496 | { 497 | Write-Host "`nCollecting data..." -ForegroundColor DarkBlue -NoNewLine 498 | Write-Host $results.TotalResultCount -ForegroundColor Blue -NoNewLine 499 | Write-Host " records returned" 500 | #Run the below steps in loop until all results are fetched 501 | 502 | if($ExportFormat -eq "Csv") 503 | { 504 | $CSVresults = $TotalResults 505 | WriteToCsv -results $CSVresults -ExportFolder $ExportFolder -QueryType $Query -date $date 506 | }elseif($ExportFormat -eq "LA") 507 | { 508 | WriteToLogsAnalytics -LogAnalyticsTableName $PoliciesLabelTableName -body $TotalResults 509 | }else 510 | { 511 | WriteToJson -results $TotalResults -ExportFolder $ExportFolder -QueryType $Query -date $date 512 | } 513 | } 514 | } 515 | } 516 | 517 | function MainFunction 518 | { 519 | #Welcome header 520 | cls 521 | Clear-Host 522 | 523 | Write-Host "`n`n----------------------------------------------------------------------------------------" 524 | Write-Host "`nWelcome to Information Protection Export script!" -ForegroundColor Green 525 | Write-Host "This script will permit to collect data from Sensitivity Labels and Policies related" 526 | Write-Host "`n----------------------------------------------------------------------------------------" 527 | 528 | 529 | #Initiate variables 530 | 531 | $ExportOption = "All" 532 | 533 | ##List only Labels 534 | if($OnlyLabels) 535 | { 536 | $ExportOption = "OnlyLabels" 537 | } 538 | if($OnlyPolicies) 539 | { 540 | $ExportOption = "OnlyPolicies" 541 | } 542 | 543 | ##Export format 544 | $ExportFormat = "Json" 545 | if($ExportToCsv) 546 | { 547 | $ExportFormat = "Csv" 548 | } 549 | if($ExportToLogsAnalytics) 550 | { 551 | $ExportFormat = "LA" 552 | $LogsAnalyticsConfigurationFile = "$PSScriptRoot\ConfigFiles\MSPurviewIPConfiguration.json" 553 | if(-not (Test-Path -Path $LogsAnalyticsConfigurationFile)) 554 | { 555 | Write-Host "`nConfiguration file is not present" -ForegroundColor DarkYellow 556 | Write-Host "Please download the configuration file from http://activityexplorer.kaznets.com and save inside of the ConfigFiles folder.`n" 557 | Write-Host "Press any key to continue..." 558 | $key = ([System.Console]::ReadKey($true)) 559 | exit 560 | } 561 | } 562 | 563 | ##Export folder Name 564 | $ExportFolderName = "ExportedData" 565 | $ExportPath = "$PSScriptRoot\$ExportFolderName" 566 | if(-Not (Test-Path $ExportPath)) 567 | { 568 | New-Item -ItemType Directory -Force -Path "$PSScriptRoot\$ExportFolderName" | Out-Null 569 | $StatusFolder = "Created" 570 | }else 571 | { 572 | $StatusFolder = "Available" 573 | } 574 | 575 | ##Show variables set 576 | Write-Host "Export format set to`t`t`t:" -NoNewline 577 | Write-Host "`t$ExportFormat" -ForegroundColor Green 578 | Write-Host "Export folder set to`t`t`t:" -NoNewline 579 | Write-Host "`t$ExportFolderName ($StatusFolder)" -ForegroundColor Green 580 | Write-Host "Export Option selected`t`t`t:" -NoNewline 581 | Write-Host "`t$ExportOption" -ForegroundColor Green 582 | if($ExportToLogsAnalytics) 583 | { 584 | if($OnlyLabels) 585 | { 586 | Write-Host "Table name for Sensitivity Labels`t:" -NoNewline 587 | Write-Host "`t$SensitivityLabelTableName" -ForegroundColor Green 588 | }elseif($OnlyPolicies) 589 | { 590 | Write-Host "Table name for Policies Labels`t`t:" -NoNewline 591 | Write-Host "`t$PoliciesLabelTableName" -ForegroundColor Green 592 | }else 593 | { 594 | Write-Host "Table name for Sensitivity Labels`t:" -NoNewline 595 | Write-Host "`t$SensitivityLabelTableName" -ForegroundColor Green 596 | Write-Host "Table name for Policies Labels`t`t:" -NoNewline 597 | Write-Host "`t$PoliciesLabelTableName" -ForegroundColor Green 598 | } 599 | } 600 | Write-Host "`n`nYou will be prompted for your credentials, remember that you need Compliance Administrator role" 601 | Write-Host "Press any key to continue..." 602 | $key = ([System.Console]::ReadKey($true)) 603 | connect2service 604 | 605 | Write-Host "Calling script..." 606 | 607 | #Call function to export data from Activity Explorer 608 | GetInformationProtectionData -ExportFormat $ExportFormat -ExportFolder $ExportFolderName -ExportOption $ExportOption 609 | } 610 | 611 | if($Help) 612 | { 613 | MSPuviewIPCollectorHelp 614 | exit 615 | } 616 | 617 | CheckPrerequisites 618 | MainFunction 619 | ``` 620 |
621 |

622 | -------------------------------------------------------------------------------- /Samples/Purview/OCR.md: -------------------------------------------------------------------------------- 1 | # Script to use Azure AI services for OCR 2 | 3 | Today, Azure AI services provide powerful OCR capabilities for processing images and PDFs. In this exercise, I am using version 3.2, which supports only image-based OCR. However, version 4, currently in preview, introduces the ability to work directly with PDFs, offering even greater flexibility. 4 | 5 | ## Configuring Azure AI Services 6 | 7 | Follow the next images to quick setup Azure AI Services - Computer Vision. 8 | 9 |

10 |

11 |

Search for Azure AI Services

12 |
13 | 14 |

15 |

16 |

Select Computer Vision

17 |
18 | 19 |

20 |

21 |

It's available a Tier 0 as free to test

22 |
23 | 24 |

25 |

26 |

At Overview you'll able to get the URL and the Key required.

27 |
28 | 29 |
30 | You can find the complete script here 31 | 32 | 33 | ```powershell 34 | # Configuration 35 | 36 | function ScriptVariables 37 | { 38 | $script:AzureEndpoint = "https://.cognitiveservices.azure.com/vision/v3.2/read/analyze" 39 | $script:ApiKey = "" 40 | $script:FolderPath = "C:\Purview\PurviewOCR\Images" 41 | $script:date = (Get-Date).ToString("yyyy-MM-dd HHmm") 42 | } 43 | 44 | # Function: Get all image files from the folder 45 | function Get-ImageFiles([string]$FolderPath) 46 | { 47 | Write-Host "Reading image files from folder: $FolderPath" 48 | Get-ChildItem -Path $FolderPath -File | Where-Object { $_.Extension -in ".jpg", ".png", ".jpeg", ".bmp" } 49 | } 50 | 51 | # Function: Send image to Azure OCR and return Operation-Location 52 | function Invoke-OCR([string]$ImagePath) 53 | { 54 | Write-Host "Sending image for OCR: $ImagePath" 55 | 56 | $imageBytes = [System.IO.File]::ReadAllBytes($ImagePath) 57 | 58 | $headers = @{ 59 | "Ocp-Apim-Subscription-Key" = $ApiKey 60 | "Content-Type" = "application/octet-stream" 61 | } 62 | 63 | try { 64 | $response = Invoke-WebRequest -Uri $AzureEndpoint -Method Post -Headers $headers -Body $imageBytes -ErrorAction Stop 65 | if ($response.Headers["Operation-Location"]) { 66 | Write-Host "Operation-Location received for $ImagePath" 67 | return $response.Headers["Operation-Location"] 68 | } else { 69 | Write-Error "No Operation-Location found for $ImagePath" 70 | return $null 71 | } 72 | } catch { 73 | Write-Error "Error sending image: $_" 74 | return $null 75 | } 76 | } 77 | 78 | # Function: Poll for OCR result 79 | function Get-OCRResult([string]$OperationLocation) 80 | { 81 | Write-Host "Polling for OCR result from: $OperationLocation" 82 | 83 | $headers = @{ 84 | "Ocp-Apim-Subscription-Key" = $ApiKey 85 | } 86 | 87 | $status = "running" 88 | while ($status -eq "running") { 89 | try { 90 | $response = Invoke-RestMethod -Uri $OperationLocation -Headers $headers -Method Get -ErrorAction Stop 91 | $status = $response.status 92 | if ($status -eq "succeeded") { 93 | Write-Host "OCR processing succeeded.`n" -ForeGroundColor Green 94 | return $response.analyzeResult.readResults 95 | } elseif ($status -eq "failed") { 96 | Write-Error "OCR processing failed.`n" -ForeGroundColor Red 97 | return $null 98 | } 99 | } catch { 100 | Write-Error "Error fetching OCR results: $_" 101 | return $null 102 | } 103 | Start-Sleep -Seconds 2 104 | } 105 | } 106 | 107 | # Function: Process all images in the folder 108 | function Process-ImagesForOCR([string]$FolderPath, [string]$OutputFile) 109 | { 110 | $files = Get-ImageFiles -FolderPath $FolderPath 111 | if ($files.Count -eq 0) { 112 | Write-Error "No image files found in folder: $FolderPath" 113 | return 114 | } 115 | Write-Host "`nTotal images found :`t" -NoNewline 116 | Write-Host $files.Count -ForeGroundColor Green 117 | 118 | $results = @() 119 | foreach ($file in $files) { 120 | Write-Host "`nProcessing file: $($file.FullName)" 121 | 122 | $operationLocation = Invoke-OCR -ImagePath $file.FullName 123 | if ($operationLocation) { 124 | $ocrResult = Get-OCRResult -OperationLocation $operationLocation 125 | if ($ocrResult) { 126 | $results += [PSCustomObject]@{ 127 | FileName = $file.Name 128 | OCRResult = $ocrResult 129 | } 130 | } 131 | } 132 | } 133 | 134 | # Save results to JSON 135 | if ($results.Count -gt 0) { 136 | $results | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputFile 137 | Write-Host "`nOCR processing complete. Results saved to " -NoNewline 138 | Write-Host "$OutputFile." -ForeGroundColor Cyan 139 | } else { 140 | Write-Error "No OCR results to save." 141 | } 142 | } 143 | 144 | # Function: Filter and Format OCR Results 145 | function Filter-OCRResults([string]$InputFile, [string]$OutputFile) 146 | { 147 | Write-Host "Filtering OCR results from: $InputFile" 148 | 149 | # Load OCR JSON Data 150 | $ocrData = Get-Content -Path $InputFile | ConvertFrom-Json 151 | 152 | # Initialize Results Array 153 | $filteredResults = @() 154 | 155 | foreach ($entry in $ocrData) { 156 | $fileName = $entry.FileName 157 | $ocrLines = $entry.OCRResult.lines 158 | 159 | foreach ($line in $ocrLines) { 160 | $text = $line.text 161 | $name = if ($line.PSObject.Properties["appearance"] -and $line.appearance.PSObject.Properties["style"] -and $line.appearance.style.PSObject.Properties["name"]) { 162 | $line.appearance.style.name 163 | } else { 164 | "N/A" 165 | } 166 | $confidence = if ($line.PSObject.Properties["appearance"] -and $line.appearance.PSObject.Properties["style"] -and $line.appearance.style.PSObject.Properties["confidence"]) { 167 | $line.appearance.style.confidence 168 | } else { 169 | "N/A" 170 | } 171 | 172 | $filteredResults += [PSCustomObject]@{ 173 | FileName = $fileName 174 | Text = $text 175 | Name = $name 176 | Confidence = $confidence 177 | } 178 | } 179 | } 180 | 181 | # Save Filtered Results to JSON 182 | if ($filteredResults.Count -gt 0) { 183 | $filteredResults | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputFile 184 | Write-Host "Filtered OCR results saved to $OutputFile." 185 | } else { 186 | Write-Error "No filtered results to save." 187 | } 188 | } 189 | 190 | function MainFunction 191 | { 192 | #Welcome header 193 | cls 194 | Clear-Host 195 | 196 | Write-Host "`n`n----------------------------------------------------------------------------------------" 197 | Write-Host "`nWelcome to OCR script!" -ForegroundColor Green 198 | Write-Host "This script will permit to request OCR Services." 199 | Write-Host "`n----------------------------------------------------------------------------------------" 200 | 201 | ##Show variables set 202 | Write-Host "OCR services set to`t:" -NoNewline 203 | Write-Host "`t$AzureEndpoint" -ForegroundColor Green 204 | Write-Host "Image folder set to`t:" -NoNewline 205 | Write-Host "`t$FolderPath" -ForegroundColor Green 206 | Write-Host "Raw export set to`t:" -NoNewline 207 | Write-Host "`t$OutputFile" -ForegroundColor Green 208 | Write-Host "Filtered export set to`t:" -NoNewline 209 | Write-Host "`t$FilteredFile" -ForegroundColor Green 210 | Write-Host "`nCalling script..." 211 | 212 | $OutputFile = "C:\Purview\PurviewOCR\OCRResults "+$date+".json" 213 | $FilteredFile = "C:\Purview\PurviewOCR\FilteredOCRResults "+$date+".json" # New filtered JSON file 214 | 215 | # Execute the OCR Process 216 | Process-ImagesForOCR -FolderPath $FolderPath -OutputFile $OutputFile 217 | 218 | # Filter and Export the Results 219 | Filter-OCRResults -InputFile $OutputFile -OutputFile $FilteredFile 220 | } 221 | 222 | ScriptVariables 223 | MainFunction 224 | ``` 225 |
226 | 227 |

228 | -------------------------------------------------------------------------------- /Samples/Purview/Purview Role Groups.md: -------------------------------------------------------------------------------- 1 | # Create a Microsoft Purview Role Group 2 | 3 | Microsoft Purview enables you to work using the principle of least privilege. This means you can create "Role Groups," which are subgroups of roles designed for specific activities. 4 | 5 | The following PowerShell script creates a Microsoft Purview Role Group. The group’s name is defined at the beginning of the script in the **ScriptVariables** section. 6 | 7 | This approach is particularly useful when you have multiple automated scripts that require a limited set of permissions, as it automatically assigns only the specific permissions needed. 8 | 9 | To verify that you have access to the correct cmdlets, run the following command: 10 | 11 | ```powershell 12 | Get-Command -Module tmp* | ft Name, CommandType -AutoSize 13 | ``` 14 | 15 | This command will provide insights into the available commands and the information accessible with the roles assigned to the new role group. 16 | 17 | ```powershell 18 | # Script to Create Role Groups. 19 | # You need to execute this script manually with enough permissions to create role groups, Compliance Administrator role is recommended. 20 | 21 | function ScriptVariables 22 | { 23 | [string]$script:RoleGroupName = "My custom role group" 24 | } 25 | 26 | function Get-RoleSelected 27 | { 28 | return @{ 29 | "Admin Unit Extension Manager" = $false 30 | "Attack Simulator Admin" = $false 31 | "Attack Simulator Payload Author" = $false 32 | "Audit Logs" = $false 33 | "Billing Admin" = $false 34 | "Case Management" = $false 35 | "Communication" = $false 36 | "Communication Compliance Admin" = $false 37 | "Communication Compliance Analysis" = $false 38 | "Communication Compliance Case Management" = $false 39 | "Communication Compliance Investigation" = $false 40 | "Communication Compliance Viewer" = $true 41 | "Compliance Administrator" = $false 42 | "Compliance Manager Administration" = $false 43 | "Compliance Manager Assessment" = $false 44 | "Compliance Manager Contribution" = $false 45 | "Compliance Manager Reader" = $false 46 | "Compliance Search" = $false 47 | "Credential Reader" = $false 48 | "Credential Writer" = $false 49 | "Custodian" = $false 50 | "Data Classification Content Download" = $false 51 | "Data Classification Content Viewer" = $true 52 | "Data Classification Feedback Provider" = $false 53 | "Data Classification Feedback Reviewer" = $false 54 | "Data Classification List Viewer" = $true 55 | "Data Connector Admin" = $false 56 | "Data Governance Administrator" = $false 57 | "Data Investigation Management" = $false 58 | "Data Map Reader" = $false 59 | "Data Map Writer" = $false 60 | "Data Security Investigation Admin" = $false 61 | "Data Security Investigation Investigator" = $false 62 | "Data Security Investigation Reviewer" = $false 63 | "Data Security Viewer" = $false 64 | "DLP Compliance Management" = $false 65 | "Disposition Management" = $false 66 | "Exchange Administrator" = $false 67 | "Exact Data Match Upload Admin" = $false 68 | "Export" = $false 69 | "Hold" = $false 70 | "IB Compliance Management" = $false 71 | "Insights Reader" = $false 72 | "Insights Writer" = $false 73 | "Information Protection Admin" = $false 74 | "Information Protection Analyst" = $false 75 | "Information Protection Investigator" = $false 76 | "Information Protection Reader" = $true 77 | "Insider Risk Management Admin" = $false 78 | "Insider Risk Management Analysis" = $false 79 | "Insider Risk Management Approval" = $false 80 | "Insider Risk Management Audit" = $false 81 | "Insider Risk Management Investigation" = $false 82 | "Insider Risk Management Permanent contribution" = $false 83 | "Insider Risk Management Reports Administrator" = $false 84 | "Insider Risk Management Sessions" = $false 85 | "Insider Risk Management Temporary contribution" = $false 86 | "Knowledge Admin" = $false 87 | "License Usage Reader" = $false 88 | "Manage Alerts" = $false 89 | "Manage Review Set Tags" = $false 90 | "MyBaseOptions" = $false 91 | "Organization Configuration" = $false 92 | "Preview" = $false 93 | "Priority Cleanup Admin" = $false 94 | "Priority Cleanup Viewer" = $false 95 | "Privacy Management Admin" = $false 96 | "Privacy Management Analysis" = $false 97 | "Privacy Management Investigation" = $false 98 | "Privacy Management Permanent contribution" = $false 99 | "Privacy Management Temporary contribution" = $false 100 | "Privacy Management Viewer" = $false 101 | "Purview Copilot Workspace Contributor" = $false 102 | "Purview Domain Manager" = $false 103 | "Purview Evaluation Administrator" = $false 104 | "Quarantine" = $false 105 | "RecordManagement" = $false 106 | "Retention Management" = $false 107 | "Review" = $false 108 | "RMS Decrypt" = $false 109 | "Role Management" = $false 110 | "Scan Reader" = $false 111 | "Scan Writer" = $false 112 | "Scope Manager" = $false 113 | "Search And Purge" = $false 114 | "Security Administrator" = $false 115 | "Security Reader" = $false 116 | "Sensitivity Label Administrator" = $false 117 | "Sensitivity Label Reader" = $true 118 | "Service Assurance View" = $false 119 | "Source Reader" = $false 120 | "Source Writer" = $false 121 | "Subject Rights Request Admin" = $false 122 | "Subject Rights Request Approver" = $false 123 | "Supervisory Review Administrator" = $false 124 | "Tag Contributor" = $false 125 | "Tag Manager" = $false 126 | "Tag Reader" = $false 127 | "Tenant AllowBlockList Manager" = $false 128 | "View-Only Audit Logs" = $false 129 | "View-Only Case" = $false 130 | "View-Only DLP Compliance Management" = $false 131 | "View-Only Device Management" = $false 132 | "View-Only IB Compliance Management" = $false 133 | "View-Only Manage Alerts" = $false 134 | "View-Only Record Management" = $false 135 | "View-Only Recipients" = $false 136 | "View-Only Retention Management" = $true 137 | } 138 | } 139 | 140 | function Create-PurviewRoleGroup 141 | { 142 | $rolesSelected = Get-RoleSelected 143 | Write-Host "`nThis script will create a role group with the following information:" 144 | Write-Host "* Role Group Name`t:" -NoNewLine 145 | Write-Host "`t'$RoleGroupName'" -ForeGroundColor DarkBlue 146 | foreach ($role in $rolesSelected.Keys) 147 | { 148 | if ($rolesSelected[$role]) 149 | { 150 | Write-Host "* Role selected`t`t:" -NoNewLine 151 | Write-Host "`t'$role'" -ForeGroundColor Green 152 | } 153 | } 154 | 155 | Write-Host "`nPress any key to continue..." -ForegroundColor DarkYellow 156 | Write-Host "(Alternatively, press Ctrl+C to exit and make any necessary changes.)" 157 | $key = ([System.Console]::ReadKey($true)) | Out-Null 158 | 159 | $existingGroup = Get-RoleGroup | Where-Object {$_.Name -eq $RoleGroupName} 160 | $rolesToAssign = $rolesSelected.GetEnumerator() | Where-Object { $_.Value } | ForEach-Object { $_.Key } 161 | if ($existingGroup) 162 | { 163 | Write-Host "`nRole group '$RoleGroupName' already exists. Exiting.`n" 164 | exit 165 | }else 166 | { 167 | Write-Host "`nCreating role group '$RoleGroupName'..." 168 | New-RoleGroup -Name "$RoleGroupName" -DisplayName "$RoleGroupName" -Roles $rolesToAssign -Description "Role group for '$RoleGroupName' Purview tasks" -ErrorAction Stop |Out-Null 169 | } 170 | 171 | Write-Host "Role group '$RoleGroupName' created and roles assigned successfully.`n`n" 172 | } 173 | 174 | function MainScript 175 | { 176 | cls 177 | ScriptVariables 178 | Write-Host "`nTo run this script, you must have sufficient permissions to create role groups." 179 | Write-Host "It is recommended to have the Compliance Administrator role for optimal execution." 180 | Write-Host "`nConnecting to Purview..." 181 | Write-Host "Please check your browser.`n" 182 | Connect-IPPSSession -UseRPSSession:$false -ShowBanner:$false 183 | Create-PurviewRoleGroup 184 | } 185 | 186 | MainScript 187 | ``` 188 | 189 |

190 | -------------------------------------------------------------------------------- /Samples/UpdateInfo/Update.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | { 4 | "version": "1.0.0", 5 | "changes": "Initial release", 6 | "directory": "ConfigFiles", 7 | "URI": "ESample/ConfigFiles/example.json", 8 | "file": "sample.ps1", 9 | "format": "ps1" 10 | }, 11 | { 12 | "version": "1.0.0", 13 | "changes": "Initial release", 14 | "directory": "ROOT", 15 | "URI": "ESample/ConfigFiles/example.json", 16 | "file": "sample2.ps1", 17 | "format": "ps1" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /Samples/WhatCanIFindHere.md: -------------------------------------------------------------------------------- 1 | # What can I find here? 2 | 3 | In the samples section, I’ll provide several scripts designed to support your daily tasks, with details on the specific [Lego](/Lego/HOWTO.md) and `LegoPlus` components used. I hope these scripts enhance your workflow or contribute to your learning journey. 4 | 5 |

6 |

7 |

PowerShell as LEGO

8 |
9 | 10 | 11 |

12 | -------------------------------------------------------------------------------- /Support/HowToConfigureAzureAIVision.md: -------------------------------------------------------------------------------- 1 | ![image](https://github.com/user-attachments/assets/0f5f180c-ec46-4e83-b6f5-56bbb5cfd45d) 2 | 3 | ![image](https://github.com/user-attachments/assets/b4d90e05-b72a-48e2-b927-7181890114c1) 4 | 5 | ![image](https://github.com/user-attachments/assets/534fb2da-0007-4657-a213-deedf4eb7a19) 6 | 7 | ![image](https://github.com/user-attachments/assets/de67c11d-9628-4648-ae60-6220bdbdcb96) 8 | 9 | ![image](https://github.com/user-attachments/assets/2eba0461-56c1-4ee3-89e4-edc3a35239ae) 10 | 11 | ![image](https://github.com/user-attachments/assets/f5356140-a41e-4fd4-aff1-13e9a58e03cb) 12 | --------------------------------------------------------------------------------