├── 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 | 
2 |
3 | 
4 |
5 | 
6 |
7 | 
8 |
9 | 
10 |
11 | 
12 |
--------------------------------------------------------------------------------