├── .gitignore ├── .vscode └── launch.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── SUPPORT.md ├── build ├── Add-PSModuleHeader.ps1 ├── Build-Module.ps1 ├── Build-PSModule.ps1 ├── CommonFunctions.psm1 ├── Get-PSModuleInfo.ps1 ├── Launch-PSModule.ps1 ├── Merge-PSModuleNestedModuleScripts.ps1 ├── PesterConfiguration.Baseline.psd1 ├── PesterConfiguration.CD.psd1 ├── PesterConfiguration.CI.psd1 ├── PesterConfiguration.Debug.psd1 ├── PesterConfiguration.psd1 ├── PesterCustomAssertions.psm1 ├── Publish-PSModule.ps1 ├── Restore-NugetPackages.ps1 ├── Restore-PSModuleDependencies.ps1 ├── Sign-PSModule.ps1 ├── Test-PSModule.ps1 ├── Update-PSModuleManifest.ps1 └── azure-pipelines │ ├── azure-pipelines-cd.yml │ ├── azure-pipelines-ci.yml │ ├── template-psmodule-build.yml │ ├── template-psmodule-package.yml │ ├── template-psmodule-publish.yml │ ├── template-psmodule-sign.yml │ └── template-psmodule-test.yml ├── example_pipeline └── azure-backup-pipeline.yml └── src ├── Connect-EntraExporter.ps1 ├── EntraExporter.psd1 ├── EntraExporter.psm1 ├── Export-Entra.ps1 ├── Get-EEAccessPackageAssignmentPolicies.ps1 ├── Get-EEAccessPackageAssignments.ps1 ├── Get-EEAccessPackageResourceScopes.ps1 ├── Get-EEDefaultSchema.ps1 ├── Get-EERequiredScopes.ps1 └── internal ├── ConvertFrom-QueryString.ps1 ├── ConvertTo-OrderedDictionary.ps1 ├── ConvertTo-QueryString.ps1 ├── Get-ObjectProperty.ps1 └── Invoke-Graph.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | .DS_Store 352 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "PowerShell: Module Interactive Session", 9 | "type": "PowerShell", 10 | "request": "launch", 11 | "script": "Import-Module -Force ${workspaceFolder}/src/EntraExporter.psd1" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to agree to a 4 | Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us 5 | the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. 6 | 7 | When you submit a pull request, a CLA bot will automatically determine whether you need to provide 8 | a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions 9 | provided by the bot. You will only need to do this once across all repos using our CLA. 10 | 11 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 12 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 13 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Entra Exporter 2 | 3 | [![PSGallery Version](https://img.shields.io/powershellgallery/v/EntraExporter.svg?style=flat&label=PSGallery%20Version)](https://www.powershellgallery.com/packages/EntraExporter) [![PSGallery Downloads](https://img.shields.io/powershellgallery/dt/EntraExporter.svg?style=flat&label=PSGallery%20Downloads)](https://www.powershellgallery.com/packages/EntraExporter) [![PSGallery Platform](https://img.shields.io/powershellgallery/p/EntraExporter.svg?style=flat&label=PSGallery%20Platform)](https://www.powershellgallery.com/packages/EntraExporter) 4 | 5 | The Entra Exporter is a PowerShell module that allows you to export your Entra and Azure AD B2C configuration settings to local .json files. 6 | 7 | This module can be run as a nightly scheduled task or a DevOps component (Azure DevOps, GitHub, Jenkins) and the exported files can be version controlled in Git or SharePoint. 8 | 9 | This will provide tenant administrators with a historical view of all the settings in the tenant including the change history over the years. 10 | 11 | > [!IMPORTANT] 12 | > The AzureADExporter module in the PowerShell Gallery is now deprecated. Please install the new **EntraExporter** module. 13 | 14 | ## Installing the module 15 | 16 | ```powershell 17 | Install-Module EntraExporter 18 | ``` 19 | 20 | ## Using the module 21 | 22 | ### Connecting and exporting your config 23 | 24 | ```powershell 25 | Connect-EntraExporter 26 | Export-Entra -Path 'C:\EntraBackup\' 27 | ``` 28 | 29 | While Connect-EntraExporter is available for convenience you can alternatively use Connect-MgGraph with the following scopes to authenticate. 30 | 31 | ```powershell 32 | Connect-MgGraph -Scopes 'Directory.Read.All', 'Policy.Read.All', 'IdentityProvider.Read.All', 'Organization.Read.All', 'User.Read.All', 'EntitlementManagement.Read.All', 'UserAuthenticationMethod.Read.All', 'IdentityUserFlow.Read.All', 'APIConnectors.Read.All', 'AccessReview.Read.All', 'Agreement.Read.All', 'Policy.Read.PermissionGrant', 'PrivilegedAccess.Read.AzureResources', 'PrivilegedAccess.Read.AzureAD', 'Application.Read.All' 33 | ``` 34 | 35 | ### Export options 36 | 37 | To export object and settings use the following command: 38 | 39 | ```powershell 40 | Export-Entra -Path 'C:\EntraBackup\' 41 | ``` 42 | 43 | This default method exports the most common set of objects and settings. 44 | 45 | > [!NOTE] 46 | > We recommend using PowerShell 7+ to create a consistent output. While PowerShell 5.1 can be used the output generated is not optimal. 47 | 48 | The following objects and settings are not exported by default: 49 | 50 | * B2C, B2B, Static Groups and group memberships, Applications, ServicePrincipals, Users, Privileged Identity Management (built in roles, default roles settings, non permanent role assignments) 51 | 52 | Use the -All parameter to perform a full export: 53 | 54 | ```powershell 55 | Export-Entra -Path 'C:\EntraBackup\' -All 56 | ``` 57 | 58 | The ``-Type`` parameter can be used to select specific objects and settings to export. The default type is "Config": 59 | 60 | ```powershell 61 | # export default all users as well as default objects and settings 62 | Export-Entra -Path 'C:\EntraBackup\' -Type "Config","Users" 63 | 64 | # export applications only 65 | Export-Entra -Path 'C:\EntraBackup\' -Type "Applications" 66 | 67 | # export B2C specific properties only 68 | Export-Entra -Path 'C:\EntraBackup\' -Type "B2C" 69 | 70 | # export B2B properties along with AD properties 71 | Export-Entra -Path 'C:\EntraBackup\' -Type "B2B","Config" 72 | ``` 73 | 74 | The currently valid types are: All (all elements), Config (default configuration), AccessReviews, ConditionalAccess, Users, Groups, Applications, ServicePrincipals, B2C, B2B, PIM, PIMAzure, PIMAAD, AppProxy, Organization, Domains, EntitlementManagement, Policies, AdministrativeUnits, SKUs, Identity, Roles, Governance 75 | 76 | This list can also be retrieved via: 77 | 78 | ```powershell 79 | (Get-Command Export-Entra | Select-Object -Expand Parameters)['Type'].Attributes.ValidValues 80 | ``` 81 | 82 | Additional filters can be applied: 83 | 84 | * To exclude on-prem synced users from the export 85 | 86 | ```powershell 87 | Export-Entra -Path 'C:\EntraBackup\' -All -CloudUsersAndGroupsOnly 88 | ``` 89 | 90 | > [!NOTE] 91 | > This module exports all settings that are available through the Microsoft Graph API. Entra settings and objects that are not yet available in the Graph API are not included. 92 | 93 | ## Exported configuration includes 94 | 95 | * Users 96 | * Groups 97 | * Dynamic and Assigned groups (incl. Members and Owners) 98 | * Group Settings 99 | * Devices 100 | * External Identities 101 | * Authorization Policy 102 | * API Connectors 103 | * User Flows 104 | * Roles and Administrators 105 | * Administrative Units 106 | * Applications 107 | * Enterprise Applications 108 | * App Registrations 109 | * Claims Mapping Policy 110 | * Extension Properties 111 | * Admin Consent Request Policy 112 | * Permission Grant Policies 113 | * Token Issuance Policies 114 | * Token Lifetime Policies 115 | * Application Management Policies 116 | * Identity Governance 117 | * Entitlement Management 118 | * Access Packages 119 | * Catalogs 120 | * Connected Organizations 121 | * Access Reviews 122 | * Privileged Identity Management 123 | * Entra Roles 124 | * Azure Resources 125 | * Terms of Use 126 | * Application Proxy 127 | * Connectors and Connect Groups 128 | * Agents and Agent Groups 129 | * Published Resources 130 | * Licenses 131 | * Connect sync settings 132 | * Custom domain names 133 | * Company branding 134 | * Profile Card Properties 135 | * User settings 136 | * Tenant Properties 137 | * Technical contacts 138 | * Security 139 | * Conditional Access Policies 140 | * Named Locations 141 | * Authentication Methods Policies 142 | * Identity Security Defaults Enforcement Policy 143 | * Permission Grant Policies 144 | * Tenant Policies and Settings 145 | * Feature Rollout Policies 146 | * Cross-tenant Access 147 | * Activity Based Timeout Policies 148 | * Application Management Policies 149 | * Hybrid Authentication 150 | * Identity Providers 151 | * Home Realm Discovery Policies 152 | 153 | * B2C Settings 154 | * B2C User Flows 155 | * Identity Providers 156 | * User Attribute Assignments 157 | * API Connector Configuration 158 | * Languages 159 | 160 | ## Integrate to Azure DevOps Pipeline 161 | 162 | Exporting Entra settings to json files makes them useful to integrate with DevOps pipelines. 163 | 164 | > **Note**: 165 | > Delegated authentication will require a dedicated agent where the authentication has been pre-configured. 166 | 167 | Below is a sample of exporting in two steps: 168 | 169 | 1. Export Entra to local json files 170 | 2. Update a git repository with the files 171 | 172 | To export the configuration (replace variables with ``<>`` with the values suited to your situation): 173 | 174 | ```powershell 175 | $tenantPath = './' 176 | $tenantId = '' 177 | Write-Host 'git checkout main...' 178 | git config --global core.longpaths true #needed for Windows 179 | git checkout main 180 | 181 | Write-Host 'Clean git folder...' 182 | Remove-Item $tenantPath -Force -Recurse 183 | 184 | Write-Host 'Installing modules...' 185 | Install-Module Microsoft.Graph.Authentication -Scope CurrentUser -Force 186 | Install-Module EntraExporter -Scope CurrentUser -Force 187 | 188 | Write-Host 'Connecting...' 189 | Connect-EntraExporter -TenantId $tenantId 190 | 191 | Write-Host 'Starting backup...' 192 | Export-Entra $tenantPath -All 193 | ``` 194 | 195 | To update the git repository with the generated files: 196 | 197 | ```powershell 198 | Write-Host 'Updating repo...' 199 | git config user.email "" 200 | git config user.name "" 201 | git add -u 202 | git add -A 203 | git commit -m "ADO Update" 204 | git push origin 205 | ``` 206 | 207 | BTW Here is a really good step by step guide from Ondrej Sebela that includes illustrations as well: 208 | 209 | [How to easily backup your Azure environment using EntraExporter and Azure DevOps Pipeline](https://doitpsway.com/how-to-easily-backup-your-azure-environment-using-entraexporter-and-azure-devops-pipeline) 210 | 211 | ## FAQs 212 | 213 | ### Error 'Could not find a part of the path' when exported JSON file paths are longer than 260 characters 214 | 215 | A workaround to this is to enable long paths via the Windows registry or a GPO setting. Run the following from an elevated PowerShell session and then close PowerShell before trying your export again: 216 | 217 | ```powershell 218 | New-ItemProperty ` 219 | -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" ` 220 | -Name "LongPathsEnabled" ` 221 | -Value 1 ` 222 | -PropertyType DWORD ` 223 | -Force 224 | ``` 225 | 226 | Credit: @shaunluttin via https://bigfont.ca/enable-long-paths-in-windows-with-powershell/ and https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=powershell. 227 | 228 | ## Trademarks 229 | 230 | This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. Any use of third-party trademarks or logos are subject to those third-party's policies. 231 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | ## How to file issues and get help 4 | 5 | This project uses GitHub Issues to track bugs and feature requests. Please search the existing 6 | issues before filing new issues to avoid duplicates. For new issues, file your bug or 7 | feature request as a new Issue. 8 | 9 | ## Microsoft Support Policy 10 | 11 | Support for this module is limited to the resources listed above. 12 | 13 | THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF 14 | ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO 15 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A 16 | PARTICULAR PURPOSE. -------------------------------------------------------------------------------- /build/Add-PSModuleHeader.ps1: -------------------------------------------------------------------------------- 1 | param 2 | ( 3 | # Path to Module Manifest 4 | [Parameter(Mandatory = $false)] 5 | [string] $ModuleManifestPath = ".\release\*\*.*.*", 6 | # 7 | [Parameter(Mandatory = $false)] 8 | [string] $OutputModulePath 9 | ) 10 | 11 | ## Initialize 12 | Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop 13 | 14 | [System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultFilename "*.psd1" -ErrorAction Stop 15 | 16 | ## Read Module Manifest 17 | $ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName 18 | 19 | if ($OutputModulePath) { 20 | [System.IO.FileInfo] $OutputModuleFileInfo = Get-PathInfo $OutputModulePath -InputPathType File -DefaultFilename "$($ModuleManifestFileInfo.BaseName).psm1" -ErrorAction SilentlyContinue 21 | } 22 | else { 23 | [System.IO.FileInfo] $OutputModuleFileInfo = Get-PathInfo $ModuleManifest['RootModule'] -InputPathType File -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction SilentlyContinue 24 | } 25 | 26 | if ($OutputModuleFileInfo.Extension -eq ".psm1") { 27 | ## Add Requires Statements 28 | $RequiresStatements = "" 29 | if ($ModuleManifest['PowerShellVersion']) { $RequiresStatements += "#Requires -Version {0}`r`n" -f $ModuleManifest['PowerShellVersion'] } 30 | if ($ModuleManifest['CompatiblePSEditions']) { $RequiresStatements += "#Requires -PSEdition {0}`r`n" -f ($ModuleManifest['CompatiblePSEditions'] -join ',') } 31 | foreach ($RequiredAssembly in $ModuleManifest['RequiredAssemblies']) { 32 | $RequiresStatements += "#Requires -Assembly $_`r`n" 33 | } 34 | foreach ($RequiredModule in $ModuleManifest['RequiredModules']) { 35 | $RequiresStatements += ConvertTo-PsString $ModuleManifest['RequiredModules'] -Compact -RemoveTypes ([hashtable], [string]) | ForEach-Object { "#Requires -Module $_`r`n" } 36 | } 37 | 38 | ## Build Module Comment Header 39 | [string] $CommentHeader = "<#`r`n" 40 | $CommentHeader += ".SYNOPSIS`r`n {0}`r`n" -f $ModuleManifestFileInfo.BaseName 41 | 42 | if ($ModuleManifest['Description']) { 43 | $CommentHeader += ".DESCRIPTION`r`n {0}`r`n" -f $ModuleManifest['Description'] 44 | } 45 | 46 | [string]$ModuleVersion = if ($ModuleManifest.PrivateData.PSData['Prerelease']) { '{0}-{1}' -f $ModuleManifest['ModuleVersion'], $ModuleManifest.PrivateData.PSData['Prerelease'] } else { $ModuleManifest['ModuleVersion'] } 47 | $CommentHeader += ".NOTES`r`n" 48 | $CommentHeader += " ModuleVersion: {0}`r`n" -f $ModuleVersion 49 | if ($ModuleManifest['GUID']) { $CommentHeader += " GUID: {0}`r`n" -f $ModuleManifest['GUID'] } 50 | if ($ModuleManifest['Author']) { $CommentHeader += " Author: {0}`r`n" -f $ModuleManifest['Author'] } 51 | if ($ModuleManifest['CompanyName']) { $CommentHeader += " CompanyName: {0}`r`n" -f $ModuleManifest['CompanyName'] } 52 | if ($ModuleManifest['Copyright']) { $CommentHeader += " Copyright: {0}`r`n" -f $ModuleManifest['Copyright'] } 53 | if ($ModuleManifest['FunctionsToExport']) { 54 | ## ToDo: Account for modules with functions and/or cmdlets. 55 | $CommentHeader += ".FUNCTIONALITY`r`n {0}`r`n" -f ($ModuleManifest['FunctionsToExport'] -join ', ') 56 | } 57 | if ($ModuleManifest.PrivateData.PSData['ProjectUri']) { 58 | $CommentHeader += ".LINK`r`n {0}`r`n" -f $ModuleManifest.PrivateData.PSData['ProjectUri'] 59 | } 60 | $CommentHeader += "#>" 61 | 62 | ## Add Comment Header to Script Module 63 | if ($OutputModuleFileInfo.Exists) { 64 | $RootModuleContent = (Get-Content $OutputModuleFileInfo.FullName -Raw) 65 | } 66 | else { 67 | $RootModuleContent = $null 68 | } 69 | 70 | $RequiresStatements, $CommentHeader, $RootModuleContent | Set-Content $OutputModuleFileInfo.FullName -Encoding utf8BOM 71 | } 72 | -------------------------------------------------------------------------------- /build/Build-Module.ps1: -------------------------------------------------------------------------------- 1 | # Build script for EntraExporter PowerShell Module 2 | 3 | param( 4 | [ValidateSet('Build', 'Test', 'Publish', 'Install', 'Clean')] 5 | [string]$Task = 'Build', 6 | 7 | [string]$OutputPath = './release/EntraExporter', 8 | 9 | [string]$Repository = 'PSGallery', 10 | 11 | [string]$ApiKey 12 | ) 13 | 14 | # Clean output directory 15 | function Invoke-Clean { 16 | if (Test-Path $OutputPath) { 17 | Remove-Item -Path $OutputPath -Recurse -Force 18 | Write-Host "Cleaned output directory: $OutputPath" -ForegroundColor Green 19 | } 20 | } 21 | 22 | # Build the module 23 | function Invoke-Build { 24 | Write-Host "Building module..." -ForegroundColor Cyan 25 | 26 | # Clean first 27 | Invoke-Clean 28 | 29 | # Copy module files including subfolders 30 | Write-Host "Copying module files to $OutputPath..." -ForegroundColor Yellow 31 | Copy-Item -Path './src' -Destination $OutputPath -Recurse 32 | 33 | Write-Host "Module built successfully in: $OutputPath" -ForegroundColor Green 34 | } 35 | 36 | # Test the module 37 | function Invoke-Test { 38 | Write-Host "Testing module..." -ForegroundColor Cyan 39 | 40 | # Import the module 41 | Import-Module "$OutputPath/EntraExporter.psd1" -Force 42 | 43 | # Test module manifest 44 | $manifest = Test-ModuleManifest "$OutputPath/EntraExporter.psd1" 45 | if ($manifest) { 46 | Write-Host "✅ Module manifest is valid" -ForegroundColor Green 47 | Write-Host " Version: $($manifest.Version)" -ForegroundColor White 48 | Write-Host " Author: $($manifest.Author)" -ForegroundColor White 49 | Write-Host " Description: $($manifest.Description)" -ForegroundColor White 50 | } else { 51 | Write-Error "❌ Module manifest validation failed" 52 | return $false 53 | } 54 | 55 | Write-Host "✅ All tests passed!" -ForegroundColor Green 56 | return $true 57 | } 58 | 59 | # Install the module locally 60 | function Invoke-Install { 61 | Write-Host "Installing EntraExporter module locally..." -ForegroundColor Cyan 62 | 63 | # Get user module path 64 | $userModulePath = $env:PSModulePath.Split([IO.Path]::PathSeparator) | 65 | Where-Object { $_ -like "*$env:USERNAME*" -or $_ -like "*Users*" } | 66 | Select-Object -First 1 67 | 68 | if (-not $userModulePath) { 69 | $userModulePath = "$env:USERPROFILE\Documents\PowerShell\Modules" 70 | } 71 | 72 | $installPath = Join-Path $userModulePath "EntraExporter" 73 | 74 | # Remove existing installation 75 | if (Test-Path $installPath) { 76 | Remove-Item -Path $installPath -Recurse -Force 77 | Write-Host "Removed existing installation" -ForegroundColor Yellow 78 | } 79 | 80 | # Create directory and copy files 81 | New-Item -Path $installPath -ItemType Directory -Force | Out-Null 82 | Copy-Item -Path "$OutputPath/*" -Destination $installPath -Recurse 83 | 84 | Write-Host "Module installed to: $installPath" -ForegroundColor Green 85 | Write-Host "You can now use: Import-Module EntraExporter" -ForegroundColor Cyan 86 | } 87 | 88 | # Publish the module 89 | function Invoke-Publish { 90 | if (-not $ApiKey) { 91 | Write-Error "API Key is required for publishing. Use -ApiKey parameter." 92 | return 93 | } 94 | 95 | Write-Host "Publishing EntraExporter module to $Repository..." -ForegroundColor Cyan 96 | 97 | try { 98 | Publish-Module -Path $OutputPath -Repository $Repository -NuGetApiKey $ApiKey 99 | Write-Host "✅ Module published successfully!" -ForegroundColor Green 100 | } 101 | catch { 102 | Write-Error "❌ Failed to publish module: $($_.Exception.Message)" 103 | } 104 | } 105 | 106 | # Main execution 107 | switch ($Task) { 108 | 'Build' { Invoke-Build } 109 | 'Test' { 110 | Invoke-Build 111 | Invoke-Test 112 | } 113 | 'Publish' { 114 | Invoke-Build 115 | if (Invoke-Test) { 116 | Invoke-Publish 117 | } 118 | } 119 | 'Install' { 120 | Invoke-Build 121 | if (Invoke-Test) { 122 | Invoke-Install 123 | } 124 | } 125 | 'Clean' { Invoke-Clean } 126 | } 127 | -------------------------------------------------------------------------------- /build/Build-PSModule.ps1: -------------------------------------------------------------------------------- 1 | param 2 | ( 3 | # Directory used to base all relative paths 4 | [Parameter(Mandatory = $false)] 5 | [string] $BaseDirectory = "..\", 6 | # 7 | [Parameter(Mandatory = $false)] 8 | [string] $OutputDirectory = ".\build\release\", 9 | # 10 | [Parameter(Mandatory = $false)] 11 | [string] $SourceDirectory = ".\src\", 12 | # 13 | [Parameter(Mandatory = $false)] 14 | [string] $ModuleManifestPath, 15 | # 16 | [Parameter(Mandatory = $false)] 17 | [string] $PackagesConfigPath = ".\packages.config", 18 | # 19 | [Parameter(Mandatory = $false)] 20 | [string] $PackagesDirectory = ".\build\packages", 21 | # 22 | [Parameter(Mandatory = $false)] 23 | [string] $LicensePath = ".\LICENSE", 24 | # 25 | [Parameter(Mandatory = $false)] 26 | [switch] $SkipMergingNestedModuleScripts 27 | ) 28 | 29 | ## Initialize 30 | Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop 31 | 32 | [System.IO.DirectoryInfo] $BaseDirectoryInfo = Get-PathInfo $BaseDirectory -InputPathType Directory -ErrorAction Stop 33 | [System.IO.DirectoryInfo] $OutputDirectoryInfo = Get-PathInfo $OutputDirectory -InputPathType Directory -DefaultDirectory $BaseDirectoryInfo.FullName -ErrorAction SilentlyContinue 34 | [System.IO.DirectoryInfo] $SourceDirectoryInfo = Get-PathInfo $SourceDirectory -InputPathType Directory -DefaultDirectory $BaseDirectoryInfo.FullName -ErrorAction Stop 35 | [System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultDirectory $SourceDirectoryInfo.FullName -DefaultFilename "*.psd1" -ErrorAction Stop 36 | [System.IO.FileInfo] $PackagesConfigFileInfo = Get-PathInfo $PackagesConfigPath -DefaultDirectory $BaseDirectoryInfo.FullName -DefaultFilename "packages.config" -ErrorAction SilentlyContinue 37 | [System.IO.DirectoryInfo] $PackagesDirectoryInfo = Get-PathInfo $PackagesDirectory -InputPathType Directory -DefaultDirectory $BaseDirectoryInfo.FullName -ErrorAction SilentlyContinue 38 | [System.IO.FileInfo] $LicenseFileInfo = Get-PathInfo $LicensePath -DefaultDirectory $BaseDirectoryInfo.FullName -DefaultFilename "LICENSE" -ErrorAction SilentlyContinue 39 | 40 | ## Read Module Manifest 41 | $ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName 42 | [System.IO.DirectoryInfo] $ModuleOutputDirectoryInfo = Join-Path $OutputDirectoryInfo.FullName (Join-Path $ModuleManifestFileInfo.BaseName $ModuleManifest['ModuleVersion']) 43 | [System.IO.FileInfo] $OutputModuleManifestFileInfo = Join-Path $ModuleOutputDirectoryInfo.FullName $ModuleManifestFileInfo.Name 44 | 45 | ## Copy Source Module Code to Module Output Directory 46 | Assert-DirectoryExists $ModuleOutputDirectoryInfo -ErrorAction Stop | Out-Null 47 | Copy-Item ("{0}\*" -f $SourceDirectoryInfo.FullName) -Destination $ModuleOutputDirectoryInfo.FullName -Recurse -Force 48 | if (!$SkipMergingNestedModuleScripts) { 49 | [System.IO.FileInfo] $OutputRootModuleFileInfo = (Join-Path $ModuleOutputDirectoryInfo.FullName $ModuleManifest['RootModule']) 50 | &$PSScriptRoot\Merge-PSModuleNestedModuleScripts.ps1 -ModuleManifestPath $OutputModuleManifestFileInfo.FullName -OutputModulePath $OutputRootModuleFileInfo.FullName -MergeWithRootModule -RemoveNestedModuleScriptFiles 51 | } 52 | if ($LicenseFileInfo.Exists) { 53 | Copy-Item $LicenseFileInfo.FullName -Destination (Join-Path $ModuleOutputDirectoryInfo.FullName License.txt) -Force 54 | } 55 | 56 | if ($PackagesConfigFileInfo.Exists) { 57 | ## NuGet Restore 58 | &$PSScriptRoot\Restore-NugetPackages.ps1 -PackagesConfigPath $PackagesConfigFileInfo.FullName -OutputDirectory $PackagesDirectoryInfo.FullName 59 | 60 | ## Read Packages Configuration 61 | $xmlPackagesConfig = New-Object xml 62 | $xmlPackagesConfig.Load($PackagesConfigFileInfo.FullName) 63 | 64 | ## Copy Packages to Module Output Directory 65 | foreach ($package in $xmlPackagesConfig.packages.package) { 66 | [string[]] $targetFrameworks = $package.targetFramework 67 | if (!$targetFrameworks) { [string[]] $targetFrameworks = "net45", "netcoreapp2.1" } 68 | foreach ($targetFramework in $targetFrameworks) { 69 | [System.IO.DirectoryInfo] $PackageDirectory = Join-Path $PackagesDirectoryInfo.FullName ("{0}.{1}\lib\{2}" -f $package.id, $package.version, $targetFramework) 70 | [System.IO.DirectoryInfo] $PackageOutputDirectory = "{0}\{1}.{2}\{3}" -f $ModuleOutputDirectoryInfo.FullName, $package.id, $package.version, $targetFramework 71 | $PackageOutputDirectory 72 | Assert-DirectoryExists $PackageOutputDirectory -ErrorAction Stop | Out-Null 73 | Copy-Item ("{0}\*" -f $PackageDirectory) -Destination $PackageOutputDirectory.FullName -Recurse -Force 74 | } 75 | } 76 | } 77 | 78 | ## Get Module Output FileList 79 | #$ModuleFileListFileInfo = Get-ChildItem $ModuleOutputDirectoryInfo.FullName -Recurse -File 80 | #$ModuleManifestOutputFileInfo = $ModuleFileListFileInfo | Where-Object Name -EQ $ModuleManifestFileInfo.Name 81 | 82 | ## Update Module Manifest in Module Output Directory 83 | &$PSScriptRoot\Update-PSModuleManifest.ps1 -ModuleManifestPath $OutputModuleManifestFileInfo.FullName 84 | if (!$SkipMergingNestedModuleScripts) { 85 | &$PSScriptRoot\Add-PSModuleHeader.ps1 -ModuleManifestPath $OutputModuleManifestFileInfo.FullName 86 | } 87 | 88 | ## Sign Module 89 | &$PSScriptRoot\Sign-PSModule.ps1 -ModuleManifestPath $OutputModuleManifestFileInfo.FullName | Format-Table Path, Status, StatusMessage 90 | -------------------------------------------------------------------------------- /build/Get-PSModuleInfo.ps1: -------------------------------------------------------------------------------- 1 | param 2 | ( 3 | # Path to Module Manifest 4 | [parameter(Mandatory = $false)] 5 | [string] $ModuleManifestPath = "..\src", 6 | # Path to packages.config file 7 | [parameter(Mandatory = $false)] 8 | [string] $PackagesConfigPath = "..\", 9 | # Return trimmed version to the depth specified 10 | [parameter(Mandatory = $false)] 11 | [int] $TrimVersionDepth 12 | ) 13 | 14 | ## Initialize 15 | Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop 16 | 17 | [System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultFilename "*.psd1" -ErrorAction Stop 18 | [System.IO.FileInfo] $PackagesConfigFileInfo = Get-PathInfo $PackagesConfigPath -DefaultFilename "packages.config" -ErrorAction SilentlyContinue 19 | 20 | ## Read Module Manifest 21 | $ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName -ErrorAction Stop 22 | 23 | ## Output moduleName Azure Pipelines 24 | $env:moduleName = $ModuleManifestFileInfo.BaseName 25 | Write-Host ('##vso[task.setvariable variable=moduleName;isOutput=true]{0}' -f $env:moduleName) 26 | Write-Host ('##[debug] {0} = {1}' -f 'moduleName', $env:moduleName) 27 | 28 | ## Output moduleVersion Azure Pipelines 29 | $env:moduleVersion = $ModuleManifest['ModuleVersion'] 30 | Write-Host ('##vso[task.setvariable variable=moduleVersion;isOutput=true]{0}' -f $env:moduleVersion) 31 | Write-Host ('##[debug] {0} = {1}' -f 'moduleVersion', $env:moduleVersion) 32 | 33 | if ($TrimVersionDepth) { 34 | $env:moduleVersionTrimmed = $env:moduleVersion -replace ('(?<=^(.?[0-9]+){{{0},}})(.[0-9]+)+$' -f $TrimVersionDepth), '' 35 | Write-Host ('##vso[task.setvariable variable=moduleVersionTrimmed;isOutput=true]{0}' -f $env:moduleVersionTrimmed) 36 | Write-Host ('##[debug] {0} = {1}' -f 'moduleVersionTrimmed', $env:moduleVersionTrimmed) 37 | } 38 | 39 | ## Read Packages Configuration 40 | if ($PackagesConfigFileInfo.Exists) { 41 | $xmlPackagesConfig = New-Object xml 42 | $xmlPackagesConfig.Load($PackagesConfigFileInfo.FullName) 43 | 44 | foreach ($package in $xmlPackagesConfig.packages.package) { 45 | ## Output packageVersion Azure Pipelines 46 | Set-Variable ('env:{0}' -f $package.id) -Value $package.version 47 | Write-Host ('##vso[task.setvariable variable=version.{0};isOutput=true]{1}' -f $package.id, $package.version) 48 | Write-Host ('##[debug] version.{0} = {1}' -f $package.id, $package.version) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /build/Launch-PSModule.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | # Module to Launch 3 | [Parameter(Mandatory = $false)] 4 | [string] $ModuleManifestPath = '.\src\*.psd1', 5 | # ScriptBlock to Execute After Module Import 6 | [Parameter(Mandatory = $false)] 7 | [scriptblock] $PostImportScriptBlock, 8 | # Paths to PowerShell Executables 9 | [Parameter(Mandatory = $false)] 10 | [string[]] $PowerShellPaths = @( 11 | 'pwsh' 12 | #'powershell' 13 | #'D:\Software\PowerShell-6.2.4-win-x64\pwsh.exe' 14 | ), 15 | # Import Module into the same session 16 | [Parameter(Mandatory = $false)] 17 | [switch] $NoNewWindow #= $true 18 | ) 19 | 20 | ## Restore Module Dependencies 21 | $PSModuleCacheDirectory = &$PSScriptRoot\Restore-PSModuleDependencies.ps1 -ModuleManifestPath $ModuleManifestPath #-OutputDirectory $OutputDirectory.FullName 22 | 23 | ## Launch PSModule 24 | if ($NoNewWindow) { 25 | Import-Module $ModuleManifestPath -PassThru -Force 26 | if ($PostImportScriptBlock) { Invoke-Command -ScriptBlock $PostImportScriptBlock -NoNewScope } 27 | } 28 | else { 29 | [scriptblock] $ScriptBlock = { 30 | param ([string]$ModulePath, [string]$PSModuleCacheDirectory, [scriptblock]$PostImportScriptBlock) 31 | ## Reset PSModulePath environment variable to default value because starting powershell.exe from pwsh.exe (or vice versa) will inherit environment variables for the wrong version of PowerShell. 32 | $PSModulePathDefault = [System.Management.Automation.ModuleIntrinsics]::GetModulePath($null, [System.Environment]::GetEnvironmentVariable('PSMODULEPATH', [EnvironmentVariableTarget]::Machine), [System.Environment]::GetEnvironmentVariable('PSMODULEPATH', [EnvironmentVariableTarget]::User)) 33 | [Environment]::SetEnvironmentVariable("PSMODULEPATH", $PSModulePathDefault) 34 | ## Add PSModuleCacheDirectory to PSModulePath environment variable 35 | if (!$env:PSModulePath.Contains($PSModuleCacheDirectory)) { $env:PSModulePath += '{0}{1}' -f [IO.Path]::PathSeparator, $PSModuleCacheDirectory } 36 | ## Import Module and Execute Post-Import ScriptBlock 37 | Import-Module $ModulePath -PassThru 38 | Invoke-Command -ScriptBlock $PostImportScriptBlock -NoNewScope 39 | } 40 | $strScriptBlock = 'Invoke-Command -ScriptBlock {{ {0} }} -ArgumentList {1}, {2}, {{ {3} }}' -f $ScriptBlock, $ModuleManifestPath, $PSModuleCacheDirectory, $PostImportScriptBlock 41 | #$strScriptBlock = 'Import-Module {0} -PassThru' -f $ModuleManifestPath 42 | 43 | foreach ($Path in $PowerShellPaths) { 44 | if ($Path -eq 'wsl') { 45 | Start-Process $Path -ArgumentList ('pwsh' , '-NoExit', '-NoProfile', '-EncodedCommand', [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($strScriptBlock))) 46 | } 47 | else { 48 | Start-Process $Path -ArgumentList ('-NoExit', '-NoProfile', '-EncodedCommand', [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($strScriptBlock))) 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /build/Merge-PSModuleNestedModuleScripts.ps1: -------------------------------------------------------------------------------- 1 | param 2 | ( 3 | # Path to Module Manifest 4 | [Parameter(Mandatory = $false)] 5 | [string] $ModuleManifestPath = ".\release\*\*.*.*", 6 | # 7 | [Parameter(Mandatory = $false)] 8 | [string] $OutputModulePath, 9 | # 10 | [Parameter(Mandatory = $false)] 11 | [switch] $MergeWithRootModule, 12 | # 13 | [Parameter(Mandatory = $false)] 14 | [switch] $RemoveNestedModuleScriptFiles 15 | ) 16 | 17 | ## Initialize 18 | Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop 19 | 20 | [System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultFilename "*.psd1" -ErrorAction Stop 21 | #[System.IO.DirectoryInfo] $ModuleSourceDirectoryInfo = $ModuleManifestFileInfo.Directory 22 | #[System.IO.DirectoryInfo] $ModuleOutputDirectoryInfo = $OutputModuleFileInfo.Directory 23 | 24 | ## Read Module Manifest 25 | $ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName 26 | 27 | if ($OutputModulePath) { 28 | [System.IO.FileInfo] $OutputModuleFileInfo = Get-PathInfo $OutputModulePath -InputPathType File -DefaultFilename "$($ModuleManifestFileInfo.BaseName).psm1" -ErrorAction SilentlyContinue 29 | } 30 | else { 31 | [System.IO.FileInfo] $OutputModuleFileInfo = Get-PathInfo $ModuleManifest['RootModule'] -InputPathType File -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction SilentlyContinue 32 | if (!$PSBoundParameters.ContainsKey('MergeWithRootModule')) { $MergeWithRootModule = $true } 33 | } 34 | 35 | if ($OutputModuleFileInfo.Extension -eq ".psm1") { 36 | 37 | [System.IO.FileInfo] $RootModuleFileInfo = Get-PathInfo $ModuleManifest['RootModule'] -InputPathType File -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction SilentlyContinue 38 | [System.IO.FileInfo[]] $NestedModulesFileInfo = $ModuleManifest['NestedModules'] | Get-PathInfo -InputPathType File -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction SilentlyContinue 39 | [System.IO.FileInfo[]] $ScriptsToProcessFileInfo = $ModuleManifest['ScriptsToProcess'] | Get-PathInfo -InputPathType File -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction SilentlyContinue 40 | 41 | if ($MergeWithRootModule) { 42 | ## Split module parameters from the rest of the module content 43 | [string] $RootModuleParameters = $null 44 | [string] $RootModuleContent = $null 45 | if ($RootModuleFileInfo.Extension -eq ".psm1" -and (Get-Content $RootModuleFileInfo.FullName -Raw) -match "(?s)^(.*\n?\s*param\s*[(](?:[^()]|(?'Nested'[(])|(?'-Nested'[)]))*[)]\s*)?(.*)$") { 46 | $RootModuleParameters = $Matches[1] 47 | $RootModuleContent = $Matches[2] 48 | } 49 | 50 | $NestedModuleRegion = "#region NestedModules Script(s)`r`n" 51 | 52 | $RootModuleParameters, $NestedModuleRegion | Set-Content $OutputModuleFileInfo.FullName -Encoding utf8BOM 53 | } 54 | 55 | ## Add Nested Module Scripts 56 | $NestedModulesFileInfo | Where-Object Extension -EQ '.ps1' | ForEach-Object { "#region $($_.Name)`r`n`r`n$(Get-Content $_ -Raw)`r`n#endregion`r`n" } | Add-Content $OutputModuleFileInfo.FullName -Encoding utf8BOM 57 | 58 | if ($MergeWithRootModule) { 59 | function Join-ModuleMembers ([string[]]$Members) { 60 | if ($Members.Count -gt 0) { 61 | return "'{0}'" -f ($Members -join "','") 62 | } 63 | else { return "" } 64 | } 65 | 66 | ## Add remainder of root module content 67 | $NestedModuleEndRegion = "#endregion`r`n" 68 | $ExportModuleMember += "Export-ModuleMember -Function @({0}) -Cmdlet @({1}) -Variable @({2}) -Alias @({3})" -f (Join-ModuleMembers $ModuleManifest['FunctionsToExport']), (Join-ModuleMembers $ModuleManifest['CmdletsToExport']), (Join-ModuleMembers $ModuleManifest['VariablesToExport']), (Join-ModuleMembers $ModuleManifest['AliasesToExport']) 69 | 70 | $NestedModuleEndRegion, $RootModuleContent, $ExportModuleMember | Add-Content $OutputModuleFileInfo.FullName -Encoding utf8BOM 71 | } 72 | 73 | if ($RemoveNestedModuleScriptFiles) { 74 | ## Remove Nested Module Scripts 75 | $NestedModulesFileInfo | Where-Object Extension -EQ '.ps1' | Where-Object { !$ScriptsToProcessFileInfo -or $_.FullName -notin $ScriptsToProcessFileInfo.FullName } | Remove-Item 76 | 77 | ## Remove Empty Directories 78 | Get-ChildItem $ModuleManifestFileInfo.DirectoryName -Recurse -Directory | Where-Object { !(Get-ChildItem $_.FullName -Recurse -File) } | Remove-Item -Recurse 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /build/PesterConfiguration.Baseline.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Run = @{ 3 | #PassThru = $true 4 | } 5 | Filter = @{ 6 | Tag = 'Common' 7 | ExcludeTag = 'IntegrationTest' 8 | } 9 | CodeCoverage = @{ 10 | Enabled = $true 11 | OutputFormat = 'JaCoCo' 12 | OutputPath = '.\build\TestResults\CodeCoverage.xml' 13 | RecursePaths = $false 14 | } 15 | TestResult = @{ 16 | Enabled = $true 17 | OutputFormat = 'NUnitXML' 18 | OutputPath = '.\build\TestResults\TestResult.xml' 19 | } 20 | Output = @{ 21 | #Verbosity = 'Detailed' 22 | } 23 | } -------------------------------------------------------------------------------- /build/PesterConfiguration.CD.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Run = @{ 3 | PassThru = $true 4 | } 5 | Filter = @{ 6 | #Tag = '' 7 | #ExcludeTag = 'IntegrationTest' 8 | } 9 | CodeCoverage = @{ 10 | Enabled = $true 11 | OutputFormat = 'JaCoCo' 12 | OutputPath = '.\build\TestResults\CodeCoverage.xml' 13 | RecursePaths = $false 14 | } 15 | TestResult = @{ 16 | Enabled = $true 17 | OutputFormat = 'NUnitXML' 18 | OutputPath = '.\build\TestResults\TestResult.xml' 19 | } 20 | Output = @{ 21 | #Verbosity = 'Detailed' 22 | } 23 | } -------------------------------------------------------------------------------- /build/PesterConfiguration.CI.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Run = @{ 3 | PassThru = $true 4 | } 5 | Filter = @{ 6 | #Tag = '' 7 | ExcludeTag = 'Deferrable', 'IntegrationTest', 'Slow' 8 | } 9 | CodeCoverage = @{ 10 | Enabled = $true 11 | OutputFormat = 'JaCoCo' 12 | OutputPath = '.\build\TestResults\CodeCoverage.xml' 13 | RecursePaths = $false 14 | } 15 | TestResult = @{ 16 | Enabled = $true 17 | OutputFormat = 'NUnitXML' 18 | OutputPath = '.\build\TestResults\TestResult.xml' 19 | } 20 | Output = @{ 21 | #Verbosity = 'Detailed' 22 | } 23 | } -------------------------------------------------------------------------------- /build/PesterConfiguration.Debug.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Run = @{ 3 | PassThru = $true 4 | } 5 | Filter = @{ 6 | #Tag = 'Common' 7 | ExcludeTag = 'IntegrationTest' 8 | } 9 | Debug = @{ 10 | ShowFullErrors = $false 11 | ShowNavigationMarkers = $false 12 | WriteDebugMessages = $false 13 | } 14 | Output = @{ 15 | Verbosity = 'Detailed' 16 | } 17 | } -------------------------------------------------------------------------------- /build/PesterConfiguration.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Run = @{ 3 | PassThru = $true 4 | } 5 | Filter = @{ 6 | #Tag = 'Debug' 7 | #ExcludeTag = 'IntegrationTest' 8 | } 9 | CodeCoverage = @{ 10 | Enabled = $true 11 | OutputFormat = 'JaCoCo' 12 | OutputPath = '.\build\TestResults\CodeCoverage.xml' 13 | RecursePaths = $false 14 | } 15 | TestResult = @{ 16 | Enabled = $false 17 | OutputFormat = 'NUnitXML' 18 | OutputPath = '.\build\TestResults\TestResult.xml' 19 | } 20 | Output = @{ 21 | Verbosity = 'Detailed' 22 | } 23 | } -------------------------------------------------------------------------------- /build/PesterCustomAssertions.psm1: -------------------------------------------------------------------------------- 1 | #Requires -Module Pester 2 | 3 | ## This module could probably use some clean up and performance optimization 4 | 5 | function Format-Collection ($Value, [switch]$Pretty) { 6 | $Limit = 10 7 | $separator = ', ' 8 | if ($Pretty) { 9 | $separator = ",`n" 10 | } 11 | $count = $Value.Count 12 | $trimmed = $count -gt $Limit 13 | 14 | $formattedCollection = @() 15 | for ($i = 0; $i -lt [System.Math]::Min($count, $Limit); $i++) { 16 | $formattedValue = Format-Nicely -Value $Value[$i] -Pretty:$Pretty 17 | $formattedCollection += $formattedValue 18 | } 19 | 20 | '@(' + ($formattedCollection -join $separator) + $(if ($trimmed) { ", ...$($count - $limit) more" }) + ')' 21 | } 22 | 23 | function Format-Object ($Value, $Property, [switch]$Pretty) { 24 | if ($null -eq $Property) { 25 | $Property = $Value.PSObject.Properties | & $SafeCommands['Select-Object'] -ExpandProperty Name 26 | } 27 | $valueType = Get-ShortType $Value 28 | $valueFormatted = ([string]([PSObject]$Value | & $SafeCommands['Select-Object'] -Property $Property)) 29 | 30 | if ($Pretty) { 31 | $margin = " " 32 | $valueFormatted = $valueFormatted ` 33 | -replace '^@{', "@{`n$margin" ` 34 | -replace '; ', ";`n$margin" ` 35 | -replace '}$', "`n}" ` 36 | 37 | } 38 | 39 | $valueFormatted -replace "^@", $valueType 40 | } 41 | 42 | function Format-Null { 43 | '$null' 44 | } 45 | 46 | function Format-String ($Value) { 47 | if ('' -eq $Value) { 48 | return '' 49 | } 50 | 51 | "'$Value'" 52 | } 53 | 54 | function Format-Date ($Value) { 55 | $Value.ToString('o') 56 | } 57 | 58 | function Format-Boolean ($Value) { 59 | '$' + $Value.ToString().ToLower() 60 | } 61 | 62 | function Format-ScriptBlock ($Value) { 63 | '{' + $Value + '}' 64 | } 65 | 66 | function Format-Number ($Value) { 67 | [string]$Value 68 | } 69 | 70 | function Format-Hashtable ($Value) { 71 | $head = '@{' 72 | $tail = '}' 73 | 74 | $entries = $Value.Keys | & $SafeCommands['Sort-Object'] | & $SafeCommands['ForEach-Object'] { 75 | $formattedValue = Format-Nicely $Value.$_ 76 | "$_=$formattedValue" } 77 | 78 | $head + ( $entries -join '; ') + $tail 79 | } 80 | 81 | function Format-Dictionary ($Value) { 82 | $head = 'Dictionary{' 83 | $tail = '}' 84 | 85 | $entries = $Value.Keys | & $SafeCommands['Sort-Object'] | & $SafeCommands['ForEach-Object'] { 86 | $formattedValue = Format-Nicely $Value.$_ 87 | "$_=$formattedValue" } 88 | 89 | $head + ( $entries -join '; ') + $tail 90 | } 91 | 92 | function Format-Nicely ($Value, [switch]$Pretty) { 93 | if ($null -eq $Value) { 94 | return Format-Null -Value $Value 95 | } 96 | 97 | if ($Value -is [bool]) { 98 | return Format-Boolean -Value $Value 99 | } 100 | 101 | if ($Value -is [string]) { 102 | return Format-String -Value $Value 103 | } 104 | 105 | if ($Value -is [DateTime]) { 106 | return Format-Date -Value $Value 107 | } 108 | 109 | if ($value -is [Type]) { 110 | return '[' + (Format-Type -Value $Value) + ']' 111 | } 112 | 113 | if (Is-DecimalNumber -Value $Value) { 114 | return Format-Number -Value $Value 115 | } 116 | 117 | if (Is-ScriptBlock -Value $Value) { 118 | return Format-ScriptBlock -Value $Value 119 | } 120 | 121 | if (Is-Value -Value $Value) { 122 | return $Value 123 | } 124 | 125 | if (Is-Hashtable -Value $Value) { 126 | # no advanced formatting of objects in the first version, till I balance it 127 | return [string]$Value 128 | #return Format-Hashtable -Value $Value 129 | } 130 | 131 | if (Is-Dictionary -Value $Value) { 132 | # no advanced formatting of objects in the first version, till I balance it 133 | return [string]$Value 134 | #return Format-Dictionary -Value $Value 135 | } 136 | 137 | if (Is-Collection -Value $Value) { 138 | return Format-Collection -Value $Value -Pretty:$Pretty 139 | } 140 | 141 | # no advanced formatting of objects in the first version, till I balance it 142 | return [string]$Value 143 | # Format-Object -Value $Value -Property (Get-DisplayProperty $Value) -Pretty:$Pretty 144 | } 145 | 146 | function Sort-Property ($InputObject, [string[]]$SignificantProperties, $Limit = 4) { 147 | 148 | $properties = @($InputObject.PSObject.Properties | 149 | & $SafeCommands['Where-Object'] { $_.Name -notlike "_*" } | 150 | & $SafeCommands['Select-Object'] -expand Name | 151 | & $SafeCommands['Sort-Object']) 152 | $significant = @() 153 | $rest = @() 154 | foreach ($p in $properties) { 155 | if ($significantProperties -contains $p) { 156 | $significant += $p 157 | } 158 | else { 159 | $rest += $p 160 | } 161 | } 162 | 163 | #todo: I am assuming id, name properties, so I am just sorting the selected ones by name. 164 | (@($significant | & $SafeCommands['Sort-Object']) + $rest) | & $SafeCommands['Select-Object'] -First $Limit 165 | 166 | } 167 | 168 | function Get-DisplayProperty ($Value) { 169 | Sort-Property -InputObject $Value -SignificantProperties 'id', 'name' 170 | } 171 | 172 | function Get-ShortType ($Value) { 173 | if ($null -ne $value) { 174 | $type = Format-Type $Value.GetType() 175 | # PSCustomObject serializes to the whole type name on normal PS but to 176 | # just PSCustomObject on PS Core 177 | 178 | $type ` 179 | -replace "^System\." ` 180 | -replace "^Management\.Automation\.PSCustomObject$", "PSObject" ` 181 | -replace "^PSCustomObject$", "PSObject" ` 182 | -replace "^Object\[\]$", "collection" ` 183 | 184 | } 185 | else { 186 | Format-Type $null 187 | } 188 | } 189 | 190 | function Format-Type ([Type]$Value) { 191 | if ($null -eq $Value) { 192 | return '' 193 | } 194 | 195 | [string]$Value 196 | } 197 | 198 | function Join-And ($Items, $Threshold = 2) { 199 | 200 | if ($null -eq $items -or $items.count -lt $Threshold) { 201 | $items -join ', ' 202 | } 203 | else { 204 | $c = $items.count 205 | ($items[0..($c - 2)] -join ', ') + ' and ' + $items[-1] 206 | } 207 | } 208 | 209 | function Add-SpaceToNonEmptyString ([string]$Value) { 210 | if ($Value) { 211 | " $Value" 212 | } 213 | } 214 | 215 | function Get-DoValuesMatch($ActualValue, $ExpectedValue) { 216 | #user did not specify any message filter, so any message matches 217 | if ($null -eq $ExpectedValue) { 218 | return $true 219 | } 220 | 221 | return $ActualValue.ToString() -like $ExpectedValue 222 | } 223 | 224 | function Get-ExceptionLineInfo($info) { 225 | # $info.PositionMessage has a leading blank line that we need to account for in PowerShell 2.0 226 | $positionMessage = $info.PositionMessage -split '\r?\n' -match '\S' -join [System.Environment]::NewLine 227 | return ($positionMessage -replace "^At ", "from ") 228 | } 229 | 230 | function Format-Because ([string] $Because) { 231 | if ($null -eq $Because) { 232 | return 233 | } 234 | 235 | $bcs = $Because.Trim() 236 | if ([string]::IsNullOrEmpty($bcs)) { 237 | return 238 | } 239 | 240 | " because $($bcs -replace 'because\s')," 241 | } 242 | 243 | 244 | function Should-WriteError ([scriptblock] $ActualValue, [string] $ExpectedMessage, [string] $ErrorId, [type] $ExceptionType, [switch] $Negate, [string] $Because, [switch] $PassThruError, [switch] $PassThruOutput) { 245 | 246 | if ($null -eq $ActualValue) { 247 | throw [ArgumentNullException] "Input is not a ScriptBlock. Input to '-Throw' and '-Not -Throw' must be enclosed in curly braces." 248 | } 249 | 250 | try { 251 | $output = Invoke-Command $ActualValue -ErrorVariable actualErrors 252 | } 253 | catch {} 254 | 255 | if (!$Negate -and @($actualErrors).Count -eq 0) { 256 | # this is for Should -Not -Throw. Once *any* exception was thrown we should fail the assertion 257 | # there is no point in filtering the exception, because there should be none 258 | $failureMessage = "Expected error,$(Format-Because $Because) but no error was returned." 259 | return [PSCustomObject] @{ 260 | Succeeded = $false 261 | FailureMessage = $failureMessage 262 | } 263 | } 264 | 265 | # the rest is for Should -Throw, we must fail the assertion when no exception is thrown 266 | # or when the exception does not match our filter 267 | 268 | function Join-And ($Items, $Threshold = 2) { 269 | 270 | if ($null -eq $items -or $items.count -lt $Threshold) { 271 | $items -join ', ' 272 | } 273 | else { 274 | $c = $items.count 275 | ($items[0..($c - 2)] -join ', ') + ' and ' + $items[-1] 276 | } 277 | } 278 | 279 | function Add-SpaceToNonEmptyString ([string]$Value) { 280 | if ($Value) { 281 | " $Value" 282 | } 283 | } 284 | 285 | $filters = @() 286 | 287 | $filterOnExceptionType = $null -ne $ExceptionType 288 | if ($filterOnExceptionType) { 289 | $filters += "of type $(Format-Nicely $ExceptionType)" 290 | } 291 | 292 | $filterOnMessage = -not [string]::IsNullOrWhitespace($ExpectedMessage) 293 | if ($filterOnMessage) { 294 | $filters += "with message $(Format-Nicely $ExpectedMessage)" 295 | } 296 | 297 | $filterOnId = -not [string]::IsNullOrWhitespace($ErrorId) 298 | if ($filterOnId) { 299 | $filters += "with FullyQualifiedErrorId $(Format-Nicely $ErrorId)" 300 | } 301 | 302 | $buts = @() 303 | $match = @() 304 | foreach ($actualError in $actualErrors) { 305 | if ($actualError -is [System.Management.Automation.ErrorRecord]) { 306 | 307 | $matchOnExceptionType = !$filterOnExceptionType -or $actualError.Exception -is $ExceptionType 308 | $matchOnMessage = !$filterOnMessage -or (Get-DoValuesMatch $actualError.Exception.Message $ExpectedMessage) 309 | $matchOnId = !$filterOnId -or (Get-DoValuesMatch $actualError.FullyQualifiedErrorId $ErrorId) 310 | if ($matchOnExceptionType -and $matchOnMessage -and $matchOnId) { 311 | $match += $actualError 312 | #break 313 | } 314 | } 315 | } 316 | 317 | if ($match) { 318 | $actualExceptionLine = (Get-ExceptionLineInfo $_.InvocationInfo) -replace [System.Environment]::NewLine, "$([System.Environment]::NewLine) " 319 | if ($Negate) { $buts += "matching error was returned. $actualExceptionLine" } 320 | } 321 | elseif (!$Negate) { 322 | $buts += "no matching error was returned" 323 | } 324 | 325 | $expected = '' 326 | if ($Negate) { $expected = ' no' } 327 | 328 | 329 | if ($buts.Count -ne 0) { 330 | $filter = Add-SpaceToNonEmptyString ( Join-And $filters -Threshold 3 ) 331 | $but = Join-And $buts 332 | $failureMessage = "Expected$expected error$filter to be returned,$(Format-Because $Because) but $but.".Trim() 333 | 334 | return [PSCustomObject] @{ 335 | Succeeded = $false 336 | FailureMessage = $failureMessage 337 | } 338 | } 339 | 340 | $result = [PSCustomObject] @{ 341 | Succeeded = $true 342 | } 343 | 344 | if ($PassThruError -or $PassThruOutput) { 345 | [array] $data = @() 346 | 347 | if ($PassThruError) { 348 | $data += $match 349 | } 350 | 351 | if ($PassThruOutput) { 352 | $data += $output 353 | } 354 | 355 | $result | Add-Member -MemberType NoteProperty -Name 'Data' -Value $data 356 | } 357 | 358 | return $result 359 | } 360 | 361 | 362 | Add-ShouldOperator -Name WriteError -InternalName 'Should-WriteError' -Test ${function:Should-WriteError} -Alias 'Error' 363 | -------------------------------------------------------------------------------- /build/Publish-PSModule.ps1: -------------------------------------------------------------------------------- 1 | #Requires -Version 7.0 2 | param 3 | ( 4 | # Path to Module Manifest 5 | [Parameter(Mandatory = $false)] 6 | [string] $ModuleManifestPath = ".\release\*\*.*.*\*.psd1", 7 | # Repository for PowerShell Gallery 8 | [Parameter(Mandatory = $false)] 9 | [string] $RepositorySourceLocation = 'https://www.powershellgallery.com/api/v2', 10 | # API Key for PowerShell Gallery 11 | [Parameter(Mandatory = $true)] 12 | [securestring] $NuGetApiKey, 13 | # Unlist from PowerShell Gallery 14 | [Parameter(Mandatory = $false)] 15 | [switch] $Unlist 16 | ) 17 | 18 | ## Initialize 19 | Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop 20 | 21 | [System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultFilename "*.psd1" -ErrorAction Stop | Select-Object -Last 1 22 | 23 | ## Read Module Manifest 24 | $ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName 25 | 26 | ## Install Module Dependencies 27 | foreach ($Module in $ModuleManifest['RequiredModules']) { 28 | if ($Module -is [hashtable]) { $ModuleName = $Module.ModuleName } 29 | else { $ModuleName = $Module } 30 | if ($ModuleName -notin $ModuleManifest.PrivateData.PSData['ExternalModuleDependencies'] -and !(Get-Module $ModuleName -ListAvailable)) { 31 | Install-Module $ModuleName -Force -SkipPublisherCheck -Repository PSGallery -AcceptLicense 32 | } 33 | } 34 | 35 | ## Publish 36 | $PSRepositoryAll = Get-PSRepository 37 | $PSRepository = $PSRepositoryAll | Where-Object SourceLocation -Like "$RepositorySourceLocation*" 38 | if (!$PSRepository) { 39 | try { 40 | [string] $RepositoryName = New-Guid 41 | Register-PSRepository $RepositoryName -SourceLocation $RepositorySourceLocation 42 | $PSRepository = Get-PSRepository $RepositoryName 43 | Publish-Module -Path $ModuleManifestFileInfo.DirectoryName -NuGetApiKey (ConvertFrom-SecureString $NuGetApiKey -AsPlainText) -Repository $PSRepository.Name 44 | } 45 | finally { 46 | Unregister-PSRepository $RepositoryName 47 | } 48 | } 49 | else { 50 | Write-Verbose ('Publishing Module Path [{0}]' -f $ModuleManifestFileInfo.DirectoryName) 51 | Publish-Module -Path $ModuleManifestFileInfo.DirectoryName -NuGetApiKey (ConvertFrom-SecureString $NuGetApiKey -AsPlainText) -Repository $PSRepository.Name 52 | } 53 | 54 | ## Unlist the Package 55 | if ($Unlist) { 56 | if ($ModuleManifest.PrivateData.PSData['Prerelease']) { 57 | Invoke-RestMethod -Method Delete -Uri ("{0}/{1}/{2}-{3}" -f $PSRepository.PublishLocation, $ModuleManifestFileInfo.BaseName, $ModuleManifest['ModuleVersion'], $ModuleManifest.PrivateData.PSData['Prerelease']) -Headers @{ 'X-NuGet-ApiKey' = ConvertFrom-SecureString $NuGetApiKey -AsPlainText } 58 | } 59 | else { 60 | Invoke-RestMethod -Method Delete -Uri ("{0}/{1}/{2}" -f $PSRepository.PublishLocation, $ModuleManifestFileInfo.BaseName, $ModuleManifest['ModuleVersion']) -Headers @{ 'X-NuGet-ApiKey' = ConvertFrom-SecureString $NuGetApiKey -AsPlainText } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /build/Restore-NugetPackages.ps1: -------------------------------------------------------------------------------- 1 | param 2 | ( 3 | # Directory used to base all relative paths 4 | [Parameter(Mandatory = $false)] 5 | [string] $BaseDirectory = "..\", 6 | # 7 | [Parameter(Mandatory = $false)] 8 | [string] $PackagesConfigPath = ".\packages.config", 9 | # 10 | [Parameter(Mandatory = $false)] 11 | [string] $NuGetConfigPath, 12 | # 13 | [Parameter(Mandatory = $false)] 14 | [string] $OutputDirectory, 15 | # 16 | [Parameter(Mandatory = $false)] 17 | [string] $NuGetPath = ".\build", 18 | # 19 | [Parameter(Mandatory = $false)] 20 | [uri] $NuGetUri = 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' 21 | ) 22 | 23 | ## Initialize 24 | Remove-Module CommonFunctions -ErrorAction SilentlyContinue 25 | Import-Module $PSScriptRoot\CommonFunctions.psm1 -DisableNameChecking 26 | 27 | [System.IO.DirectoryInfo] $BaseDirectoryInfo = Get-PathInfo $BaseDirectory -InputPathType Directory -ErrorAction Stop 28 | [System.IO.FileInfo] $PackagesConfigFileInfo = Get-PathInfo $PackagesConfigPath -DefaultDirectory $BaseDirectoryInfo.FullName -DefaultFilename "packages.config" -ErrorAction Stop 29 | [System.IO.FileInfo] $NuGetConfigFileInfo = Get-PathInfo $NuGetConfigPath -DefaultDirectory $BaseDirectoryInfo.FullName -DefaultFilename "NuGet.config" -SkipEmptyPaths 30 | [System.IO.DirectoryInfo] $OutputDirectoryInfo = Get-PathInfo $OutputDirectory -InputPathType Directory -DefaultDirectory $BaseDirectoryInfo.FullName -SkipEmptyPaths -ErrorAction SilentlyContinue 31 | [System.IO.FileInfo] $NuGetFileInfo = Get-PathInfo $NuGetPath -DefaultDirectory $BaseDirectoryInfo.FullName -DefaultFilename "nuget.exe" -ErrorAction SilentlyContinue 32 | #Set-Alias nuget -Value $itemNuGetPath.FullName 33 | 34 | ## Download NuGet 35 | if (!$NuGetFileInfo.Exists) { 36 | Invoke-WebRequest $NuGetUri.AbsoluteUri -UseBasicParsing -OutFile $NuGetFileInfo.FullName 37 | } 38 | 39 | ## Run NuGet 40 | $argsNuget = New-Object System.Collections.Generic.List[string] 41 | $argsNuget.Add('restore') 42 | $argsNuget.Add($PackagesConfigFileInfo.FullName) 43 | if ($VerbosePreference -eq 'Continue') { 44 | $argsNuget.Add('-Verbosity') 45 | $argsNuget.Add('Detailed') 46 | } 47 | if ($NuGetConfigFileInfo) { 48 | $argsNuget.Add('-ConfigFile') 49 | $argsNuget.Add($NuGetConfigFileInfo.FullName) 50 | } 51 | if ($OutputDirectoryInfo) { 52 | $argsNuget.Add('-OutputDirectory') 53 | $argsNuget.Add($OutputDirectoryInfo.FullName) 54 | } 55 | 56 | Use-StartProcess $NuGetFileInfo.FullName -ArgumentList $argsNuget 57 | -------------------------------------------------------------------------------- /build/Restore-PSModuleDependencies.ps1: -------------------------------------------------------------------------------- 1 | param 2 | ( 3 | # Path to Module Manifest 4 | [Parameter(Mandatory = $false)] 5 | [string] $ModuleManifestPath = ".\src\*.psd1", 6 | # 7 | [Parameter(Mandatory = $false)] 8 | [string] $PSModuleCacheDirectory = ".\build\TestResults\PSModuleCache", 9 | # 10 | [Parameter(Mandatory = $false)] 11 | [string[]] $Repository = "PSGallery", 12 | # 13 | [Parameter(Mandatory = $false)] 14 | [switch] $SkipExternalModuleDependencies 15 | ) 16 | 17 | ## Initialize 18 | Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop 19 | #$PSModulePathBackup = $env:PSModulePath 20 | 21 | [System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultFilename "*.psd1" -ErrorAction Stop | Select-Object -Last 1 22 | [System.IO.DirectoryInfo] $PSModuleCacheDirectoryInfo = Get-PathInfo $PSModuleCacheDirectory -InputPathType Directory -SkipEmptyPaths -ErrorAction SilentlyContinue 23 | 24 | ## Read Module Manifest 25 | $ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName 26 | 27 | ## Restore Nuget Packages 28 | #.\build\Restore-NugetPackages.ps1 -BaseDirectory ".\" -Verbose:$false 29 | 30 | ## Create directory 31 | if ($ModuleManifest['RequiredModules']) { 32 | Assert-DirectoryExists $PSModuleCacheDirectoryInfo.FullName -ErrorAction Stop | Out-Null 33 | if (!$env:PSModulePath.Contains($PSModuleCacheDirectoryInfo.FullName)) { $env:PSModulePath += '{0}{1}' -f [IO.Path]::PathSeparator, $PSModuleCacheDirectoryInfo.FullName } 34 | } 35 | 36 | ## Save Module Dependencies 37 | foreach ($Module in $ModuleManifest['RequiredModules']) { 38 | if (!(Get-Module -FullyQualifiedName $Module -ListAvailable -ErrorAction SilentlyContinue)) { 39 | $paramSaveModule = @{} 40 | if ($Module -is [hashtable]) { 41 | $paramSaveModule['Name'] = $Module.ModuleName 42 | if ($Module.ContainsKey('ModuleVersion')) { $paramSaveModule['MinimumVersion'] = $Module.ModuleVersion } 43 | elseif ($Module.ContainsKey('RequiredVersion')) { $paramSaveModule['RequiredVersion'] = $Module.RequiredVersion } 44 | } 45 | else { $paramSaveModule['Name'] = $Module } 46 | 47 | if (!$SkipExternalModuleDependencies -or $paramSaveModule['Name'] -notin $ModuleManifest.PrivateData.PSData['ExternalModuleDependencies']) { 48 | Save-Module -Repository $Repository -Path $PSModuleCacheDirectoryInfo.FullName @paramSaveModule 49 | } 50 | } 51 | } 52 | 53 | #$env:PSModulePath = $PSModulePathBackup 54 | 55 | return $PSModuleCacheDirectoryInfo.FullName 56 | -------------------------------------------------------------------------------- /build/Sign-PSModule.ps1: -------------------------------------------------------------------------------- 1 | param 2 | ( 3 | # Path to Module Manifest 4 | [Parameter(Mandatory = $false)] 5 | [string] $ModuleManifestPath = ".\release\*\*.*.*", 6 | # Specifies the certificate that will be used to sign the script or file. 7 | [Parameter(Mandatory = $false)] 8 | [object] $SigningCertificate = (Get-ChildItem Cert:\CurrentUser\My\E7413D745138A6DC584530AECE27CEFDDA9D9CD6 -CodeSigningCert), 9 | # Uses the specified time stamp server to add a time stamp to the signature. 10 | [Parameter(Mandatory = $false)] 11 | [string] $TimestampServer = 'http://timestamp.digicert.com', 12 | # Generate and sign catalog file 13 | [Parameter(Mandatory = $false)] 14 | [switch] $AddFileCatalog 15 | ) 16 | 17 | ## Initialize 18 | Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop 19 | 20 | [System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultFilename "*.psd1" | Select-Object -Last 1 21 | 22 | ## Parse Signing Certificate 23 | if ($SigningCertificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2]) { } 24 | elseif ($SigningCertificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2Collection]) { $SigningCertificate = $SigningCertificate[-1] } 25 | else { $SigningCertificate = Get-X509Certificate $SigningCertificate -EndEntityCertificateOnly } 26 | 27 | ## Read Module Manifest 28 | $ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName 29 | 30 | $FileList = $ModuleManifest['FileList'] -like "*.ps*1*" 31 | for ($i = 0; $i -lt $FileList.Count; $i++) { 32 | $FileList[$i] = Join-Path $ModuleManifestFileInfo.DirectoryName $FileList[$i] -Resolve 33 | } 34 | 35 | #$FileList = Get-ChildItem $ModuleManifestFileInfo.DirectoryName -Filter "*.ps*1" -Recurse 36 | 37 | ## Sign PowerShell Files 38 | Set-AuthenticodeSignature $FileList -Certificate $SigningCertificate -HashAlgorithm SHA256 -IncludeChain NotRoot -TimestampServer $TimestampServer 39 | 40 | ## Generate and Sign File Catalog 41 | if ($AddFileCatalog) { 42 | $FileCatalogPath = Join-Path $ModuleManifestFileInfo.Directory ('{0}.cat' -f $ModuleManifestFileInfo.Name) 43 | $FileCatalogPath = New-FileCatalog $FileCatalogPath -Path $ModuleManifestFileInfo.Directory -CatalogVersion 2.0 44 | Set-AuthenticodeSignature $FileCatalog.FullName -Certificate $SigningCertificate -HashAlgorithm SHA256 -IncludeChain NotRoot -TimestampServer $TimestampServer 45 | } 46 | -------------------------------------------------------------------------------- /build/Test-PSModule.ps1: -------------------------------------------------------------------------------- 1 | param 2 | ( 3 | # 4 | [Parameter(Mandatory = $false)] 5 | [string] $ModuleManifestPath = ".\src\*.psd1", 6 | # 7 | [Parameter(Mandatory = $false)] 8 | [string] $PSModuleCacheDirectory = ".\build\TestResults\PSModuleCache", 9 | # 10 | [Parameter(Mandatory = $false)] 11 | [string] $PesterConfigurationPath = ".\build\PesterConfiguration.psd1", 12 | # 13 | [Parameter(Mandatory = $false)] 14 | [string] $TestResultPath, 15 | # 16 | [Parameter(Mandatory = $false)] 17 | [string] $CodeCoveragePath, 18 | # 19 | [Parameter(Mandatory = $false)] 20 | [string] $ModuleTestsDirectory = ".\tests" 21 | ) 22 | 23 | ## Initialize 24 | Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop 25 | 26 | [System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultFilename "*.psd1" -ErrorAction Stop | Select-Object -Last 1 27 | [System.IO.FileInfo] $TestResultFileInfo = Get-PathInfo $TestResultPath -DefaultFilename 'TestResult.xml' -ErrorAction Ignore 28 | [System.IO.FileInfo] $CodeCoverageFileInfo = Get-PathInfo $CodeCoveragePath -DefaultFilename 'CodeCoverage.xml' -ErrorAction Ignore 29 | [System.IO.DirectoryInfo] $PSModuleCacheDirectoryInfo = Get-PathInfo $PSModuleCacheDirectory -InputPathType Directory -SkipEmptyPaths -ErrorAction SilentlyContinue 30 | [System.IO.FileInfo] $PesterConfigurationFileInfo = Get-PathInfo $PesterConfigurationPath -DefaultFilename 'PesterConfiguration.psd1' -ErrorAction SilentlyContinue 31 | [System.IO.DirectoryInfo] $ModuleTestsDirectoryInfo = Get-PathInfo $ModuleTestsDirectory -InputPathType Directory -ErrorAction SilentlyContinue 32 | 33 | ## Restore Module Dependencies 34 | &$PSScriptRoot\Restore-PSModuleDependencies.ps1 -ModuleManifestPath $ModuleManifestPath -PSModuleCacheDirectory $PSModuleCacheDirectoryInfo.FullName | Out-Null 35 | 36 | Import-Module Pester -MinimumVersion 5.0.0 37 | #$PSModule = Import-Module $ModulePath -PassThru -Force 38 | 39 | $PesterConfiguration = New-PesterConfiguration (Import-PowerShellDataFile $PesterConfigurationFileInfo.FullName) 40 | $PesterConfiguration.Run.Container = New-PesterContainer -Path $ModuleTestsDirectoryInfo.FullName -Data @{ ModulePath = $ModuleManifestFileInfo.FullName } 41 | $PesterConfiguration.CodeCoverage.Path = Split-Path $ModuleManifestFileInfo.FullName -Parent 42 | if ($TestResultPath) { $PesterConfiguration.TestResult.OutputPath = $TestResultFileInfo.FullName } 43 | if ($CodeCoveragePath) { $PesterConfiguration.CodeCoverage.OutputPath = $CodeCoverageFileInfo.FullName } 44 | #$PesterConfiguration.CodeCoverage.OutputPath = [IO.Path]::ChangeExtension($PesterConfiguration.CodeCoverage.OutputPath.Value, "$($PSVersionTable.PSVersion).xml") 45 | #$PesterConfiguration.TestResult.OutputPath = [IO.Path]::ChangeExtension($PesterConfiguration.TestResult.OutputPath.Value, "$($PSVersionTable.PSVersion).xml") 46 | $PesterRun = Invoke-Pester -Configuration $PesterConfiguration 47 | $PesterRun 48 | 49 | ## Return SucceededWithIssues when running in ADO Pipeline and a test fails. 50 | if ($env:AGENT_ID -and $PesterRun -and $PesterRun.Result -ne 'Passed') { Write-Host '##vso[task.complete result=SucceededWithIssues;]FailedTest' } 51 | -------------------------------------------------------------------------------- /build/Update-PSModuleManifest.ps1: -------------------------------------------------------------------------------- 1 | param 2 | ( 3 | # Path to Module Manifest 4 | [Parameter(Mandatory = $false)] 5 | [string] $ModuleManifestPath = ".\release\*\*.*.*", 6 | # Specifies a unique identifier for the module. 7 | [Parameter(Mandatory = $false)] 8 | [string] $Guid, 9 | # Specifies the version of the module. 10 | [Parameter(Mandatory = $false)] 11 | [string] $ModuleVersion, 12 | # Indicates the module is prerelease. 13 | [Parameter(Mandatory = $false)] 14 | [string] $Prerelease, 15 | # Skip automatic additions to RequiredAssemblies from module file list. 16 | [Parameter(Mandatory = $false)] 17 | [switch] $SkipRequiredAssembliesDetection 18 | ) 19 | 20 | ## Initialize 21 | Import-Module "$PSScriptRoot\CommonFunctions.psm1" -Force -WarningAction SilentlyContinue -ErrorAction Stop 22 | [hashtable] $paramUpdateModuleManifest = @{ } 23 | 24 | [System.IO.FileInfo] $ModuleManifestFileInfo = Get-PathInfo $ModuleManifestPath -DefaultFilename "*.psd1" -ErrorAction Stop 25 | #[System.IO.DirectoryInfo] $ModuleOutputDirectoryInfo = $ModuleManifestFileInfo.Directory 26 | 27 | ## Read Module Manifest 28 | $ModuleManifest = Import-PowerShellDataFile $ModuleManifestFileInfo.FullName 29 | $paramUpdateModuleManifest['NestedModules'] = $ModuleManifest['NestedModules'] | Where-Object { $null -ne $_ -and (Get-PathInfo $_ -DefaultDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction Ignore).Exists } 30 | $paramUpdateModuleManifest['FunctionsToExport'] = $ModuleManifest['FunctionsToExport'] 31 | $paramUpdateModuleManifest['CmdletsToExport'] = $ModuleManifest['CmdletsToExport'] 32 | $paramUpdateModuleManifest['AliasesToExport'] = $ModuleManifest['AliasesToExport'] 33 | if ($ModuleManifest.PrivateData.PSData['Prerelease'] -eq 'source') { $paramUpdateModuleManifest['Prerelease'] = " " } 34 | 35 | ## Override from Parameters 36 | if ($Guid) { $paramUpdateModuleManifest['Guid'] = $Guid } 37 | if ($ModuleVersion) { $paramUpdateModuleManifest['ModuleVersion'] = $ModuleVersion } 38 | if ($Prerelease) { $paramUpdateModuleManifest['Prerelease'] = $Prerelease } 39 | 40 | ## Get Module Output FileList 41 | $ModuleFileListFileInfo = Get-ChildItem $ModuleManifestFileInfo.DirectoryName -Recurse -File 42 | $ModuleRequiredAssembliesFileInfo = $ModuleFileListFileInfo | Where-Object Extension -EQ '.dll' 43 | 44 | ## Get Paths Relative to Module Base Directory 45 | $ModuleFileList = Get-RelativePath $ModuleFileListFileInfo.FullName -WorkingDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction Stop 46 | $ModuleFileList = $ModuleFileList -replace '\\net45\\', '\!!!\' -replace '\\netcoreapp2.1\\', '\net45\' -replace '\\!!!\\', '\netcoreapp2.1\' # PowerShell Core fails to load assembly if net45 dll comes before netcoreapp2.1 dll in the FileList. 47 | $paramUpdateModuleManifest['FileList'] = $ModuleFileList 48 | 49 | ## Generate RequiredAssemblies list based on existing items and file list 50 | $paramUpdateModuleManifest['RequiredAssemblies'] = $ModuleManifest['RequiredAssemblies'] | Where-Object { $_ -notin $ModuleFileListFileInfo.Name } 51 | if (!$SkipRequiredAssembliesDetection -and $ModuleRequiredAssembliesFileInfo) { 52 | $ModuleRequiredAssemblies = Get-RelativePath $ModuleRequiredAssembliesFileInfo.FullName -WorkingDirectory $ModuleManifestFileInfo.DirectoryName -ErrorAction Stop 53 | $paramUpdateModuleManifest['RequiredAssemblies'] += $ModuleRequiredAssemblies 54 | } 55 | 56 | ## Clear Existing RequiredAssemblies, NestedModules, and FileList 57 | if ($paramUpdateModuleManifest.ContainsKey('RequiredAssemblies')) { 58 | if (!$paramUpdateModuleManifest['RequiredAssemblies']) { $paramUpdateModuleManifest.Remove('RequiredAssemblies') } 59 | (Get-Content $ModuleManifestFileInfo.FullName -Raw) -replace "(?s)(#\s*)?RequiredAssemblies\s*=\s*@\([^)]*\)", "# RequiredAssemblies = @()" | Set-Content $ModuleManifestFileInfo.FullName 60 | } 61 | if ($paramUpdateModuleManifest.ContainsKey('NestedModules') -and !$paramUpdateModuleManifest['NestedModules']) { 62 | $paramUpdateModuleManifest.Remove('NestedModules') 63 | (Get-Content $ModuleManifestFileInfo.FullName -Raw) -replace "(?s)(#\s*)?NestedModules\s*=\s*@\([^)]*\)", "# NestedModules = @()" | Set-Content $ModuleManifestFileInfo.FullName 64 | } 65 | if ($paramUpdateModuleManifest.ContainsKey('FileList')) { 66 | (Get-Content $ModuleManifestFileInfo.FullName -Raw) -replace "(?s)(#\s*)?FileList\s*=\s*@\([^)]*\)", "# FileList = @()" | Set-Content $ModuleManifestFileInfo.FullName 67 | } 68 | 69 | ## Install Module Dependencies 70 | foreach ($Module in $ModuleManifest['RequiredModules']) { 71 | if ($Module -is [hashtable]) { $ModuleName = $Module.ModuleName } 72 | else { $ModuleName = $Module } 73 | if ($ModuleName -notin $ModuleManifest.PrivateData.PSData['ExternalModuleDependencies'] -and !(Get-Module $ModuleName -ListAvailable)) { 74 | Install-Module $ModuleName -Force -SkipPublisherCheck -Repository PSGallery -AcceptLicense 75 | } 76 | } 77 | 78 | ## Update Module Manifest in Module Output Directory 79 | Update-ModuleManifest -Path $ModuleManifestFileInfo.FullName -ErrorAction Stop @paramUpdateModuleManifest 80 | -------------------------------------------------------------------------------- /build/azure-pipelines/azure-pipelines-cd.yml: -------------------------------------------------------------------------------- 1 | # Continuous Delivery Pipeline 2 | # https://aka.ms/yaml 3 | 4 | parameters: 5 | - name: vmImage 6 | displayName: 'Pool Image' 7 | type: string 8 | default: ubuntu-latest 9 | values: 10 | - windows-latest 11 | - ubuntu-latest 12 | - macOS-latest 13 | 14 | # trigger: 15 | # batch: true 16 | # branches: 17 | # include: 18 | # - main 19 | # - preview 20 | # paths: 21 | # include: 22 | # - src/* 23 | 24 | pr: none 25 | 26 | variables: 27 | ${{ if eq(variables['Build.SourceBranchName'], 'main') }}: 28 | prereleaseTag: 29 | ${{ if ne(variables['Build.SourceBranchName'], 'main') }}: 30 | prereleaseTag: 'preview' 31 | vmImage: '${{ parameters.vmImage }}' 32 | artifactModule: 'PSModule' 33 | artifactModuleSigned: 'PSModuleSigned' 34 | artifactModulePackage: 'PSModulePackage' 35 | #Packaging.EnableSBOMSigning: true 36 | 37 | pool: 38 | vmImage: $(vmImage) 39 | 40 | stages: 41 | - stage: Build 42 | jobs: 43 | - job: Prepare 44 | variables: 45 | skipComponentGovernanceDetection: true 46 | runCodesignValidationInjection: false 47 | steps: 48 | - task: PowerShell@2 49 | name: ModuleInfo 50 | displayName: 'Get Parameters for PowerShell Module' 51 | inputs: 52 | filePath: '$(System.DefaultWorkingDirectory)/build/Get-PSModuleInfo.ps1' 53 | arguments: '-ModuleManifestPath "$(Build.SourcesDirectory)/src/*.psd1" -TrimVersionDepth 2' 54 | pwsh: true 55 | 56 | - job: Build 57 | dependsOn: Prepare 58 | variables: 59 | runCodesignValidationInjection: false 60 | moduleNameSrc: '$[ dependencies.Prepare.outputs[''ModuleInfo.moduleName''] ]' 61 | moduleVersionSrc: '$[ coalesce(dependencies.Prepare.outputs[''ModuleInfo.moduleVersionTrimmed''], dependencies.Prepare.outputs[''ModuleInfo.moduleVersion'']) ]' 62 | moduleVersion.Revision: '$[ counter(variables[''moduleVersionSrc''], 0) ]' 63 | moduleVersion: '$[ coalesce(variables[''moduleVersionOverride''], format(''{0}.{1}'', variables[''moduleVersionSrc''], variables[''moduleVersion.Revision''])) ]' 64 | steps: 65 | - template: template-psmodule-build.yml 66 | parameters: 67 | moduleName: '$(moduleNameSrc)' 68 | moduleVersion: '$(moduleVersion)' 69 | prereleaseTag: '$(prereleaseTag)' 70 | GenerateManifest: true 71 | 72 | - job: Sign 73 | dependsOn: 74 | - Prepare 75 | - Build 76 | variables: 77 | skipComponentGovernanceDetection: false 78 | moduleName: '$[ dependencies.Prepare.outputs[''ModuleInfo.moduleName''] ]' 79 | pool: 80 | vmImage: 'windows-latest' 81 | steps: 82 | - download: current 83 | artifact: '$(artifactModule)' 84 | - template: template-psmodule-sign.yml 85 | parameters: 86 | moduleName: '$(moduleName)' 87 | EsrpCodeSigningServiceName: 'ESRP - MSFT Identity - Community Projects' 88 | 89 | 90 | 91 | - stage: Package 92 | displayName: 'Standalone Package' 93 | dependsOn: Build 94 | jobs: 95 | - job: Prepare 96 | variables: 97 | skipComponentGovernanceDetection: true 98 | runCodesignValidationInjection: false 99 | steps: 100 | - download: current 101 | artifact: '$(artifactModuleSigned)' 102 | - task: PowerShell@2 103 | name: ModuleInfo 104 | displayName: 'Get PowerShell Module Information' 105 | inputs: 106 | filePath: '$(System.DefaultWorkingDirectory)/build/Get-PSModuleInfo.ps1' 107 | arguments: '-ModuleManifestPath "$(Pipeline.Workspace)/$(artifactModuleSigned)/*/*.psd1"' 108 | pwsh: true 109 | 110 | - deployment: Package 111 | dependsOn: Prepare 112 | environment: Standalone 113 | variables: 114 | runCodesignValidationInjection: false 115 | moduleName: '$[ dependencies.Prepare.outputs[''ModuleInfo.moduleName''] ]' 116 | moduleVersion: '$[ dependencies.Prepare.outputs[''ModuleInfo.moduleVersion''] ]' 117 | strategy: 118 | runOnce: 119 | deploy: 120 | steps: 121 | - template: template-psmodule-package.yml 122 | parameters: 123 | moduleName: '$(moduleName)' 124 | moduleVersion: '$(moduleVersion)' 125 | 126 | - stage: Production 127 | displayName: 'Deploy Production' 128 | dependsOn: 129 | - Build 130 | #- Test 131 | - Package 132 | jobs: 133 | - job: Prepare 134 | variables: 135 | skipComponentGovernanceDetection: true 136 | runCodesignValidationInjection: false 137 | steps: 138 | - download: current 139 | artifact: '$(artifactModuleSigned)' 140 | - task: PowerShell@2 141 | name: ModuleInfo 142 | displayName: 'Get PowerShell Module Information' 143 | inputs: 144 | filePath: '$(System.DefaultWorkingDirectory)/build/Get-PSModuleInfo.ps1' 145 | arguments: '-ModuleManifestPath "$(Pipeline.Workspace)/$(artifactModuleSigned)/*/*.psd1"' 146 | pwsh: true 147 | 148 | - deployment: Publish 149 | environment: Production 150 | dependsOn: Prepare 151 | variables: 152 | moduleName: '$[ dependencies.Prepare.outputs[''ModuleInfo.moduleName''] ]' 153 | moduleVersion: '$[ dependencies.Prepare.outputs[''ModuleInfo.moduleVersion''] ]' 154 | strategy: 155 | runOnce: 156 | deploy: 157 | steps: 158 | - template: template-psmodule-publish.yml 159 | parameters: 160 | moduleName: '$(moduleName)' 161 | RepositorySourceLocation: 'https://www.powershellgallery.com/api/v2' 162 | NuGetApiKeyAzureConnection: 'Azure - MSFT Identity - Community Projects' 163 | NuGetApiKeyVaultName: 'codesign-kv' 164 | NuGetApiKeySecretName: 'PSGallery-API-Key' 165 | - task: GitHubRelease@1 166 | displayName: 'Create Release on GitHub' 167 | condition: and(succeeded(), eq(variables['Build.SourceBranchName'], 'main')) 168 | inputs: 169 | gitHubConnection: 'EntraExporter' 170 | repositoryName: '$(Build.Repository.Name)' 171 | action: 'create' 172 | target: '$(Build.SourceVersion)' 173 | tagSource: 'userSpecifiedTag' 174 | tag: 'v$(moduleVersion)' 175 | title: '$(moduleName) v$(moduleVersion)' 176 | assets: '$(Pipeline.Workspace)/$(artifactModulePackage)/*' 177 | addChangeLog: false 178 | 179 | 180 | # - stage: Test 181 | # dependsOn: Build 182 | # jobs: 183 | # - job: Windows 184 | # variables: 185 | # skipComponentGovernanceDetection: true 186 | # runCodesignValidationInjection: false 187 | # pool: 188 | # vmImage: 'windows-latest' 189 | # steps: 190 | # - download: current 191 | # artifact: '$(artifactModuleSigned)' 192 | # - task: PowerShell@2 193 | # name: ModuleInfo 194 | # displayName: 'Get Parameters for PowerShell Module' 195 | # inputs: 196 | # filePath: '$(System.DefaultWorkingDirectory)/build/Get-PSModuleInfo.ps1' 197 | # arguments: '-ModuleManifestPath "$(Pipeline.Workspace)/$(artifactModuleSigned)/*/*.psd1"' 198 | # pwsh: true 199 | # - template: template-psmodule-test.yml 200 | # parameters: 201 | # moduleName: '$(ModuleInfo.moduleName)' 202 | # artifactInput: '$(artifactModuleSigned)' 203 | # PesterConfigurationName: 'CD' 204 | # TestWindowsPowershell: true 205 | # - task: PSScriptAnalyzer@1 206 | # displayName: 'PowerShell Script Analyzer' 207 | # inputs: 208 | # Path: '$(Pipeline.Workspace)/$(artifactModuleSigned)' 209 | # Settings: 'required' 210 | # Recurse: true 211 | 212 | # - job: Ubuntu 213 | # variables: 214 | # skipComponentGovernanceDetection: true 215 | # runCodesignValidationInjection: false 216 | # pool: 217 | # vmImage: 'ubuntu-latest' 218 | # steps: 219 | # - download: current 220 | # artifact: '$(artifactModuleSigned)' 221 | # - task: PowerShell@2 222 | # name: ModuleInfo 223 | # displayName: 'Get Parameters for PowerShell Module' 224 | # inputs: 225 | # filePath: '$(System.DefaultWorkingDirectory)/build/Get-PSModuleInfo.ps1' 226 | # arguments: '-ModuleManifestPath "$(Pipeline.Workspace)/$(artifactModuleSigned)/*/*.psd1"' 227 | # pwsh: true 228 | # - template: template-psmodule-test.yml 229 | # parameters: 230 | # moduleName: '$(ModuleInfo.moduleName)' 231 | # artifactInput: '$(artifactModuleSigned)' 232 | # PesterConfigurationName: 'CD' 233 | 234 | # - job: MacOS 235 | # variables: 236 | # skipComponentGovernanceDetection: true 237 | # runCodesignValidationInjection: false 238 | # pool: 239 | # vmImage: 'macOS-latest' 240 | # steps: 241 | # - download: current 242 | # artifact: '$(artifactModuleSigned)' 243 | # - task: PowerShell@2 244 | # name: ModuleInfo 245 | # displayName: 'Get Parameters for PowerShell Module' 246 | # inputs: 247 | # filePath: '$(System.DefaultWorkingDirectory)/build/Get-PSModuleInfo.ps1' 248 | # arguments: '-ModuleManifestPath "$(Pipeline.Workspace)/$(artifactModuleSigned)/*/*.psd1"' 249 | # pwsh: true 250 | # - template: template-psmodule-test.yml 251 | # parameters: 252 | # moduleName: '$(ModuleInfo.moduleName)' 253 | # artifactInput: '$(artifactModuleSigned)' 254 | # PesterConfigurationName: 'CD' -------------------------------------------------------------------------------- /build/azure-pipelines/azure-pipelines-ci.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration Pipeline 2 | # https://aka.ms/yaml 3 | 4 | parameters: 5 | - name: vmImage 6 | displayName: 'Pool Image' 7 | type: string 8 | default: 'ubuntu-latest' 9 | values: 10 | - windows-latest 11 | - ubuntu-latest 12 | - macOS-latest 13 | 14 | trigger: 15 | batch: true 16 | branches: 17 | include: 18 | - main 19 | - preview 20 | paths: 21 | include: 22 | - src/* 23 | 24 | #pr: none 25 | 26 | variables: 27 | vmImage: '${{ parameters.vmImage }}' 28 | ${{ if eq(variables['Build.SourceBranchName'], 'main') }}: 29 | prereleaseTag: 30 | ${{ if ne(variables['Build.SourceBranchName'], 'main') }}: 31 | prereleaseTag: 'preview' 32 | artifactModule: 'PSModule' 33 | 34 | pool: 35 | vmImage: $(vmImage) 36 | 37 | stages: 38 | 39 | - stage: Test 40 | # dependsOn: Build 41 | jobs: 42 | - job: Windows 43 | variables: 44 | skipComponentGovernanceDetection: true 45 | runCodesignValidationInjection: false 46 | pool: 47 | vmImage: 'windows-latest' 48 | steps: 49 | - template: template-psmodule-test.yml 50 | parameters: 51 | pipelineId: 's' 52 | artifactInput: 'src' 53 | PesterConfigurationName: 'CI' 54 | TestWindowsPowershell: true 55 | 56 | - job: Ubuntu 57 | variables: 58 | skipComponentGovernanceDetection: true 59 | runCodesignValidationInjection: false 60 | pool: 61 | vmImage: 'ubuntu-latest' 62 | steps: 63 | - template: template-psmodule-test.yml 64 | parameters: 65 | pipelineId: 's' 66 | artifactInput: 'src' 67 | PesterConfigurationName: 'CI' 68 | 69 | - job: MacOS 70 | variables: 71 | skipComponentGovernanceDetection: true 72 | runCodesignValidationInjection: false 73 | pool: 74 | vmImage: 'macOS-latest' 75 | steps: 76 | - template: template-psmodule-test.yml 77 | parameters: 78 | pipelineId: 's' 79 | artifactInput: 'src' 80 | PesterConfigurationName: 'CI' 81 | -------------------------------------------------------------------------------- /build/azure-pipelines/template-psmodule-build.yml: -------------------------------------------------------------------------------- 1 | # PowerShell Module Build Pipeline Template 2 | # https://aka.ms/yaml 3 | 4 | parameters: 5 | - name: moduleName 6 | type: string 7 | default: 8 | - name: moduleRename 9 | type: string 10 | default: 11 | - name: moduleGuid 12 | type: string 13 | default: 14 | - name: moduleVersion 15 | type: string 16 | default: 17 | - name: prereleaseTag 18 | type: string 19 | default: 20 | - name: packages 21 | type: object 22 | default: 23 | - name: artifactOutput 24 | type: string 25 | default: 'PSModule' 26 | - name: GenerateManifest 27 | type: boolean 28 | default: false 29 | 30 | steps: 31 | - task: CopyFiles@2 32 | displayName: 'Copy Source to Staging' 33 | inputs: 34 | SourceFolder: '$(Build.SourcesDirectory)/src' 35 | Contents: '**' 36 | TargetFolder: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}' 37 | preserveTimestamp: true 38 | 39 | - task: PowerShell@2 40 | displayName: 'Merge PowerShell Module Nested Module Scripts' 41 | inputs: 42 | filePath: '$(System.DefaultWorkingDirectory)/build/Merge-PSModuleNestedModuleScripts.ps1' 43 | arguments: '-ModuleManifestPath "$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}.psd1" -MergeWithRootModule -RemoveNestedModuleScriptFiles' 44 | pwsh: true 45 | 46 | - task: CopyFiles@2 47 | displayName: 'Copy LICENSE to Staging' 48 | inputs: 49 | Contents: 'LICENSE' 50 | TargetFolder: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}' 51 | preserveTimestamp: true 52 | 53 | - pwsh: 'Rename-Item "$env:StagingDirectory/$env:ModuleName/LICENSE" -NewName "License.txt"' 54 | displayName: 'Rename LICENSE to License.txt' 55 | env: 56 | StagingDirectory: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}' 57 | ModuleName: '${{ coalesce(parameters.moduleRename,parameters.moduleName) }}' 58 | 59 | - ${{ if gt(length(parameters.packages), 0) }}: 60 | - task: NuGetCommand@2 61 | displayName: 'Restore NuGet Packages' 62 | inputs: 63 | command: 'restore' 64 | restoreSolution: '$(System.DefaultWorkingDirectory)/packages.config' 65 | feedsToUse: 'config' 66 | nugetConfigPath: '$(System.DefaultWorkingDirectory)/NuGet.config' 67 | restoreDirectory: '$(System.DefaultWorkingDirectory)/packages' 68 | 69 | - ${{ each package in parameters.packages }}: 70 | - ${{ each targetFramework in package.targetFramework }}: 71 | - task: CopyFiles@2 72 | displayName: 'Copy ${{ package.id }}.${{ package.version }} ${{ targetFramework }} to Staging' 73 | inputs: 74 | SourceFolder: '$(System.DefaultWorkingDirectory)/packages/${{ package.id }}.${{ package.version }}/lib/${{ targetFramework }}' 75 | Contents: '**' 76 | TargetFolder: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}/${{ package.id }}.${{ package.version }}/${{ targetFramework }}' 77 | preserveTimestamp: true 78 | 79 | - ${{ if ne(parameters.moduleRename, '') }}: 80 | - pwsh: 'Rename-Item "$env:StagingDirectory/$env:ModuleRename/$env:ModuleName.psd1" -NewName "$env:ModuleRename.psd1"' 81 | displayName: 'Rename PowerShell Module Manifest' 82 | env: 83 | StagingDirectory: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}' 84 | ModuleName: '${{ parameters.moduleName }}' 85 | ModuleRename: '${{ parameters.moduleRename }}' 86 | 87 | - task: PowerShell@2 88 | displayName: 'Update PowerShell Module Manifest' 89 | inputs: 90 | filePath: '$(System.DefaultWorkingDirectory)/build/Update-PSModuleManifest.ps1' 91 | arguments: '-ModuleManifestPath "$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}.psd1" -Guid "${{ parameters.moduleGuid }}" -ModuleVersion "${{ parameters.moduleVersion }}" -Prerelease "${{ parameters.prereleaseTag }}" -SkipRequiredAssembliesDetection' 92 | pwsh: true 93 | 94 | - task: PowerShell@2 95 | displayName: 'Add PowerShell Module Header' 96 | inputs: 97 | filePath: '$(System.DefaultWorkingDirectory)/build/Add-PSModuleHeader.ps1' 98 | arguments: '-ModuleManifestPath "$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}/${{ coalesce(parameters.moduleRename,parameters.moduleName) }}.psd1"' 99 | pwsh: true 100 | 101 | - ${{ if parameters.GenerateManifest }}: 102 | - task: ManifestGeneratorTask@0 103 | displayName: 'Generate Software Bill of Materials (SBOM)' 104 | inputs: 105 | BuildDropPath: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}' 106 | 107 | - task: PublishPipelineArtifact@1 108 | displayName: 'Publish PowerShell Module Artifact' 109 | inputs: 110 | targetPath: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}' 111 | artifact: '${{ parameters.artifactOutput }}' 112 | publishLocation: 'pipeline' 113 | -------------------------------------------------------------------------------- /build/azure-pipelines/template-psmodule-package.yml: -------------------------------------------------------------------------------- 1 | # PowerShell Module Package Pipeline Template 2 | # https://aka.ms/yaml 3 | 4 | parameters: 5 | - name: moduleName 6 | type: string 7 | default: 8 | - name: moduleVersion 9 | type: string 10 | default: 11 | - name: pipelineId 12 | type: string 13 | default: 14 | - name: artifactInput 15 | type: string 16 | default: 'PSModuleSigned' 17 | - name: artifactOutput 18 | type: string 19 | default: 'PSModulePackage' 20 | 21 | steps: 22 | #- download: current 23 | # artifact: '${{ parameters.artifactName }}' 24 | 25 | - task: ArchiveFiles@2 26 | displayName: 'Package PowerShell Module' 27 | inputs: 28 | rootFolderOrFile: '$(Pipeline.Workspace)/${{ parameters.pipelineId }}/${{ parameters.artifactInput }}/${{ parameters.moduleName }}' 29 | includeRootFolder: true 30 | archiveType: 'zip' 31 | archiveFile: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ parameters.moduleName }}_${{ parameters.moduleVersion }}.zip' 32 | replaceExistingArchive: true 33 | 34 | - task: PublishPipelineArtifact@1 35 | displayName: 'Publish PowerShell Module Package Artifact' 36 | inputs: 37 | targetPath: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/${{ parameters.moduleName }}_${{ parameters.moduleVersion }}.zip' 38 | artifact: '${{ parameters.artifactOutput }}' 39 | publishLocation: 'pipeline' 40 | -------------------------------------------------------------------------------- /build/azure-pipelines/template-psmodule-publish.yml: -------------------------------------------------------------------------------- 1 | # PowerShell Module Publish Pipeline Template 2 | # https://aka.ms/yaml 3 | 4 | parameters: 5 | - name: moduleName 6 | type: string 7 | - name: pipelineId 8 | type: string 9 | default: 10 | - name: artifactInput 11 | type: string 12 | default: 'PSModuleSigned' 13 | - name: RepositorySourceLocation 14 | type: string 15 | default: 'https://www.powershellgallery.com/api/v2' 16 | - name: NuGetApiKeyAzureConnection 17 | type: string 18 | - name: NuGetApiKeyVaultName 19 | type: string 20 | - name: NuGetApiKeySecretName 21 | type: string 22 | - name: Unlist 23 | type: boolean 24 | default: false 25 | 26 | steps: 27 | - checkout: self 28 | 29 | - task: AzureKeyVault@1 30 | displayName: 'Download NuGet API Key' 31 | inputs: 32 | azureSubscription: '${{ parameters.NuGetApiKeyAzureConnection }}' 33 | KeyVaultName: '${{ parameters.NuGetApiKeyVaultName }}' 34 | SecretsFilter: '${{ parameters.NuGetApiKeySecretName }}' 35 | RunAsPreJob: false 36 | 37 | - task: PowerShell@2 38 | displayName: 'Publish PowerShell Module' 39 | inputs: 40 | filePath: '$(System.DefaultWorkingDirectory)/build/Publish-PSModule.ps1' 41 | arguments: > 42 | -ModuleManifestPath "$(Pipeline.Workspace)/${{ parameters.pipelineId }}/${{ parameters.artifactInput }}/${{ parameters.moduleName }}" 43 | -RepositorySourceLocation ${{ parameters.RepositorySourceLocation }} 44 | -NuGetApiKey (ConvertTo-SecureString "$(${{ parameters.NuGetApiKeySecretName }})" -AsPlainText) 45 | -Unlist:$${{ parameters.Unlist }} 46 | pwsh: true 47 | -------------------------------------------------------------------------------- /build/azure-pipelines/template-psmodule-sign.yml: -------------------------------------------------------------------------------- 1 | # PowerShell Module Sign Pipeline Template 2 | # https://aka.ms/yaml 3 | 4 | parameters: 5 | - name: moduleName 6 | type: string 7 | default: 8 | - name: pipelineId 9 | type: string 10 | default: 11 | - name: artifactInput 12 | type: string 13 | default: 'PSModule' 14 | - name: artifactOutput 15 | type: string 16 | default: 'PSModuleSigned' 17 | - name: SigningCertificateAzureConnection 18 | type: string 19 | default: 20 | - name: SigningCertificateKeyVaultName 21 | type: string 22 | default: 23 | - name: SigningCertificateSecretName 24 | type: string 25 | default: 26 | - name: EsrpCodeSigningServiceName 27 | type: string 28 | default: 29 | 30 | steps: 31 | - ${{ if eq(coalesce(parameters.SigningCertificateSecretName, parameters.EsrpCodeSigningServiceName, ''),'') }}: 32 | - pwsh: 'exit 1' 33 | displayName: 'Required Template Parameter Error' 34 | 35 | #- download: none 36 | - checkout: self 37 | 38 | - task: CopyFiles@2 39 | displayName: 'Copy PowerShell Module to Staging' 40 | inputs: 41 | SourceFolder: '$(Pipeline.Workspace)/${{ parameters.pipelineId }}/${{ parameters.artifactInput }}' 42 | Contents: '**' 43 | TargetFolder: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}' 44 | CleanTargetFolder: true 45 | preserveTimestamp: true 46 | 47 | - ${{ if ne(parameters.SigningCertificateSecretName,'') }}: 48 | - task: AzureKeyVault@1 49 | displayName: 'Download Signing Certificate' 50 | inputs: 51 | azureSubscription: '${{ parameters.SigningCertificateAzureConnection }}' 52 | KeyVaultName: '${{ parameters.SigningCertificateKeyVaultName }}' 53 | SecretsFilter: '${{ parameters.SigningCertificateSecretName }}' 54 | RunAsPreJob: false 55 | 56 | - task: PowerShell@2 57 | displayName: 'Sign PowerShell Module Files' 58 | inputs: 59 | filePath: '$(System.DefaultWorkingDirectory)/build/Sign-PSModule.ps1' 60 | arguments: > 61 | -ModuleManifestPath "$(Pipeline.Workspace)/${{ parameters.artifactOutput }}/*" 62 | -SigningCertificate "$(${{ parameters.SigningCertificateSecretName }})" 63 | -AddCatalogFile 64 | pwsh: true 65 | 66 | - ${{ if ne(parameters.EsrpCodeSigningServiceName,'') }}: 67 | - task: EsrpCodeSigning@2 68 | displayName: 'Sign PowerShell Module Files' 69 | inputs: 70 | ConnectedServiceName: '${{ parameters.EsrpCodeSigningServiceName }}' 71 | FolderPath: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}' 72 | Pattern: '*.psd1,*.psm1,*.ps1*' 73 | signConfigType: inlineSignParams 74 | inlineOperation: >- 75 | [ 76 | { 77 | "KeyCode" : "CP-230012", 78 | "OperationCode" : "SigntoolSign", 79 | "Parameters" : { 80 | "OpusName" : "Microsoft", 81 | "OpusInfo" : "http://www.microsoft.com", 82 | "FileDigest" : "/fd \"SHA256\"", 83 | "PageHash" : "/PH", 84 | "TimeStamp" : "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" 85 | }, 86 | "ToolName" : "sign", 87 | "ToolVersion" : "1.0" 88 | }, 89 | { 90 | "KeyCode" : "CP-230012", 91 | "OperationCode" : "SigntoolVerify", 92 | "Parameters" : {}, 93 | "ToolName" : "sign", 94 | "ToolVersion" : "1.0" 95 | } 96 | ] 97 | 98 | - pwsh: 'New-FileCatalog "$env:StagingDirectory/$env:ModuleName/$env:ModuleName.cat" -Path "$env:StagingDirectory/$env:ModuleName" -CatalogVersion 2.0' 99 | displayName: 'Create File Catalog' 100 | condition: 'and(succeeded(), eq(variables[''Agent.OS''], ''Windows_NT''))' 101 | env: 102 | StagingDirectory: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}' 103 | ModuleName: '${{ parameters.moduleName }}' 104 | 105 | - task: EsrpCodeSigning@2 106 | displayName: 'Sign File Catalog' 107 | condition: 'and(succeeded(), eq(variables[''Agent.OS''], ''Windows_NT''))' 108 | inputs: 109 | ConnectedServiceName: '${{ parameters.EsrpCodeSigningServiceName }}' 110 | FolderPath: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}' 111 | Pattern: '*.cat' 112 | signConfigType: inlineSignParams 113 | inlineOperation: >- 114 | [ 115 | { 116 | "KeyCode" : "CP-230012", 117 | "OperationCode" : "SigntoolSign", 118 | "Parameters" : { 119 | "OpusName" : "Microsoft", 120 | "OpusInfo" : "http://www.microsoft.com", 121 | "FileDigest" : "/fd \"SHA256\"", 122 | "PageHash" : "/PH", 123 | "TimeStamp" : "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" 124 | }, 125 | "ToolName" : "sign", 126 | "ToolVersion" : "1.0" 127 | }, 128 | { 129 | "KeyCode" : "CP-230012", 130 | "OperationCode" : "SigntoolVerify", 131 | "Parameters" : {}, 132 | "ToolName" : "sign", 133 | "ToolVersion" : "1.0" 134 | } 135 | ] 136 | 137 | - task: PublishPipelineArtifact@1 138 | displayName: 'Publish PowerShell Module Artifact' 139 | inputs: 140 | targetPath: '$(Pipeline.Workspace)/${{ parameters.artifactOutput }}' 141 | artifact: '${{ parameters.artifactOutput }}' 142 | publishLocation: 'pipeline' 143 | -------------------------------------------------------------------------------- /build/azure-pipelines/template-psmodule-test.yml: -------------------------------------------------------------------------------- 1 | # PowerShell Module Test Pipeline Template 2 | # https://aka.ms/yaml 3 | 4 | parameters: 5 | - name: moduleName 6 | type: string 7 | default: 8 | - name: pipelineId 9 | type: string 10 | default: 11 | - name: artifactInput 12 | type: string 13 | default: 'PSModule' 14 | - name: artifactOutput 15 | type: string 16 | default: 'PSModuleTestResults' 17 | - name: PesterConfigurationName 18 | type: string 19 | default: 'Pipeline' 20 | - name: TestWindowsPowershell 21 | type: boolean 22 | default: false 23 | 24 | steps: 25 | - pwsh: | 26 | $ModuleManifest = Import-PowerShellDataFile (Join-Path $env:ModuleDirectory '*.psd1') 27 | $LockFileContent = [pscustomobject]@{ RequiredModules = $ModuleManifest.RequiredModules; ExternalModuleDependencies = $ModuleManifest.PrivateData.PSData.ExternalModuleDependencies } | ConvertTo-Json -Compress -Depth 2 28 | Set-Content $env:LockFilePath -Value $LockFileContent 29 | displayName: 'Generate PSModule Dependencies Lock File' 30 | env: 31 | ModuleDirectory: '$(Pipeline.Workspace)/${{ parameters.pipelineId }}/${{ parameters.artifactInput }}/${{ parameters.moduleName }}' 32 | LockFilePath: '$(Pipeline.Workspace)/PSModuleDependencies.lock.json' 33 | 34 | - task: Cache@2 35 | displayName: 'Cache PSModule Dependencies' 36 | inputs: 37 | key: PSModuleDependencies | $(Pipeline.Workspace)/PSModuleDependencies.lock.json 38 | path: "$(Pipeline.Workspace)/PSModuleCache" 39 | cacheHitVar: PSModuleDependencies_IsCached 40 | 41 | - task: PowerShell@2 42 | displayName: 'Restore PSModule Dependencies' 43 | condition: 'and(succeeded(), variables[''PSModuleDependencies_IsCached''])' 44 | inputs: 45 | filePath: '$(System.DefaultWorkingDirectory)/build/Restore-PSModuleDependencies.ps1' 46 | arguments: > 47 | -ModuleManifestPath "$(Pipeline.Workspace)/${{ parameters.pipelineId }}/${{ parameters.artifactInput }}/${{ parameters.moduleName }}" 48 | -PSModuleCacheDirectory "$(Pipeline.Workspace)/PSModuleCache" 49 | pwsh: true 50 | 51 | - task: PowerShell@2 52 | displayName: 'Test PSModule' 53 | inputs: 54 | filePath: '$(System.DefaultWorkingDirectory)/build/Test-PSModule.ps1' 55 | arguments: > 56 | -ModuleManifestPath "$(Pipeline.Workspace)/${{ parameters.pipelineId }}/${{ parameters.artifactInput }}/${{ parameters.moduleName }}" 57 | -PSModuleCacheDirectory "$(Pipeline.Workspace)/PSModuleCache" 58 | -PesterConfigurationPath "$(System.DefaultWorkingDirectory)/build/PesterConfiguration.${{ parameters.PesterConfigurationName }}.psd1" 59 | -TestResultPath "$(Common.TestResultsDirectory)/TestResult_$(Agent.OS)_$(Agent.OSArchitecture)_PowerShellCore_$(Build.BuildId).xml" 60 | -CodeCoveragePath "$(Common.TestResultsDirectory)/CodeCoverage_$(Agent.OS)_$(Agent.OSArchitecture)_PowerShellCore_$(Build.BuildId).xml" 61 | pwsh: true 62 | ignoreLASTEXITCODE: true 63 | 64 | - task: PublishCodeCoverageResults@1 65 | displayName: 'Publish PSModule Code Coverage Results' 66 | condition: 'succeeded()' 67 | inputs: 68 | codeCoverageTool: 'JaCoCo' 69 | summaryFileLocation: '$(Common.TestResultsDirectory)/CodeCoverage_$(Agent.OS)_$(Agent.OSArchitecture)_PowerShellCore_$(Build.BuildId).xml' 70 | pathToSources: '$(Pipeline.Workspace)/${{ parameters.pipelineId }}/${{ parameters.artifactInput }}/${{ parameters.moduleName }}' 71 | 72 | - task: PublishTestResults@2 73 | displayName: 'Publish PSModule Test Results' 74 | condition: 'succeeded()' 75 | inputs: 76 | testResultsFormat: 'NUnit' 77 | testResultsFiles: '$(Common.TestResultsDirectory)/TestResult_$(Agent.OS)_$(Agent.OSArchitecture)_PowerShellCore_$(Build.BuildId).xml' 78 | testRunTitle: '$(Agent.OS)_$(Agent.OSArchitecture)_PowerShellCore_$(Build.BuildId)' 79 | buildPlatform: '$(Agent.OSArchitecture)' 80 | failTaskOnFailedTests: true 81 | 82 | - ${{ if eq(parameters.TestWindowsPowershell, true) }}: 83 | - task: PowerShell@2 84 | displayName: 'Test PSModule on Windows PowerShell' 85 | condition: 'and(succeededOrFailed(), eq(variables[''Agent.OS''], ''Windows_NT''))' 86 | inputs: 87 | filePath: '$(System.DefaultWorkingDirectory)/build/Test-PSModule.ps1' 88 | arguments: > 89 | -ModuleManifestPath "$(Pipeline.Workspace)/${{ parameters.pipelineId }}/${{ parameters.artifactInput }}/${{ parameters.moduleName }}" 90 | -PSModuleCacheDirectory "$(Pipeline.Workspace)/PSModuleCache" 91 | -PesterConfigurationPath "$(System.DefaultWorkingDirectory)/build/PesterConfiguration.${{ parameters.PesterConfigurationName }}.psd1" 92 | -TestResultPath "$(Common.TestResultsDirectory)/TestResult_$(Agent.OS)_$(Agent.OSArchitecture)_WindowsPowerShell_$(Build.BuildId).xml" 93 | -CodeCoveragePath "$(Common.TestResultsDirectory)/CodeCoverage_$(Agent.OS)_$(Agent.OSArchitecture)_WindowsPowerShell_$(Build.BuildId).xml" 94 | pwsh: false 95 | 96 | - task: PublishCodeCoverageResults@1 97 | displayName: 'Publish PSModule Code Coverage Results on Windows PowerShell' 98 | condition: 'and(succeededOrFailed(), eq(variables[''Agent.OS''], ''Windows_NT''))' 99 | inputs: 100 | codeCoverageTool: 'JaCoCo' 101 | summaryFileLocation: '$(Common.TestResultsDirectory)/CodeCoverage_$(Agent.OS)_$(Agent.OSArchitecture)_WindowsPowerShell_$(Build.BuildId).xml' 102 | pathToSources: '$(Pipeline.Workspace)/${{ parameters.pipelineId }}/${{ parameters.artifactInput }}/${{ parameters.moduleName }}' 103 | 104 | - task: PublishTestResults@2 105 | displayName: 'Publish PSModule Test Results on Windows PowerShell' 106 | condition: 'and(succeededOrFailed(), eq(variables[''Agent.OS''], ''Windows_NT''))' 107 | inputs: 108 | testResultsFormat: 'NUnit' 109 | testResultsFiles: '$(Common.TestResultsDirectory)/TestResult_$(Agent.OS)_$(Agent.OSArchitecture)_WindowsPowerShell_$(Build.BuildId).xml' 110 | testRunTitle: '$(Agent.OS)_$(Agent.OSArchitecture)_WindowsPowerShell_$(Build.BuildId)' 111 | buildPlatform: '$(Agent.OSArchitecture)' 112 | #failTaskOnFailedTests: false 113 | 114 | - task: PublishPipelineArtifact@1 115 | displayName: 'Publish PSModule Test Results Directory' 116 | condition: 'succeededOrFailed()' 117 | inputs: 118 | targetPath: '$(Common.TestResultsDirectory)' 119 | artifact: '${{ parameters.artifactOutput }}_$(Agent.OS)_$(Agent.OSArchitecture)' 120 | publishLocation: 'pipeline' 121 | -------------------------------------------------------------------------------- /example_pipeline/azure-backup-pipeline.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | schedules: 3 | - cron: '0 1 * * *' 4 | displayName: "1am" 5 | branches: 6 | include: 7 | - main 8 | always: true 9 | 10 | jobs: 11 | - job: backup_azure 12 | displayName: Backup Azure configuration 13 | pool: 14 | vmImage: windows-latest 15 | continueOnError: false 16 | steps: 17 | - checkout: self 18 | persistCredentials: true 19 | 20 | # Set git global settings 21 | - task: Bash@3 22 | displayName: Configure Git 23 | inputs: 24 | targetType: 'inline' 25 | script: | 26 | git config --global user.name $(USER_NAME) 27 | git config --global user.email $(USER_EMAIL) 28 | git config --global core.longpaths true 29 | workingDirectory: '$(Build.SourcesDirectory)' 30 | failOnStderr: true 31 | 32 | - task: PowerShell@2 33 | displayName: Prepare environment & install prerequisites 34 | inputs: 35 | targetType: 'inline' 36 | script: | 37 | Write-Host 'Clean git folder' 38 | Remove-Item "$(Build.SourcesDirectory)\prod-backup" -Force -Recurse -ErrorAction silentlycontinue 39 | 40 | Write-Host 'Install EntraExporter module' 41 | Install-Module -Name EntraExporter -AllowClobber -Force -AcceptLicense 42 | failOnStderr: true 43 | pwsh: false 44 | 45 | - task: PowerShell@2 46 | displayName: Create the backup 47 | inputs: 48 | targetType: 'inline' 49 | script: | 50 | $applicationId = "$(CLIENT_ID)" 51 | $securedPassword = "$(CLIENT_SECRET)" 52 | $tenantID = "$(TENANT_NAME)" 53 | 54 | $securedPasswordPassword = ConvertTo-SecureString -String $SecuredPassword -AsPlainText -Force 55 | $clientSecretCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $ApplicationId, $SecuredPasswordPassword 56 | Connect-MgGraph -TenantId $tenantID -ClientSecretCredential $ClientSecretCredential -NoWelcome 57 | 58 | Write-Host 'Creating backup' 59 | Export-Entra "$(Build.SourcesDirectory)\prod-backup" -All -CloudUsersAndGroupsOnly #-Type "Config" 60 | failOnStderr: true 61 | pwsh: false 62 | 63 | # Commit changes and push to repo 64 | - task: Bash@3 65 | displayName: Commit changes 66 | inputs: 67 | targetType: 'inline' 68 | script: | 69 | DATEF=`date +%Y.%m.%d` 70 | git add --all 71 | git commit -m "Azure config backup $DATEF" 72 | git push origin HEAD:main 73 | workingDirectory: '$(Build.SourcesDirectory)' 74 | failOnStderr: false 75 | 76 | - job: tag 77 | displayName: Tag repo 78 | dependsOn: backup_azure 79 | pool: 80 | vmImage: windows-latest 81 | continueOnError: false 82 | steps: 83 | - checkout: self 84 | persistCredentials: true 85 | 86 | # Set git global settings 87 | - task: Bash@3 88 | displayName: Configure Git 89 | inputs: 90 | targetType: 'inline' 91 | script: | 92 | git config --global user.name $(USER_NAME) 93 | git config --global user.email $(USER_EMAIL) 94 | git config --global core.longpaths true 95 | workingDirectory: '$(Build.SourcesDirectory)' 96 | failOnStderr: true 97 | 98 | - task: Bash@3 99 | displayName: Pull origin 100 | inputs: 101 | targetType: 'inline' 102 | script: | 103 | git pull origin main 104 | workingDirectory: '$(Build.SourcesDirectory)' 105 | failOnStderr: false 106 | 107 | # Commit changes and push to repo 108 | - task: Bash@3 109 | displayName: Git tag 110 | inputs: 111 | targetType: 'inline' 112 | script: | 113 | DATEF=`date +%Y.%m.%d_%H.%M` 114 | git tag -a "v$DATEF" -m "Azure configuration snapshot $DATEF" 115 | git push origin "v$DATEF" 116 | workingDirectory: '$(Build.SourcesDirectory)' 117 | failOnStderr: false 118 | -------------------------------------------------------------------------------- /src/Connect-EntraExporter.ps1: -------------------------------------------------------------------------------- 1 | $global:TenantID = $null 2 | <# 3 | .SYNOPSIS 4 | Connect the Entra Exporter module to the Entra tenant. 5 | .DESCRIPTION 6 | This command will connect Microsoft.Graph to your Entra tenant. 7 | You can also directly call Connect-MgGraph if you require other options to connect 8 | 9 | Use the following scopes when authenticating with Connect-MgGraph. 10 | 11 | Connect-MgGraph -Scopes 'Directory.Read.All', 'Policy.Read.All', 'IdentityProvider.Read.All', 'Organization.Read.All', 'User.Read.All', 'EntitlementManagement.Read.All', 'UserAuthenticationMethod.Read.All', 'IdentityUserFlow.Read.All', 'APIConnectors.Read.All', 'AccessReview.Read.All', 'Agreement.Read.All', 'Policy.Read.PermissionGrant', 'PrivilegedAccess.Read.AzureResources', 'PrivilegedAccess.Read.AzureAD', 'Application.Read.All' 12 | 13 | .EXAMPLE 14 | PS C:\>Connect-EntraExporter 15 | Connect to home tenant of authenticated user. 16 | .EXAMPLE 17 | PS C:\>Connect-EntraExporter -TenantId 3043-343434-343434 18 | Connect to a specific Tenant 19 | #> 20 | function Connect-EntraExporter { 21 | param( 22 | [Parameter(Mandatory = $false)] 23 | [string] $TenantId = 'common', 24 | [Parameter(Mandatory=$false)] 25 | [ArgumentCompleter( { 26 | param ( $CommandName, $ParameterName, $WordToComplete, $CommandAst, $FakeBoundParameters ) 27 | (Get-MgEnvironment).Name 28 | } )] 29 | [string]$Environment = 'Global' 30 | ) 31 | 32 | 33 | Connect-MgGraph -TenantId $TenantId -Environment $Environment -Scopes 'Directory.Read.All', 34 | 'Policy.Read.All', 35 | 'IdentityProvider.Read.All', 36 | 'Organization.Read.All', 37 | 'User.Read.All', 38 | 'EntitlementManagement.Read.All', 39 | 'UserAuthenticationMethod.Read.All', 40 | 'IdentityUserFlow.Read.All', 41 | 'APIConnectors.Read.All', 42 | 'AccessReview.Read.All', 43 | 'Agreement.Read.All', 44 | 'Policy.Read.PermissionGrant', 45 | 'PrivilegedAccess.Read.AzureResources', 46 | 'PrivilegedAccess.Read.AzureAD', 47 | 'Application.Read.All', 48 | 'OnPremDirectorySynchronization.Read.All', 49 | 'Teamwork.Read.All', 50 | 'TeamworkAppSettings.ReadWrite.All', 51 | 'SharepointTenantSettings.Read.All', 52 | 'Reports.Read.All', 53 | 'RoleManagement.Read.All', 54 | 'AuditLog.Read.All' 55 | Get-MgContext 56 | $global:TenantID = (Get-MgContext).TenantId 57 | } 58 | -------------------------------------------------------------------------------- /src/EntraExporter.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | 3 | # Script module or binary module file associated with this manifest. 4 | RootModule = 'EntraExporter.psm1' 5 | 6 | # Version number of this module. 7 | ModuleVersion = '2.0.8' 8 | 9 | # Supported PSEditions 10 | CompatiblePSEditions = 'Core','Desktop' 11 | 12 | # ID used to uniquely identify this module 13 | GUID = 'd6c15273-d343-4556-a30d-b333eca3c1ab' 14 | 15 | # Author of this module 16 | Author = 'Microsoft Identity' 17 | 18 | # Company or vendor of this module 19 | CompanyName = 'Microsoft Corporation' 20 | 21 | # Copyright statement for this module 22 | Copyright = 'Microsoft Corporation. All rights reserved.' 23 | 24 | # Description of the functionality provided by this module 25 | Description = 'This module exports an Entra tenant''s identity related configuration settings and objects and writes them to json files.' 26 | 27 | # Minimum version of the Windows PowerShell engine required by this module 28 | PowerShellVersion = '5.1' 29 | 30 | # Name of the Windows PowerShell host required by this module 31 | # PowerShellHostName = '' 32 | 33 | # Minimum version of the Windows PowerShell host required by this module 34 | # PowerShellHostVersion = '' 35 | 36 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 37 | # DotNetFrameworkVersion = '' 38 | 39 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 40 | # CLRVersion = '' 41 | 42 | # Processor architecture (None, X86, Amd64) required by this module 43 | # ProcessorArchitecture = '' 44 | 45 | # Modules that must be imported into the global environment prior to importing this module 46 | RequiredModules = @( 47 | @{ ModuleName = 'Microsoft.Graph.Authentication'; Guid = '883916f2-9184-46ee-b1f8-b6a2fb784cee'; ModuleVersion = '2.2.0' } 48 | ) 49 | 50 | # Assemblies that must be loaded prior to importing this module 51 | # RequiredAssemblies = @() 52 | 53 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 54 | # ScriptsToProcess = @() 55 | 56 | # Type files (.ps1xml) to be loaded when importing this module 57 | # TypesToProcess = @() 58 | 59 | # Format files (.ps1xml) to be loaded when importing this module 60 | # FormatsToProcess = @() 61 | 62 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 63 | NestedModules = @( 64 | 'internal\Invoke-Graph.ps1' 65 | 'internal\Get-ObjectProperty.ps1' 66 | 'internal\ConvertTo-OrderedDictionary.ps1' 67 | 'internal\ConvertFrom-QueryString.ps1' 68 | 'internal\ConvertTo-QueryString.ps1' 69 | 'Connect-EntraExporter.ps1' 70 | 'Export-Entra.ps1' 71 | 'Get-EEDefaultSchema.ps1' 72 | 'Get-EERequiredScopes.ps1' 73 | 'Get-EEAccessPackageAssignmentPolicies.ps1' 74 | 'Get-EEAccessPackageAssignments.ps1' 75 | 'Get-EEAccessPackageResourceScopes.ps1' 76 | ) 77 | 78 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 79 | FunctionsToExport = @( 80 | 'Connect-EntraExporter' 81 | 'Export-Entra' 82 | ) 83 | 84 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 85 | CmdletsToExport = @() 86 | 87 | # Variables to export from this module 88 | VariablesToExport = @() 89 | 90 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 91 | AliasesToExport = @() 92 | 93 | # DSC resources to export from this module 94 | # DscResourcesToExport = @() 95 | 96 | # List of all modules packaged with this module 97 | # ModuleList = @() 98 | 99 | # List of all files packaged with this module 100 | # FileList = @() 101 | 102 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 103 | PrivateData = @{ 104 | 105 | PSData = @{ 106 | 107 | # Tags applied to this module. These help with module discovery in online galleries. 108 | Tags = 'Microsoft', 'Identity', 'Azure', 'Entra', 'AzureAD', 'AAD', 'PSEdition_Desktop', 'Windows', 'Export', 'Backup', 'DR' 109 | 110 | # A URL to the license for this module. 111 | LicenseUri = 'https://raw.githubusercontent.com/microsoft/entraexporter/main/LICENSE' 112 | 113 | # A URL to the main website for this project. 114 | ProjectUri = 'https://github.com/microsoft/entraexporter' 115 | 116 | # A URL to an icon representing this module. 117 | # IconUri = '' 118 | 119 | # ReleaseNotes of this module 120 | # ReleaseNotes = '' 121 | 122 | } # End of PSData hashtable 123 | 124 | } # End of PrivateData hashtable 125 | 126 | # HelpInfo URI of this module 127 | # HelpInfoURI = '' 128 | 129 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 130 | # DefaultCommandPrefix = '' 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/EntraExporter.psm1: -------------------------------------------------------------------------------- 1 | <# 2 | .DISCLAIMER 3 | THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF 4 | ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO 5 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A 6 | PARTICULAR PURPOSE. 7 | 8 | Copyright (c) Microsoft Corporation. All rights reserved. 9 | #> 10 | 11 | ## Set Strict Mode for Module. https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/set-strictmode 12 | Set-StrictMode -Version 3.0 13 | 14 | ## Initialize Module Configuration 15 | 16 | ## Initialize Module Variables -------------------------------------------------------------------------------- /src/Export-Entra.ps1: -------------------------------------------------------------------------------- 1 | function Export-Entra { 2 | <# 3 | .SYNOPSIS 4 | Exports Entra's configuration and settings for a tenant. 5 | 6 | .DESCRIPTION 7 | This cmdlet reads the configuration information from the target Entra tenant and produces the output files in a target directory. 8 | 9 | .PARAMETER Path 10 | Specifies the directory path where the output files will be generated. 11 | 12 | .PARAMETER Type 13 | Specifies the type of objects to export. Default to Config which exports the key configuration settings of the tenant. 14 | 15 | .PARAMETER All 16 | If specified performs a full export of all objects and configuration in the tenant. 17 | 18 | .PARAMETER CloudUsersAndGroupsOnly 19 | Excludes synched on-premises users and groups from the export. Only cloud-managed users and groups will be included. 20 | 21 | .EXAMPLE 22 | Export-Entra -Path 'C:\EntraBackup\' 23 | 24 | Runs a default export and includes the key tenant configuration settings. Does not include large data collections such as users, static groups, applications, service principals, etc. 25 | 26 | .EXAMPLE 27 | Export-Entra -Path 'C:\EntraBackup\' -All 28 | 29 | Runs a full export of all objects and configuration settings. 30 | 31 | .EXAMPLE 32 | Export-Entra -Path 'C:\EntraBackup\' -All -CloudUsersAndGroupsOnly 33 | 34 | Runs a full export but excludes on-prem synced users and groups. 35 | 36 | .EXAMPLE 37 | Export-Entra -Path 'C:\EntraBackup\' -Type ConditionalAccess, AppProxy 38 | 39 | Runs an export that includes just the Conditional Access and Application Proxy settings. 40 | 41 | .EXAMPLE 42 | Export-Entra -Path 'C:\EntraBackup\' -Type B2C 43 | 44 | Runs an export of all B2C settings. 45 | #> 46 | [CmdletBinding(DefaultParameterSetName = 'SelectTypes')] 47 | param ( 48 | 49 | # The directory path where the output files will be generated. 50 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'AllTypes')] 51 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ParameterSetName = 'SelectTypes')] 52 | [String]$Path, 53 | 54 | <# Specify the type of objects to export. Defaults to Config, which exports the key configuration settings of 55 | the tenant. The available types are: 56 | 'All', 'Config', 'AccessReviews', 'ConditionalAccess', 'Users', 'Groups', 'Applications', 'ServicePrincipals', 57 | 'B2C', 'B2B', 'PIM', 'PIMAzure', 'PIMAAD', 'AppProxy', 'Organization', 'Domains', 'EntitlementManagement', 58 | 'Policies', 'AdministrativeUnits', 'SKUs', 'Identity', 'Roles', 'Governance', 'Devices', 'Teams', 'Sharepoint', 59 | 'RoleManagement', 'DirectoryRoles', 'ExchangeRoles', 'IntuneRoles', 'CloudPCRoles', 'EntitlementManagementRoles', 60 | 'Reports', and 'UsersRegisteredByFeatureReport'. 61 | #> 62 | [Parameter(ParameterSetName = 'SelectTypes')] 63 | [ValidateSet('All', 'Config', 'AccessReviews', 'ConditionalAccess', 'Users', 'Groups', 'Applications', 'ServicePrincipals', 'B2C', 'B2B', 'PIM', 'PIMAzure', 'PIMAAD', 'AppProxy', 'Organization', 'Domains', 'EntitlementManagement', 'Policies', 'AdministrativeUnits', 'SKUs', 'Identity', 'Roles', 'Governance', 'Devices', 'Teams', 'Sharepoint', 'RoleManagement', 'DirectoryRoles', 'ExchangeRoles', 'IntuneRoles', 'CloudPCRoles', 'EntitlementManagementRoles', 'Reports', 'UsersRegisteredByFeatureReport')] 64 | [String[]]$Type = 'Config', 65 | 66 | # Perform a full export of all available configuration item types. 67 | [Parameter(ParameterSetName = 'AllTypes')] 68 | [switch]$All, 69 | 70 | # Exclude synced on-premises users and groups from the Entra export. Only cloud-managed users and groups will be included. 71 | [Parameter(Mandatory = $false, ParameterSetName = 'AllTypes')] 72 | [Parameter(Mandatory = $false, ParameterSetName = 'SelectTypes')] 73 | [switch]$CloudUsersAndGroupsOnly, 74 | 75 | # Specifies the schema to use for the export. If not specified, the default schema will be used. 76 | [Parameter(Mandatory = $false, ParameterSetName = 'AllTypes')] 77 | [Parameter(Mandatory = $false, ParameterSetName = 'SelectTypes')] 78 | [object]$ExportSchema, 79 | 80 | # Specifies the schema to use for the export. If not specified, the default schema will be used. 81 | [Parameter(Mandatory = $false, ParameterSetName = 'AllTypes')] 82 | [Parameter(Mandatory = $false, ParameterSetName = 'SelectTypes')] 83 | [string[]]$Parents 84 | ) 85 | 86 | if ($null -eq (Get-MgContext)) { 87 | Write-Error 'No active connection. Run Connect-EntraExporter or Connect-MgGraph to sign in and then retry.' 88 | break 89 | } 90 | 91 | if ($All) { $Type = @('All') } 92 | $global:Type = $Type #Used in places like Groups where Config flag will limit the resultset to just dynamic groups. 93 | 94 | if (!$ExportSchema) { 95 | $ExportSchema = Get-EEDefaultSchema 96 | } 97 | 98 | # Additional Filters 99 | foreach ($entry in $ExportSchema) { 100 | $graphUri = Get-ObjectProperty $entry 'GraphUri' 101 | 102 | # Filter out synced users or groups. 103 | if ($CloudUsersAndGroupsOnly -and ($graphUri -in 'users', 'groups')) { 104 | if ([string]::IsNullOrEmpty($entry.Filter)) { 105 | $entry.Filter = 'onPremisesSyncEnabled ne true' 106 | } else { 107 | $entry.Filter = $entry.Filter + ' and (onPremisesSyncEnabled ne true)' 108 | } 109 | } 110 | 111 | # Get all PIM elements. 112 | if ($All -and ($graphUri -in 'privilegedAccess/aadroles/resources', 'privilegedAccess/azureResources/resources')) { 113 | $entry.Filter = $null 114 | } 115 | } 116 | 117 | foreach ($item in $ExportSchema) { 118 | $typeMatch = Compare-Object $item.Tag $Type -ExcludeDifferent -IncludeEqual 119 | $hasParents = $Parents -and $Parents.Count -gt 0 120 | if (($typeMatch)) { 121 | $outputFileName = Join-Path -Path $Path -ChildPath $item.Path 122 | 123 | $spacer = '' 124 | if ($hasParents) { $spacer = ''.PadRight($Parents.Count + 3, ' ') + $Parents[$Parents.Count - 1] } 125 | 126 | Write-Host "$spacer $($item.Path)" 127 | 128 | $command = Get-ObjectProperty $item 'Command' 129 | $graphUri = Get-ObjectProperty $item 'GraphUri' 130 | $apiVersion = Get-ObjectProperty $item 'ApiVersion' 131 | $ignoreError = Get-ObjectProperty $item 'IgnoreError' 132 | if (!$apiVersion) { $apiVersion = 'v1.0' } 133 | $resultItems = $null 134 | if ($command) { 135 | if ($hasParents) { $command += " -Parents $Parents" } 136 | $resultItems = Invoke-Expression -Command $command 137 | } else { 138 | if ($hasParents) { $graphUri = $graphUri -replace '{id}', $Parents[$Parents.Count - 1] } 139 | try { 140 | $resultItems = Invoke-Graph $graphUri -Filter (Get-ObjectProperty $item 'Filter') -Select (Get-ObjectProperty $item 'Select') -QueryParameters (Get-ObjectProperty $item 'QueryParameters') -ApiVersion $apiVersion 141 | } catch { 142 | $e = '' 143 | if ($_.ErrorDetails -and $_.ErrorDetails.Message) { 144 | $e = $_.ErrorDetails.Message 145 | } 146 | 147 | if ($e.Contains($ignoreError) -or $e.Contains('Encountered an internal server error')) { 148 | Write-Debug $_ 149 | } else { 150 | Write-Error $_ 151 | } 152 | } 153 | } 154 | 155 | if ($outputFileName -match '\.json$') { 156 | if ($resultItems) { 157 | $resultItems | ConvertTo-Json -Depth 100 | Out-File (New-Item -Path $outputFileName -Force) 158 | } 159 | } else { 160 | foreach ($resultItem in $resultItems) { 161 | if (!$resultItem.PSObject.Properties['id']) { 162 | continue 163 | } 164 | $itemOutputFileName = Join-Path -Path $outputFileName -ChildPath $resultItem.id 165 | $parentOutputFileName = Join-Path $itemOutputFileName -ChildPath $resultItem.id 166 | $resultItem | ConvertTo-Json -Depth 100 | Out-File (New-Item -Path "$($parentOutputFileName).json" -Force) 167 | if ($item.ContainsKey('Children')) { 168 | $itemParents = $Parents 169 | $itemParents += $resultItem.Id 170 | Export-Entra -Path $itemOutputFileName -Type $Type -ExportSchema $item.Children -Parents $itemParents 171 | } 172 | } 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/Get-EEAccessPackageAssignmentPolicies.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Gets the list of accessPackageAssignmentPolicies 4 | 5 | .Description 6 | GET /identityGovernance/entitlementManagement/accessPackageAssignmentPolicies 7 | https://docs.microsoft.com/en-us/graph/api/accesspackageassignmentpolicy-list?view=graph-rest-beta&tabs=http 8 | 9 | .Example 10 | EEAccessPackagesAssignmentPolicies 11 | #> 12 | 13 | Function Get-EEAccessPackageAssignmentPolicies { 14 | [CmdletBinding()] 15 | param 16 | ( 17 | [Parameter(Mandatory = $true)] 18 | [string[]]$Parents 19 | ) 20 | Invoke-Graph 'identityGovernance/entitlementManagement/accessPackageAssignmentPolicies' -Filter "(accessPackage/id eq '$($Parents[0])')" -ApiVersion 'beta' 21 | } 22 | -------------------------------------------------------------------------------- /src/Get-EEAccessPackageAssignments.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Gets the list of accessPackageAssignments 4 | 5 | .Description 6 | GET /identityGovernance/entitlementManagement/accessPackageAssignments?$filter=accessPackage/id eq 7 | https://docs.microsoft.com/en-us/graph/api/accesspackageassignment-list?view=graph-rest-beta&tabs=http 8 | 9 | .Example 10 | Get-EEAccessPackagesAssignments 11 | #> 12 | 13 | Function Get-EEAccessPackageAssignments { 14 | [CmdletBinding()] 15 | param 16 | ( 17 | [Parameter(Mandatory = $true)] 18 | [string[]]$Parents 19 | ) 20 | Invoke-Graph 'identityGovernance/entitlementManagement/accessPackageAssignments' -Filter "(accessPackage/id eq '$($Parents[0])')" -ApiVersion 'beta' 21 | } 22 | -------------------------------------------------------------------------------- /src/Get-EEAccessPackageResourceScopes.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Gets the list of businessflowtemplatesRetrieve a list of accessPackage objects. 4 | 5 | .Description 6 | GET /identityGovernance/entitlementManagement/accessPackages/{id}?$expand=accessPackageResourceRoleScopes($expand=accessPackageResourceRole,accessPackageResourceScope) 7 | https://docs.microsoft.com/en-us/graph/api/accesspackage-list-accesspackageresourcerolescopes?view=graph-rest-beta&tabs=http 8 | 9 | .Example 10 | Get-EEAccessPackageResourceScopes 11 | #> 12 | 13 | Function Get-EEAccessPackageResourceScopes { 14 | [CmdletBinding()] 15 | param 16 | ( 17 | [Parameter(Mandatory = $true)] 18 | [string[]]$Parents 19 | ) 20 | Invoke-Graph "identityGovernance/entitlementManagement/accessPackages/$($Parents[0])" -QueryParameters @{expand='accessPackageResourceRoleScopes(expand=accessPackageResourceRole,accessPackageResourceScope)'} -ApiVersion 'beta' 21 | } 22 | -------------------------------------------------------------------------------- /src/Get-EEDefaultSchema.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Gets the default export schema definition 4 | 5 | .Description 6 | Gets the default export schema definition. Defining the order in which elements are exported. 7 | 8 | .Example 9 | Get-EEDefaultSchema 10 | #> 11 | 12 | function Get-EEDefaultSchema { 13 | $global:TenantID = (Get-MgContext).TenantId 14 | return @( 15 | # Organization 16 | @{ 17 | GraphUri = 'organization' 18 | Path = 'Organization/Organization.json' 19 | Tag = @('All', 'Config', 'Organization') 20 | DelegatedPermission = 'Directory.Read.All' 21 | ApplicationPermission = 'Directory.Read.All' 22 | }, 23 | @{ 24 | GraphUri = 'organization/{0}/branding/localizations' -f $TenantID 25 | Path = 'Organization/Branding/Localizations.json' 26 | Tag = @('All', 'Config', 'Organization') 27 | DelegatedPermission = 'User.Read.All' 28 | }, 29 | @{ 30 | GraphUri = 'organization/{0}/certificateBasedAuthConfiguration' -f $TenantID 31 | Path = 'Organization/CertificateBasedAuthConfiguration.json' 32 | Tag = @('All', 'Config', 'Organization') 33 | DelegatedPermission = 'Organization.Read.All' 34 | ApplicationPermission = 'Organization.Read.All' 35 | }, 36 | @{ 37 | GraphUri = 'directory/onPremisesSynchronization/{0}' -f $TenantID 38 | Path = 'Directory/OnPremisesSynchronization.json' 39 | Tag = @('All', 'Config', 'Directory') 40 | DelegatedPermission = 'OnPremDirectorySynchronization.Read.All' 41 | }, 42 | @{ 43 | GraphUri = 'domains' 44 | Path = 'Domains' 45 | Tag = @('All', 'Config','Domains') 46 | DelegatedPermission = 'Directory.Read.All' 47 | ApplicationPermission = 'Directory.Read.All' 48 | }, 49 | @{ 50 | GraphUri = 'identity/apiConnectors' 51 | Path = 'Identity/APIConnectors' 52 | ApiVersion = 'beta' 53 | IgnoreError = 'The feature self service sign up is not enabled for the tenant' 54 | Tag = @('All', 'Config', 'Identity') 55 | DelegatedPermission = 'APIConnectors.ReadWrite.All' 56 | ApplicationPermission = 'APIConnectors.ReadWrite.All' 57 | }, 58 | @{ 59 | GraphUri = 'identityProviders' 60 | Path = 'IdentityProviders' 61 | Tag = @('All', 'Config', 'Identity') 62 | DelegatedPermission = 'IdentityProvider.Read.All' 63 | }, 64 | @{ 65 | GraphUri = 'subscribedSkus' 66 | Path = 'SubscribedSkus' 67 | Tag = @('All', 'Config', 'SKUs') 68 | DelegatedPermission = 'Directory.Read.All' 69 | ApplicationPermission = 'Directory.Read.All' 70 | }, 71 | @{ 72 | GraphUri = 'directoryRoles' 73 | Path = 'DirectoryRoles' 74 | Tag = @('All', 'Config', 'Roles') 75 | DelegatedPermission = 'Directory.Read.All' 76 | ApplicationPermission = 'Directory.Read.All' 77 | Children = @( 78 | @{ 79 | GraphUri = 'directoryRoles/{id}/members' 80 | Select = 'id, userPrincipalName, displayName' 81 | Path = 'Members' 82 | Tag = @('All', 'Config', 'Roles') 83 | DelegatedPermission = 'Directory.Read.All' 84 | ApplicationPermission = 'Directory.Read.All' 85 | } 86 | @{ 87 | GraphUri = 'directoryroles/{id}/scopedMembers' 88 | Path = 'ScopedMembers' 89 | Tag = @('All', 'Config', 'Roles') 90 | DelegatedPermission = 'Directory.Read.All' 91 | ApplicationPermission = 'Directory.Read.All' 92 | } 93 | ) 94 | }, 95 | 96 | # B2C 97 | @{ 98 | GraphUri = 'identity/userFlows' 99 | Path = 'Identity/UserFlows' 100 | Tag = @('B2C') 101 | DelegatedPermission = 'IdentityUserFlow.Read.All' 102 | ApplicationPermission = 'IdentityUserFlow.Read.All' 103 | }, 104 | @{ 105 | GraphUri = 'identity/b2cUserFlows' 106 | Path = 'Identity/B2CUserFlows' 107 | Tag = @('B2C') 108 | DelegatedPermission = 'IdentityUserFlow.Read.All' 109 | ApplicationPermission = 'IdentityUserFlow.Read.All' 110 | Children = @( 111 | @{ 112 | GraphUri = 'identity/b2cUserFlows/{id}/identityProviders' 113 | Path = 'IdentityProviders' 114 | Tag = @('B2C') 115 | DelegatedPermission = 'IdentityUserFlow.Read.All' 116 | ApplicationPermission = 'IdentityUserFlow.Read.All' 117 | }, 118 | @{ 119 | GraphUri = 'identity/b2cUserFlows/{id}/userAttributeAssignments' 120 | QueryParameters = @{ expand = 'userAttribute' } 121 | Path = 'UserAttributeAssignments' 122 | Tag = @('B2C') 123 | DelegatedPermission = 'IdentityUserFlow.Read.All' 124 | ApplicationPermission = 'IdentityUserFlow.Read.All' 125 | }, 126 | @{ 127 | GraphUri = 'identity/b2cUserFlows/{id}/apiConnectorConfiguration' 128 | QueryParameters = @{ expand = 'postFederationSignup,postAttributeCollection' } 129 | Path = 'ApiConnectorConfiguration' 130 | Tag = @('B2C') 131 | DelegatedPermission = 'IdentityUserFlow.Read.All' 132 | ApplicationPermission = 'IdentityUserFlow.Read.All' 133 | }, 134 | @{ 135 | GraphUri = 'identity/b2cUserFlows/{id}/languages' 136 | Path = 'Languages' 137 | Tag = @('B2C') 138 | DelegatedPermission = 'IdentityUserFlow.Read.All' 139 | ApplicationPermission = 'IdentityUserFlow.Read.All' 140 | } 141 | ) 142 | }, 143 | 144 | # B2B 145 | @{ 146 | GraphUri = 'identity/userFlowAttributes' 147 | Path = 'Identity/UserFlowAttributes' 148 | ApiVersion = 'beta' 149 | Tag = @('Config', 'B2B', 'B2C') 150 | DelegatedPermission = 'IdentityUserFlow.Read.All' 151 | ApplicationPermission = 'IdentityUserFlow.Read.All' 152 | IgnoreError = 'The feature self service sign up is not enabled for the tenant' 153 | }, 154 | @{ 155 | GraphUri = 'identity/b2xUserFlows' 156 | Path = 'Identity/B2XUserFlows' 157 | ApiVersion = 'beta' 158 | Tag = @('All', 'Config', 'B2B') 159 | DelegatedPermission = 'IdentityUserFlow.Read.All' 160 | ApplicationPermission = 'IdentityUserFlow.Read.All' 161 | Children = @( 162 | @{ 163 | GraphUri = 'identity/b2xUserFlows/{id}/identityProviders' 164 | Path = 'IdentityProviders' 165 | ApiVersion = 'beta' 166 | Tag = @('All', 'Config', 'B2B') 167 | DelegatedPermission = 'IdentityUserFlow.Read.All' 168 | ApplicationPermission = 'IdentityUserFlow.Read.All' 169 | }, 170 | @{ 171 | GraphUri = 'identity/b2xUserFlows/{id}/userAttributeAssignments' 172 | QueryParameters = @{ expand = 'userAttribute' } 173 | Path = 'AttributeAssignments' 174 | ApiVersion = 'beta' 175 | Tag = @('All', 'Config', 'B2B') 176 | DelegatedPermission = 'IdentityUserFlow.Read.All' 177 | ApplicationPermission = 'IdentityUserFlow.Read.All' 178 | }, 179 | @{ 180 | GraphUri = 'identity/b2xUserFlows/{id}/apiConnectorConfiguration' 181 | QueryParameters = @{ expand = 'postFederationSignup,postAttributeCollection' } 182 | Path = 'APIConnectors' 183 | ApiVersion = 'beta' 184 | Tag = @('All', 'Config', 'B2B') 185 | DelegatedPermission = 'IdentityUserFlow.Read.All' 186 | ApplicationPermission = 'IdentityUserFlow.Read.All' 187 | }, 188 | @{ 189 | GraphUri = 'identity/b2xUserFlows/{id}/languages' 190 | Path = 'Languages' 191 | ApiVersion = 'beta' 192 | Tag = @('All', 'Config', 'B2B') 193 | DelegatedPermission = 'IdentityUserFlow.Read.All' 194 | ApplicationPermission = 'IdentityUserFlow.Read.All' 195 | } 196 | ) 197 | }, 198 | 199 | # Policies 200 | @{ 201 | GraphUri = 'policies/identitySecurityDefaultsEnforcementPolicy' 202 | Path = 'Policies/IdentitySecurityDefaultsEnforcementPolicy' 203 | Tag = @('All', 'Config', 'Policies') 204 | DelegatedPermission = 'Policy.Read.All' 205 | ApplicationPermission = 'Policy.Read.All' 206 | }, 207 | @{ 208 | GraphUri = 'policies/authorizationPolicy' 209 | Path = 'Policies/AuthorizationPolicy' 210 | Tag = @('All', 'Config', 'Policies') 211 | DelegatedPermission = 'Policy.Read.All' 212 | ApplicationPermission = 'Policy.Read.All' 213 | }, 214 | @{ 215 | GraphUri = 'policies/featureRolloutPolicies' 216 | Path = 'Policies/FeatureRolloutPolicies' 217 | Tag = @('All', 'Config', 'Policies') 218 | DelegatedPermission = 'Directory.ReadWrite.All' 219 | }, 220 | @{ 221 | GraphUri = 'policies/activityBasedTimeoutPolicies' 222 | Path = 'Policies/ActivityBasedTimeoutPolicy' 223 | Tag = @('All', 'Config', 'Policies') 224 | DelegatedPermission = 'Policy.Read.All' 225 | ApplicationPermission = 'Policy.Read.All' 226 | }, 227 | @{ 228 | GraphUri = 'policies/homeRealmDiscoveryPolicies' 229 | Path = 'Policies/HomeRealmDiscoveryPolicy' 230 | Tag = @('All', 'Config', 'Policies') 231 | DelegatedPermission = 'Policy.Read.All' 232 | ApplicationPermission = 'Policy.Read.All' 233 | }, 234 | @{ 235 | GraphUri = 'policies/claimsMappingPolicies' 236 | Path = 'Policies/ClaimsMappingPolicy' 237 | Tag = @('All', 'Config', 'Policies') 238 | DelegatedPermission = 'Policy.Read.All' 239 | ApplicationPermission = 'Policy.Read.All' 240 | }, 241 | @{ 242 | GraphUri = 'policies/tokenIssuancePolicies' 243 | Path = 'Policies/TokenIssuancePolicy' 244 | Tag = @('All', 'Config', 'Policies') 245 | DelegatedPermission = 'Policy.Read.All' 246 | ApplicationPermission = 'Policy.Read.All' 247 | }, 248 | @{ 249 | GraphUri = 'policies/tokenLifetimePolicies' 250 | Path = 'Policies/TokenLifetimePolicy' 251 | Tag = @('All', 'Config', 'Policies') 252 | DelegatedPermission = 'Policy.Read.All' 253 | ApplicationPermission = 'Policy.Read.All' 254 | }, 255 | @{ 256 | GraphUri = 'policies/defaultAppManagementPolicy' 257 | Path = 'Policies/DefaultAppManagementPolicy' 258 | ApiVersion = 'beta' 259 | Tag = @('All', 'Config', 'Policies') 260 | DelegatedPermission = 'Policy.Read.All' 261 | ApplicationPermission = 'Policy.Read.All' 262 | }, 263 | @{ 264 | GraphUri = 'policies/appManagementPolicies' 265 | Path = 'Policies/AppManagementPolicies' 266 | ApiVersion = 'beta' 267 | Tag = @('All', 'Config', 'Policies') 268 | DelegatedPermission = 'Policy.Read.All' 269 | ApplicationPermission = 'Policy.Read.All' 270 | }, 271 | @{ 272 | GraphUri = 'policies/authenticationMethodsPolicy/authenticationMethodConfigurations/email' 273 | Path = 'Policies/AuthenticationMethodsPolicy/AuthenticationMethodConfigurations/Email.json' 274 | Tag = @('All', 'Config', 'Policies') 275 | DelegatedPermission = 'Policy.Read.All' 276 | ApplicationPermission = 'Policy.Read.All' 277 | }, 278 | @{ 279 | GraphUri = 'policies/authenticationMethodsPolicy/authenticationMethodConfigurations/fido2' 280 | Path = 'Policies/AuthenticationMethodsPolicy/AuthenticationMethodConfigurations/FIDO2.json' 281 | Tag = @('All', 'Config', 'Policies') 282 | DelegatedPermission = 'Policy.Read.All' 283 | ApplicationPermission = 'Policy.Read.All' 284 | }, 285 | @{ 286 | GraphUri = 'policies/authenticationMethodsPolicy/authenticationMethodConfigurations/microsoftAuthenticator' 287 | Path = 'Policies/AuthenticationMethodsPolicy/AuthenticationMethodConfigurations/MicrosoftAuthenticator.json' 288 | Tag = @('All', 'Config', 'Policies') 289 | DelegatedPermission = 'Policy.Read.All' 290 | ApplicationPermission = 'Policy.Read.All' 291 | }, 292 | @{ 293 | GraphUri = 'policies/authenticationMethodsPolicy/authenticationMethodConfigurations/sms' 294 | Path = 'Policies/AuthenticationMethodsPolicy/AuthenticationMethodConfigurations/SMS.json' 295 | Tag = @('All', 'Config', 'Policies') 296 | DelegatedPermission = 'Policy.Read.All' 297 | ApplicationPermission = 'Policy.Read.All' 298 | }, 299 | @{ 300 | GraphUri = 'policies/authenticationMethodsPolicy/authenticationMethodConfigurations/temporaryAccessPass' 301 | Path = 'Policies/AuthenticationMethodsPolicy/AuthenticationMethodConfigurations/TemporaryAccessPass.json' 302 | Tag = @('All', 'Config', 'Policies') 303 | DelegatedPermission = 'Policy.Read.All' 304 | ApplicationPermission = 'Policy.Read.All' 305 | }, 306 | @{ 307 | GraphUri = 'policies/authenticationMethodsPolicy/authenticationMethodConfigurations/softwareOath' 308 | Path = 'Policies/AuthenticationMethodsPolicy/AuthenticationMethodConfigurations/SoftwareOath.json' 309 | Tag = @('All', 'Config', 'Policies') 310 | DelegatedPermission = 'Policy.Read.All' 311 | ApplicationPermission = 'Policy.Read.All' 312 | }, 313 | @{ 314 | GraphUri = 'policies/authenticationMethodsPolicy/authenticationMethodConfigurations/voice' 315 | Path = 'Policies/AuthenticationMethodsPolicy/AuthenticationMethodConfigurations/Voice.json' 316 | Tag = @('All', 'Config', 'Policies') 317 | DelegatedPermission = 'Policy.Read.All' 318 | ApplicationPermission = 'Policy.Read.All' 319 | }, 320 | @{ 321 | GraphUri = 'policies/authenticationMethodsPolicy/authenticationMethodConfigurations/x509Certificate' 322 | Path = 'Policies/AuthenticationMethodsPolicy/AuthenticationMethodConfigurations/X509Certificate.json' 323 | Tag = @('All', 'Config', 'Policies') 324 | DelegatedPermission = 'Policy.Read.All' 325 | ApplicationPermission = 'Policy.Read.All' 326 | }, 327 | @{ 328 | GraphUri = 'policies/adminConsentRequestPolicy' 329 | Path = 'Policies/AdminConsentRequestPolicy' 330 | Tag = @('All', 'Config', 'Policies') 331 | DelegatedPermission = 'Policy.Read.All' 332 | ApplicationPermission = 'Policy.Read.All' 333 | }, 334 | @{ 335 | GraphUri = 'policies/permissionGrantPolicies' 336 | Path = 'Policies/PermissionGrantPolicies' 337 | Tag = @('All', 'Config', 'Policies') 338 | DelegatedPermission = 'Policy.Read.PermissionGrant' 339 | ApplicationPermission = 'Policy.Read.PermissionGrant' 340 | }, 341 | @{ 342 | GraphUri = 'policies/externalIdentitiesPolicy' 343 | Path = 'Policies/ExternalIdentitiesPolicy' 344 | ApiVersion = 'beta' 345 | Tag = @('All', 'Config', 'Policies') 346 | DelegatedPermission = 'Policy.Read.All' 347 | ApplicationPermission = 'Policy.Read.All' 348 | }, 349 | @{ 350 | GraphUri = 'policies/crossTenantAccessPolicy' 351 | Path = 'Policies/CrossTenantAccessPolicy' 352 | ApiVersion = 'beta' 353 | Tag = @('All', 'Config', 'Policies') 354 | DelegatedPermission = 'Policy.Read.All' 355 | ApplicationPermission = 'Policy.Read.All' 356 | }, 357 | @{ 358 | GraphUri = 'policies/crossTenantAccessPolicy/default' 359 | Path = 'Policies/CrossTenantAccessPolicy/Default' 360 | ApiVersion = 'beta' 361 | Tag = @('All', 'Config', 'Policies') 362 | DelegatedPermission = 'Policy.Read.All' 363 | ApplicationPermission = 'Policy.Read.All' 364 | }, 365 | @{ 366 | GraphUri = 'policies/crossTenantAccessPolicy/partners' 367 | Path = 'Policies/CrossTenantAccessPolicy/Partners' 368 | ApiVersion = 'beta' 369 | Tag = @('All', 'Config', 'Policies') 370 | DelegatedPermission = 'Policy.Read.All' 371 | ApplicationPermission = 'Policy.Read.All' 372 | }, 373 | @{ 374 | GraphUri = 'identity/customAuthenticationExtensions' 375 | Path = 'Identity/CustomAuthenticationExtensions' 376 | ApiVersion = 'beta' 377 | Tag = @('All', 'Config') 378 | DelegatedPermission = 'Application.Read.All' 379 | ApplicationPermission = 'Application.Read.All' 380 | }, 381 | # Conditional Access 382 | @{ 383 | GraphUri = 'identity/conditionalAccess/policies' 384 | Path = 'Identity/Conditional/AccessPolicies' 385 | ApiVersion = 'beta' 386 | Tag = @('All', 'Config', 'ConditionalAccess') 387 | DelegatedPermission = 'Policy.Read.All' 388 | ApplicationPermission = 'Policy.Read.All' 389 | }, 390 | @{ 391 | GraphUri = 'identity/conditionalAccess/namedLocations' 392 | Path = 'Identity/Conditional/NamedLocations' 393 | Tag = @('All', 'Config', 'ConditionalAccess') 394 | DelegatedPermission = 'Policy.Read.All' 395 | ApplicationPermission = 'Policy.Read.All' 396 | }, 397 | 398 | # Identity Governance, 399 | @{ 400 | GraphUri = 'identityGovernance/entitlementManagement/accessPackages' 401 | Path = 'IdentityGovernance\EntitlementManagement\AccessPackages' 402 | ApiVersion = 'beta' 403 | Tag = @('All', 'Governance', 'EntitlementManagement') 404 | DelegatedPermission = 'EntitlementManagement.Read.All' 405 | ApplicationPermission = 'EntitlementManagement.Read.All' 406 | Children = @( 407 | @{ 408 | Command = 'Get-EEAccessPackageAssignmentPolicies' 409 | Path = 'AssignmentPolicies' 410 | Tag = @('All', 'Governance', 'EntitlementManagement') 411 | DelegatedPermission = 'EntitlementManagement.Read.All' 412 | ApplicationPermission = 'EntitlementManagement.Read.All' 413 | }, 414 | @{ 415 | Command = 'Get-EEAccessPackageAssignments' 416 | Path = 'Assignments' 417 | Tag = @('All', 'Governance', 'EntitlementManagement') 418 | DelegatedPermission = 'EntitlementManagement.Read.All' 419 | ApplicationPermission = 'EntitlementManagement.Read.All' 420 | }, 421 | @{ 422 | Command = 'Get-EEAccessPackageResourceScopes' 423 | Path = 'ResourceScopes' 424 | Tag = @('All', 'Governance', 'EntitlementManagement') 425 | DelegatedPermission = 'EntitlementManagement.Read.All' 426 | ApplicationPermission = 'EntitlementManagement.Read.All' 427 | } 428 | ) 429 | }, 430 | @{ 431 | GraphUri = 'identityGovernance/accessReviews/definitions' 432 | Path = 'IdentityGovernance/AccessReviews' 433 | ApiVersion = 'beta' 434 | Tag = @('All', 'AccessReviews', 'Governance') 435 | DelegatedPermission = 'AccessReview.Read.All' 436 | ApplicationPermission = 'AccessReview.Read.All' 437 | Children = @( 438 | @{ 439 | GraphUri = 'identityGovernance/accessReviews/definitions/{id}/instances' 440 | Path = '' 441 | Tag = @('All', 'AccessReviews', 'Governance') 442 | DelegatedPermission = 'AccessReview.Read.All' 443 | ApplicationPermission = 'AccessReview.Read.All' 444 | Children = @( 445 | @{ 446 | GraphUri = 'identityGovernance/accessReviews/definitions/{id}/instances/{id}/contactedReviewers' 447 | Path = 'Reviewers' 448 | ApiVersion = 'beta' 449 | Tag = @('All', 'AccessReviews', 'Governance') 450 | DelegatedPermission = 'AccessReview.Read.All' 451 | ApplicationPermission = 'AccessReview.Read.All' 452 | } 453 | ) 454 | } 455 | ) 456 | }, 457 | @{ 458 | GraphUri = 'identityGovernance/termsOfUse/agreements' 459 | Path = 'IdentityGovernance/TermsOfUse/Agreements' 460 | Tag = @('All', 'Config', 'Governance') 461 | DelegatedPermission = 'Agreement.Read.All' 462 | }, 463 | @{ 464 | GraphUri = 'identityGovernance/entitlementManagement/connectedOrganizations' 465 | Path = 'IdentityGovernance/EntitlementManagement/ConnectedOrganizations' 466 | ApiVersion = 'beta' 467 | Tag = @('All', 'Config') 468 | DelegatedPermission = 'EntitlementManagement.Read.All' 469 | ApplicationPermission = 'EntitlementManagement.Read.All' 470 | Children = @( 471 | @{ 472 | GraphUri = 'identityGovernance/entitlementManagement/connectedOrganizations/{id}/externalSponsors' 473 | Path = 'ExternalSponsors' 474 | ApiVersion = 'beta' 475 | Tag = @('All', 'Config', 'Governance') 476 | DelegatedPermission = 'EntitlementManagement.Read.All' 477 | ApplicationPermission = 'EntitlementManagement.Read.All' 478 | }, 479 | @{ 480 | GraphUri = 'identityGovernance/entitlementManagement/connectedOrganizations/{id}/internalSponsors' 481 | Path = 'InternalSponsors' 482 | ApiVersion = 'beta' 483 | Tag = @('All', 'Config', 'Governance') 484 | DelegatedPermission = 'EntitlementManagement.Read.All' 485 | ApplicationPermission = 'EntitlementManagement.Read.All' 486 | } 487 | ) 488 | }, 489 | @{ 490 | GraphUri = 'identityGovernance/entitlementManagement/settings' 491 | Path = 'IdentityGovernance/EntitlementManagement/Settings' 492 | ApiVersion = 'beta' 493 | Tag = @('All', 'Config', 'Governance') 494 | DelegatedPermission = 'EntitlementManagement.Read.All' 495 | ApplicationPermission = 'EntitlementManagement.Read.All' 496 | }, 497 | @{ 498 | GraphUri = 'AdministrativeUnits' 499 | Path = 'AdministrativeUnits' 500 | ApiVersion = 'beta' 501 | Tag = @('All', 'Config', 'AdministrativeUnits') 502 | DelegatedPermission = 'Directory.Read.All' 503 | ApplicationPermission = 'Directory.Read.All' 504 | Children = @( 505 | @{ 506 | GraphUri = 'administrativeUnits/{id}/members' 507 | Select = 'Id' 508 | Path = 'Members' 509 | ApiVersion = 'beta' 510 | Tag = @('All', 'Config', 'AdministrativeUnits') 511 | DelegatedPermission = 'Directory.Read.All' 512 | ApplicationPermission = 'Directory.Read.All' 513 | }, 514 | @{ 515 | GraphUri = 'administrativeUnits/{id}/scopedRoleMembers' 516 | Path = 'ScopedRoleMembers' 517 | ApiVersion = 'beta' 518 | Tag = @('All', 'Config', 'AdministrativeUnits') 519 | DelegatedPermission = 'Directory.Read.All' 520 | ApplicationPermission = 'Directory.Read.All' 521 | }, 522 | @{ 523 | GraphUri = 'administrativeUnits/{id}/extensions' 524 | Path = 'Extensions' 525 | ApiVersion = 'beta' 526 | Tag = @('All', 'Config', 'AdministrativeUnits') 527 | DelegatedPermission = 'Directory.Read.All' 528 | ApplicationPermission = 'Directory.Read.All' 529 | } 530 | ) 531 | }, 532 | 533 | # PIM 534 | @{ 535 | GraphUri = 'privilegedAccess/aadroles/resources' 536 | Path = 'PrivilegedAccess/AADRoles/Resources' 537 | ApiVersion = 'beta' 538 | Tag = @('All', 'Config', 'PIM', 'PIMAAD') 539 | DelegatedPermission = 'PrivilegedAccess.ReadWrite.AzureAD' 540 | ApplicationPermission = 'PrivilegedAccess.Read.AzureAD' 541 | Children = @( 542 | @{ 543 | GraphUri = 'privilegedAccess/aadroles/resources/{id}/roleDefinitions' 544 | Path = 'RoleDefinitions' 545 | ApiVersion = 'beta' 546 | Filter = "Type ne 'BuiltInRole'" 547 | Tag = @('All', 'Config', 'PIM', 'PIMAAD') 548 | DelegatedPermission = 'PrivilegedAccess.ReadWrite.AzureAD' 549 | ApplicationPermission = 'PrivilegedAccess.Read.AzureAD' 550 | }, 551 | @{ 552 | GraphUri = 'privilegedAccess/aadroles/resources/{id}/roleSettings' 553 | Path = 'RoleSettings' 554 | ApiVersion = 'beta' 555 | Filter = 'isDefault eq false' 556 | Tag = @('All', 'Config', 'PIM', 'PIMAAD') 557 | DelegatedPermission = 'PrivilegedAccess.ReadWrite.AzureAD' 558 | ApplicationPermission = 'PrivilegedAccess.Read.AzureAD' 559 | }, 560 | @{ 561 | GraphUri = 'privilegedAccess/aadroles/resources/{id}/roleAssignments' 562 | Path = 'RoleAssignments' 563 | ApiVersion = 'beta' 564 | Filter = 'endDateTime eq null' 565 | Tag = @('All', 'Config', 'PIM', 'PIMAAD') 566 | DelegatedPermission = 'PrivilegedAccess.ReadWrite.AzureAD' 567 | ApplicationPermission = 'PrivilegedAccess.Read.AzureAD' 568 | } 569 | ) 570 | }, 571 | @{ 572 | GraphUri = 'privilegedAccess/azureResources/resources' 573 | Path = 'PrivilegedAccess/AzureResources/Resources' 574 | ApiVersion = 'beta' 575 | IgnoreError = 'The tenant has not onboarded to PIM.' 576 | Tag = @('All', 'Config', 'PIM', 'PIMAzure') 577 | DelegatedPermission = 'PrivilegedAccess.ReadWrite.AzureResources' 578 | ApplicationPermission = 'PrivilegedAccess.Read.AzureResources' 579 | Children = @( 580 | @{ 581 | GraphUri = 'privilegedAccess/azureResources/resources/{id}/roleDefinitions' 582 | Path = 'RoleDefinitions' 583 | ApiVersion = 'beta' 584 | Filter = "Type ne 'BuiltInRole'" 585 | Tag = @('All', 'Config', 'PIM', 'PIMAzure') 586 | DelegatedPermission = 'PrivilegedAccess.ReadWrite.AzureResources' 587 | ApplicationPermission = 'PrivilegedAccess.Read.AzureResources' 588 | }, 589 | @{ 590 | GraphUri = 'privilegedAccess/azureResources/resources/{id}/roleSettings' 591 | Path = 'RoleSettings' 592 | ApiVersion = 'beta' 593 | Filter = 'isDefault eq false' 594 | Tag = @('All', 'Config', 'PIM', 'PIMAzure') 595 | DelegatedPermission = 'PrivilegedAccess.ReadWrite.AzureResources' 596 | ApplicationPermission = 'PrivilegedAccess.Read.AzureResources' 597 | }, 598 | @{ 599 | GraphUri = 'privilegedAccess/azureResources/resources/{id}/roleAssignments' 600 | Path = 'RoleAssignments' 601 | ApiVersion = 'beta' 602 | Filter = 'endDateTime eq null' 603 | Tag = @('All', 'Config', 'PIM', 'PIMAzure') 604 | DelegatedPermission = 'PrivilegedAccess.ReadWrite.AzureResources' 605 | ApplicationPermission = 'PrivilegedAccess.Read.AzureResources' 606 | } 607 | ) 608 | }, 609 | 610 | #Application Proxy 611 | @{ 612 | GraphUri = 'onPremisesPublishingProfiles/provisioning' 613 | QueryParameters = @{ expand = 'publishedResources,agents,agentGroups' } 614 | Path = 'OnPremisesPublishingProfiles/Provisioning.json' 615 | ApiVersion = 'beta' 616 | Tag = @('All', 'Config', 'AppProxy') 617 | DelegatedPermission = 'OnPremisesPublishingProfiles.ReadWrite.All' 618 | }, 619 | @{ 620 | GraphUri = 'onPremisesPublishingProfiles/provisioning/publishedResources' 621 | QueryParameters = @{ expand = 'agentGroups' } 622 | Path = 'OnPremisesPublishingProfiles/Provisioning/PublishedResources' 623 | ApiVersion = 'beta' 624 | Tag = @('All', 'Config', 'AppProxy') 625 | DelegatedPermission = 'OnPremisesPublishingProfiles.ReadWrite.All' 626 | }, 627 | @{ 628 | GraphUri = 'onPremisesPublishingProfiles/provisioning/agentGroups' 629 | QueryParameters = @{ expand = 'agents,publishedResources' } 630 | Path = 'OnPremisesPublishingProfiles/Provisioning/AgentGroups' 631 | ApiVersion = 'beta' 632 | Tag = @('All', 'Config', 'AppProxy') 633 | DelegatedPermission = 'OnPremisesPublishingProfiles.ReadWrite.All' 634 | }, 635 | @{ 636 | GraphUri = 'onPremisesPublishingProfiles/provisioning/agents' 637 | QueryParameters = @{ expand = 'agentGroups' } 638 | Path = 'OnPremisesPublishingProfiles/Provisioning/Agents' 639 | ApiVersion = 'beta' 640 | Tag = @('All', 'Config', 'AppProxy') 641 | DelegatedPermission = 'OnPremisesPublishingProfiles.ReadWrite.All' 642 | }, 643 | @{ 644 | GraphUri = 'onPremisesPublishingProfiles/applicationProxy/connectors' 645 | Path = 'OnPremisesPublishingProfiles/ApplicationProxy/Connectors' 646 | ApiVersion = 'beta' 647 | Tag = @('All', 'Config', 'AppProxy') 648 | DelegatedPermission = 'Directory.ReadWrite.All' 649 | }, 650 | @{ 651 | GraphUri = 'onPremisesPublishingProfiles/applicationProxy/connectorGroups' 652 | Path = 'OnPremisesPublishingProfiles/ApplicationProxy/ConnectorGroups' 653 | ApiVersion = 'beta' 654 | Tag = @('All', 'Config', 'AppProxy') 655 | DelegatedPermission = 'Directory.ReadWrite.All' 656 | Children = @( 657 | @{ 658 | GraphUri = 'onPremisesPublishingProfiles/applicationProxy/connectorGroups/{id}/applications' 659 | Path = 'Applications' 660 | ApiVersion = 'beta' 661 | IgnoreError = 'ApplicationsForGroup_NotFound' 662 | Tag = @('All', 'Config', 'AppProxy') 663 | DelegatedPermission = 'Directory.ReadWrite.All' 664 | }, 665 | @{ 666 | GraphUri = 'onPremisesPublishingProfiles/applicationProxy/connectorGroups/{id}/members' 667 | Path = 'Members' 668 | ApiVersion = 'beta' 669 | IgnoreError = 'ConnectorGroup_NotFound' 670 | Tag = @('All', 'Config', 'AppProxy') 671 | DelegatedPermission = 'Directory.ReadWrite.All' 672 | } 673 | ) 674 | }, 675 | 676 | # Groups 677 | # need to looks at app roles assignements 678 | # expanding app roles assignements breaks 'ne' filtering (needs eventual consistency and count) 679 | @{ 680 | GraphUri = 'groups' 681 | Filter = "groupTypes/any(c:c eq 'DynamicMembership')" 682 | Path = 'Groups' 683 | QueryParameters = @{ '$count' = 'true'; expand = 'extensions' } 684 | ApiVersion = 'beta' 685 | Tag = @('All', 'Config', 'Groups') 686 | DelegatedPermission = 'Directory.Read.All' 687 | ApplicationPermission = 'Directory.Read.All' 688 | Children = @( 689 | @{ 690 | GraphUri = 'groups/{id}/owners' 691 | Select = 'id, userPrincipalName, displayName' 692 | Path = 'Owners' 693 | Tag = @('All', 'Config', 'Groups') 694 | DelegatedPermission = 'Directory.Read.All' 695 | ApplicationPermission = 'Directory.Read.All' 696 | } 697 | ) 698 | }, 699 | @{ 700 | GraphUri = 'groups' 701 | Filter = "not(groupTypes/any(c:c eq 'DynamicMembership'))" 702 | Path = 'Groups' 703 | QueryParameters = @{ '$count' = 'true'; expand = 'extensions' } 704 | ApiVersion = 'beta' 705 | Tag = @('All', 'Groups') 706 | DelegatedPermission = 'Directory.Read.All' 707 | ApplicationPermission = 'Directory.Read.All' 708 | Children = @( 709 | @{ 710 | GraphUri = 'groups/{id}/owners' 711 | Select = 'id, userPrincipalName, displayName' 712 | Path = 'Owners' 713 | Tag = @('All', 'Config', 'Groups') 714 | DelegatedPermission = 'Directory.Read.All' 715 | ApplicationPermission = 'Directory.Read.All' 716 | }, 717 | @{ 718 | GraphUri = 'groups/{id}/members' 719 | Select = 'id, userPrincipalName, displayName' 720 | Path = 'Members' 721 | Tag = @('All', 'Groups') 722 | DelegatedPermission = 'Directory.Read.All' 723 | ApplicationPermission = 'Directory.Read.All' 724 | } 725 | ) 726 | }, 727 | @{ 728 | GraphUri = 'groupSettings' 729 | Path = 'GroupSettings' 730 | Tag = @('All', 'Config', 'Groups') 731 | DelegatedPermission = 'Directory.Read.All' 732 | ApplicationPermission = 'Directory.Read.All' 733 | }, 734 | 735 | # Applications 736 | @{ 737 | GraphUri = 'applications' 738 | Path = 'Applications' 739 | Tag = @('All', 'Applications') 740 | DelegatedPermission = 'Directory.Read.All' 741 | ApplicationPermission = 'Directory.Read.All' 742 | Children = @( 743 | @{ 744 | GraphUri = 'applications/{id}/extensionProperties' 745 | Path = 'ExtensionProperties' 746 | Tag = @('All', 'Applications') 747 | DelegatedPermission = 'Directory.Read.All' 748 | ApplicationPermission = 'Directory.Read.All' 749 | }, 750 | @{ 751 | GraphUri = 'applications/{id}/owners' 752 | Select = 'id, userPrincipalName, displayName' 753 | Path = 'Owners' 754 | Tag = @('All', 'Applications') 755 | DelegatedPermission = 'Directory.Read.All' 756 | ApplicationPermission = 'Directory.Read.All' 757 | }, 758 | @{ 759 | GraphUri = 'applications/{id}/tokenIssuancePolicies' 760 | Path = 'TokenIssuancePolicies' 761 | Tag = @('All', 'Applications') 762 | DelegatedPermission = 'Policy.Read.All' 763 | ApplicationPermission = 'Policy.Read.All','Application.ReadWrite.All' 764 | }, 765 | @{ 766 | GraphUri = 'applications/{id}/tokenLifetimePolicies' 767 | Path = 'TokenLifetimePolicies' 768 | Tag = @('All', 'Applications') 769 | DelegatedPermission = 'Policy.Read.All' 770 | ApplicationPermission = 'Policy.Read.All','Application.ReadWrite.All' 771 | }, 772 | @{ 773 | GraphUri = "applications/{id}/appManagementPolicies" 774 | Path = 'appManagementPolicies' 775 | Tag = @('All', 'Applications') 776 | DelegatedPermission = 'Policy.Read.All' 777 | ApplicationPermission = 'Policy.Read.All','Application.ReadWrite.All' 778 | } 779 | ) 780 | }, 781 | 782 | # Service Principals 783 | @{ 784 | GraphUri = 'servicePrincipals' 785 | Path = 'ServicePrincipals' 786 | Tag = @('All', 'ServicePrincipals') 787 | DelegatedPermission = 'Directory.Read.All' 788 | ApplicationPermission = 'Directory.Read.All' 789 | Children = @( 790 | @{ 791 | GraphUri = 'servicePrincipals/{id}/appRoleAssignments' 792 | Path = 'AppRoleAssignments' 793 | Tag = @('All', 'ServicePrincipals') 794 | DelegatedPermission = 'Directory.Read.All' 795 | ApplicationPermission = 'Directory.Read.All' 796 | }, 797 | @{ 798 | GraphUri = 'servicePrincipals/{id}/appRoleAssignedTo' 799 | Path = 'AppRoleAssignedTo' 800 | Tag = @('All', 'ServicePrincipals') 801 | DelegatedPermission = 'Directory.Read.All' 802 | ApplicationPermission = 'Directory.Read.All' 803 | }, 804 | @{ 805 | GraphUri = 'servicePrincipals/{id}/oauth2PermissionGrants' 806 | Path = 'Oauth2PermissionGrants' 807 | Tag = @('All', 'ServicePrincipals') 808 | DelegatedPermission = 'Directory.Read.All' 809 | ApplicationPermission = 'Directory.Read.All' 810 | }, 811 | @{ 812 | GraphUri = 'servicePrincipals/{id}/delegatedPermissionClassifications' 813 | Path = 'DelegatedPermissionClassifications' 814 | Tag = @('All', 'ServicePrincipals') 815 | DelegatedPermission = 'Directory.Read.All' 816 | ApplicationPermission = 'Directory.Read.All' 817 | }, 818 | @{ 819 | GraphUri = 'servicePrincipals/{id}/owners' 820 | Select = 'id, userPrincipalName, displayName' 821 | Path = 'Owners' 822 | Tag = @('All', 'ServicePrincipals') 823 | DelegatedPermission = 'Directory.Read.All' 824 | ApplicationPermission = 'Directory.Read.All' 825 | }, 826 | @{ 827 | GraphUri = 'servicePrincipals/{id}/claimsMappingPolicies' 828 | Path = 'claimsMappingPolicies' 829 | Tag = @('All', 'ServicePrincipals') 830 | DelegatedPermission = 'Policy.Read.All' 831 | ApplicationPermission = 'Policy.Read.All','Application.ReadWrite.All' 832 | }, 833 | @{ 834 | GraphUri = 'servicePrincipals/{id}/homeRealmDiscoveryPolicies' 835 | Path = 'homeRealmDiscoveryPolicies' 836 | Tag = @('All', 'ServicePrincipals') 837 | DelegatedPermission = 'Policy.Read.All' 838 | ApplicationPermission = 'Policy.Read.All','Application.ReadWrite.All' 839 | }, 840 | @{ 841 | GraphUri = 'servicePrincipals/{id}/tokenIssuancePolicies' 842 | Path = 'tokenIssuancePolicies' 843 | Tag = @('All', 'ServicePrincipals') 844 | DelegatedPermission = 'Policy.Read.All' 845 | ApplicationPermission = 'Policy.Read.All','Application.ReadWrite.All' 846 | }, 847 | @{ 848 | GraphUri = 'servicePrincipals/{id}/tokenLifetimePolicies' 849 | Path = 'tokenLifetimePolicies' 850 | Tag = @('All', 'ServicePrincipals') 851 | DelegatedPermission = 'Policy.Read.All' 852 | ApplicationPermission = 'Policy.Read.All','Application.ReadWrite.All' 853 | }, 854 | @{ 855 | GraphUri = 'servicePrincipals/{id}/appManagementPolicies' 856 | Path = 'appManagementPolicies' 857 | Tag = @('All', 'ServicePrincipals') 858 | DelegatedPermission = 'Policy.Read.All' 859 | ApplicationPermission = 'Policy.Read.All','Application.ReadWrite.All' 860 | } 861 | ) 862 | }, 863 | 864 | # Users 865 | # Todo look at app roles assignments 866 | @{ 867 | GraphUri = 'users' 868 | Path = 'Users' 869 | Filter = $null 870 | QueryParameters = @{ '$count' = 'true'; expand = "extensions" } 871 | ApiVersion = 'beta' 872 | Tag = @('All', 'Users') 873 | DelegatedPermission = 'Directory.Read.All' 874 | ApplicationPermission = 'Directory.Read.All' 875 | }, 876 | # Devices 877 | @{ 878 | GraphUri = 'devices' 879 | Path = 'Devices' 880 | Filter = $null 881 | ApiVersion = 'beta' 882 | Tag = @('All', 'Devices') 883 | DelegatedPermission = 'Directory.Read.All' 884 | ApplicationPermission = 'Directory.Read.All' 885 | }, 886 | # Teams 887 | @{ 888 | GraphUri = 'teamwork' 889 | Path = 'Admin/Teams/settings.json' 890 | Filter = $null 891 | ApiVersion = 'beta' 892 | Tag = @('All', 'Config', 'Teams') 893 | DelegatedPermission = 'Teamwork.Read.All' 894 | ApplicationPermission = 'Teamwork.Read.All' 895 | }, 896 | # Sharepoint 897 | @{ 898 | GraphUri = 'admin/sharepoint/settings' 899 | Path = 'Admin/Sharepoint/settings.json' 900 | Filter = $null 901 | ApiVersion = 'beta' 902 | Tag = @('All', 'Config', 'Sharepoint') 903 | DelegatedPermission = 'SharePointTenantSettings.Read.All' 904 | ApplicationPermission = 'SharePointTenantSettings.Read.All' 905 | }, 906 | # RoleManagement - Directory Role Definitions 907 | @{ 908 | GraphUri = 'roleManagement/directory/roleDefinitions' 909 | Path = 'RoleManagement/directory/roleDefinitions' 910 | ApiVersion = 'beta' 911 | Tag = @('All', 'Config', 'RoleManagement', 'DirectoryRoles') 912 | DelegatedPermission = 'RoleManagement.Read.All' 913 | ApplicationPermission = 'RoleManagement.Read.All' 914 | }, 915 | # RoleManagement - Directory Role Assignments 916 | @{ 917 | GraphUri = 'roleManagement/directory/roleAssignments' 918 | Path = 'RoleManagement/directory/roleAssignments' 919 | QueryParameters = @{ expand = 'principal' } 920 | ApiVersion = 'beta' 921 | Tag = @('All', 'Config', 'RoleManagement', 'DirectoryRoles') 922 | DelegatedPermission = 'RoleManagement.Read.All' 923 | ApplicationPermission = 'RoleManagement.Read.All' 924 | } 925 | # RoleManagement - Exchange Role Definitions 926 | @{ 927 | GraphUri = 'roleManagement/exchange/roleDefinitions' 928 | Path = 'RoleManagement/exchange/roleDefinitions' 929 | ApiVersion = 'beta' 930 | Tag = @('All', 'Config', 'RoleManagement', 'ExchangeRoles') 931 | DelegatedPermission = 'RoleManagement.Read.All' 932 | ApplicationPermission = 'RoleManagement.Read.All' 933 | }, 934 | # RoleManagement - Exchange Role Assignments 935 | @{ 936 | GraphUri = 'roleManagement/exchange/roleAssignments' 937 | Path = 'RoleManagement/exchange/roleAssignments' 938 | ApiVersion = 'beta' 939 | Tag = @('All', 'Config', 'RoleManagement', 'ExchangeRoles') 940 | DelegatedPermission = 'RoleManagement.Read.All' 941 | ApplicationPermission = 'RoleManagement.Read.All' 942 | }, 943 | # RoleManagement - Intune Role Definitions 944 | @{ 945 | GraphUri = 'roleManagement/deviceManagement/roleDefinitions' 946 | Path = 'RoleManagement/deviceManagement/roleDefinitions' 947 | ApiVersion = 'beta' 948 | Tag = @('All', 'Config', 'RoleManagement', 'IntuneRoles') 949 | DelegatedPermission = 'RoleManagement.Read.All' 950 | ApplicationPermission = 'RoleManagement.Read.All' 951 | }, 952 | # RoleManagement - Intune Role Assignments 953 | @{ 954 | GraphUri = 'roleManagement/deviceManagement/roleAssignments' 955 | Path = 'RoleManagement/deviceManagement/roleAssignments' 956 | QueryParameters = @{ expand = 'principals' } 957 | ApiVersion = 'beta' 958 | Tag = @('All', 'Config', 'RoleManagement', 'IntuneRoles') 959 | DelegatedPermission = 'RoleManagement.Read.All' 960 | ApplicationPermission = 'RoleManagement.Read.All' 961 | } 962 | # RoleManagement - CloudPC Role Definitions 963 | @{ 964 | GraphUri = 'roleManagement/cloudPC/roleDefinitions' 965 | Path = 'RoleManagement/cloudPC/roleDefinitions' 966 | ApiVersion = 'beta' 967 | Tag = @('All', 'Config', 'RoleManagement', 'CloudPCRoles') 968 | DelegatedPermission = 'RoleManagement.Read.All' 969 | ApplicationPermission = 'RoleManagement.Read.All' 970 | }, 971 | # RoleManagement - CloudPC Role Assignments 972 | @{ 973 | GraphUri = 'roleManagement/cloudPC/roleAssignments' 974 | Path = 'RoleManagement/cloudPC/roleAssignments' 975 | QueryParameters = @{ expand = 'principals' } 976 | ApiVersion = 'beta' 977 | Tag = @('All', 'Config', 'RoleManagement', 'CloudPCRoles') 978 | DelegatedPermission = 'RoleManagement.Read.All' 979 | ApplicationPermission = 'RoleManagement.Read.All' 980 | } 981 | # RoleManagement - Entitlement Management Role Definitions 982 | @{ 983 | GraphUri = 'roleManagement/entitlementManagement/roleDefinitions' 984 | Path = 'RoleManagement/entitlementManagement/roleDefinitions' 985 | ApiVersion = 'beta' 986 | Tag = @('All', 'Config', 'RoleManagement', 'EntitlementManagementRoles') 987 | DelegatedPermission = 'RoleManagement.Read.All' 988 | ApplicationPermission = 'RoleManagement.Read.All' 989 | }, 990 | # RoleManagement - Entitlement Management Role Assignments 991 | @{ 992 | GraphUri = 'roleManagement/entitlementManagement/roleAssignments' 993 | Path = 'RoleManagement/entitlementManagement/roleAssignments' 994 | QueryParameters = @{ expand = 'principal' } 995 | ApiVersion = 'beta' 996 | Tag = @('All', 'Config', 'RoleManagement', 'EntitlementManagementRoles') 997 | DelegatedPermission = 'RoleManagement.Read.All' 998 | ApplicationPermission = 'RoleManagement.Read.All' 999 | }, 1000 | # Reports - Users Registered By Feature 1001 | @{ 1002 | GraphUri = 'reports/authenticationMethods/microsoft.graph.usersRegisteredByFeature()' 1003 | Path = 'Reports/authenticationMethods/usersRegisteredByFeature/report.json' 1004 | ApiVersion = 'beta' 1005 | Tag = @('All', 'Reports', 'UsersRegisteredByFeatureReport') 1006 | DelegatedPermission = 'AuditLog.Read.All' 1007 | } 1008 | ) 1009 | } 1010 | -------------------------------------------------------------------------------- /src/Get-EERequiredScopes.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | Gets the required scopes for schema 4 | 5 | .Description 6 | Gets the require scopes for schema 7 | 8 | .Example 9 | Get-EERequiredScopes 10 | #> 11 | 12 | function Get-EERequiredScopes { 13 | [CmdletBinding()] 14 | param 15 | ( 16 | [Parameter(Mandatory = $true)] 17 | [ValidateSet('Delegated','Application')] 18 | [string]$PermissionType, 19 | [Parameter(Mandatory = $false)] 20 | [object]$ExportSchema 21 | ) 22 | 23 | if (!$ExportSchema) { 24 | $ExportSchema = Get-EEDefaultSchema 25 | } 26 | 27 | $scopeProperty = "DelegatedPermission" 28 | if ($PermissionType -eq "Application") { 29 | $scopeProperty = "ApplicationPermission" 30 | } 31 | 32 | $scopes = @() 33 | foreach($entry in $ExportSchema) { 34 | $entryScopes = Get-ObjectProperty $entry $scopeProperty 35 | $command = Get-ObjectProperty $entry 'Command' 36 | $graphUri = Get-ObjectProperty $entry 'GraphUri' 37 | $entryType = "graphuri" 38 | $tocall = $graphUri 39 | if ($command) { 40 | $entryType = "command" 41 | $tocall = $command 42 | } 43 | 44 | if (!$entryScopes) { 45 | write-warning "call to $entryType '$tocall' doesn't provide $PermissionType permissions" 46 | } 47 | 48 | foreach ($entryScope in $entryScopes) { 49 | if ($entryScope -notin $scopes) { 50 | $scopes += $entryScope 51 | } 52 | } 53 | if ($entry.ContainsKey('Children')) { 54 | $childScopes = Get-EERequiredScopes -PermissionType $PermissionType -ExportSchema $entry.Children 55 | foreach ($entryScope in $childScopes) { 56 | if ($entryScope -notin $scopes) { 57 | $scopes += $entryScope 58 | } 59 | } 60 | } 61 | } 62 | 63 | $scopes | sort-object 64 | } -------------------------------------------------------------------------------- /src/internal/ConvertFrom-QueryString.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Convert Query String to object. 4 | .EXAMPLE 5 | PS C:\>ConvertFrom-QueryString '?name=path/file.json&index=10' 6 | Convert query string to object. 7 | .EXAMPLE 8 | PS C:\>'name=path/file.json&index=10' | ConvertFrom-QueryString -AsHashtable 9 | Convert query string to hashtable. 10 | .INPUTS 11 | System.String 12 | .LINK 13 | https://github.com/jasoth/Utility.PS 14 | #> 15 | function ConvertFrom-QueryString { 16 | [CmdletBinding()] 17 | [OutputType([psobject])] 18 | [OutputType([hashtable])] 19 | param ( 20 | # Value to convert 21 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] 22 | [string[]] $InputStrings, 23 | # URL decode parameter names 24 | [Parameter(Mandatory = $false)] 25 | [switch] $DecodeParameterNames, 26 | # Converts to hash table object 27 | [Parameter(Mandatory = $false)] 28 | [switch] $AsHashtable 29 | ) 30 | 31 | process { 32 | foreach ($InputString in $InputStrings) { 33 | if ($AsHashtable) { [hashtable] $OutputObject = @{ } } 34 | else { [psobject] $OutputObject = New-Object psobject } 35 | 36 | if ($InputString[0] -eq '?') { $InputString = $InputString.Substring(1) } 37 | [string[]] $QueryParameters = $InputString.Split('&') 38 | foreach ($QueryParameter in $QueryParameters) { 39 | [string[]] $QueryParameterPair = $QueryParameter.Split('=') 40 | if ($DecodeParameterNames) { $QueryParameterPair[0] = [System.Net.WebUtility]::UrlDecode($QueryParameterPair[0]) } 41 | if ($OutputObject -is [hashtable]) { 42 | $OutputObject.Add($QueryParameterPair[0], [System.Net.WebUtility]::UrlDecode($QueryParameterPair[1])) 43 | } 44 | else { 45 | $OutputObject | Add-Member $QueryParameterPair[0] -MemberType NoteProperty -Value ([System.Net.WebUtility]::UrlDecode($QueryParameterPair[1])) 46 | } 47 | } 48 | Write-Output $OutputObject 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/internal/ConvertTo-OrderedDictionary.ps1: -------------------------------------------------------------------------------- 1 | function ConvertTo-OrderedDictionary 2 | { 3 | [CmdletBinding()] 4 | param ( 5 | [Parameter(Mandatory = $true, ValueFromPipeline = $true)] 6 | $InputObject 7 | ) 8 | 9 | process 10 | { 11 | if($InputObject){ 12 | if($InputObject -is [array]){ 13 | $outputArray = @() 14 | foreach($item in $InputObject){ 15 | $outputArray += ConvertTo-OrderedDictionary $item 16 | } 17 | return $outputArray 18 | } 19 | elseif($InputObject -is [hashtable]){ 20 | $outputObject = [ordered]@{} 21 | foreach ($Item in ($InputObject.GetEnumerator() | Sort-Object -Property Key)) { 22 | if($Item){ 23 | $value = Get-ObjectProperty $Item 'Value' 24 | if($value -is [hashtable] -or $value -is [array]){ #if child is a hashtable or array, sort it too 25 | $Item.Value = ConvertTo-OrderedDictionary $value 26 | } 27 | } 28 | $outputObject[$Item.Key] = $Item.Value 29 | } 30 | return $outputObject 31 | } 32 | } 33 | else { 34 | return $InputObject 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/internal/ConvertTo-QueryString.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Convert Hashtable to Query String. 4 | .EXAMPLE 5 | PS C:\>ConvertTo-QueryString @{ name = 'path/file.json'; index = 10 } 6 | Convert hashtable to query string. 7 | .EXAMPLE 8 | PS C:\>[ordered]@{ title = 'convert&prosper'; id = [guid]'352182e6-9ab0-4115-807b-c36c88029fa4' } | ConvertTo-QueryString 9 | Convert ordered dictionary to query string. 10 | .INPUTS 11 | System.Collections.Hashtable 12 | .LINK 13 | https://github.com/jasoth/Utility.PS 14 | #> 15 | function ConvertTo-QueryString { 16 | [CmdletBinding()] 17 | [OutputType([string])] 18 | param ( 19 | # Value to convert 20 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] 21 | [object] $InputObjects, 22 | # URL encode parameter names 23 | [Parameter(Mandatory = $false)] 24 | [switch] $EncodeParameterNames 25 | ) 26 | 27 | process { 28 | foreach ($InputObject in $InputObjects) { 29 | $QueryString = New-Object System.Text.StringBuilder 30 | if ($InputObject -is [hashtable] -or $InputObject -is [System.Collections.Specialized.OrderedDictionary] -or $InputObject.GetType().FullName.StartsWith('System.Collections.Generic.Dictionary')) { 31 | foreach ($Item in $InputObject.GetEnumerator()) { 32 | if ($QueryString.Length -gt 0) { [void]$QueryString.Append('&') } 33 | [string] $ParameterName = $Item.Key 34 | if ($EncodeParameterNames) { $ParameterName = [System.Net.WebUtility]::UrlEncode($ParameterName) } 35 | [void]$QueryString.AppendFormat('{0}={1}', $ParameterName, [System.Net.WebUtility]::UrlEncode($Item.Value)) 36 | } 37 | } 38 | elseif ($InputObject -is [object] -and $InputObject -isnot [ValueType]) { 39 | foreach ($Item in ($InputObject | Get-Member -MemberType Property, NoteProperty)) { 40 | if ($QueryString.Length -gt 0) { [void]$QueryString.Append('&') } 41 | [string] $ParameterName = $Item.Name 42 | if ($EncodeParameterNames) { $ParameterName = [System.Net.WebUtility]::UrlEncode($ParameterName) } 43 | [void]$QueryString.AppendFormat('{0}={1}', $ParameterName, [System.Net.WebUtility]::UrlEncode($InputObject.($Item.Name))) 44 | } 45 | } 46 | else { 47 | ## Non-Terminating Error 48 | $Exception = New-Object ArgumentException -ArgumentList ('Cannot convert input of type {0} to query string.' -f $InputObject.GetType()) 49 | Write-Error -Exception $Exception -Category ([System.Management.Automation.ErrorCategory]::ParserError) -CategoryActivity $MyInvocation.MyCommand -ErrorId 'ConvertQueryStringFailureTypeNotSupported' -TargetObject $InputObject 50 | continue 51 | } 52 | 53 | Write-Output $QueryString.ToString() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/internal/Get-ObjectProperty.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Get object property value. 4 | .EXAMPLE 5 | PS C:\>$object = New-Object psobject -Property @{ title = 'title value' } 6 | PS C:\>$object | Get-ObjectProperty -Property 'title' 7 | Get value of object property named title. 8 | .EXAMPLE 9 | PS C:\>$object = New-Object psobject -Property @{ lvl1 = (New-Object psobject -Property @{ nextLevel = 'lvl2 data' }) } 10 | PS C:\>Get-ObjectProperty $object -Property 'lvl1', 'nextLevel' 11 | Get value of nested object property named nextLevel. 12 | .INPUTS 13 | System.Collections.Hashtable 14 | System.Management.Automation.PSObject 15 | #> 16 | function Get-ObjectProperty { 17 | [CmdletBinding()] 18 | [OutputType([object])] 19 | param ( 20 | # Object containing property values 21 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] 22 | [object] $InputObjects, 23 | # Name of property. Specify an array of property names to tranverse nested objects. 24 | [Parameter(Mandatory = $true, ValueFromRemainingArguments = $true)] 25 | [string[]] $Property 26 | ) 27 | 28 | process { 29 | foreach ($InputObject in $InputObjects) { 30 | for ($iProperty = 0; $iProperty -lt $Property.Count; $iProperty++) { 31 | ## Get property value 32 | if ($InputObject -is [hashtable]) { 33 | if ($InputObject.ContainsKey($Property[$iProperty])) { 34 | $PropertyValue = $InputObject[$Property[$iProperty]] 35 | } 36 | else { $PropertyValue = $null} 37 | } 38 | else { 39 | $PropertyValue = Select-Object -InputObject $InputObject -ExpandProperty $Property[$iProperty] -ErrorAction SilentlyContinue 40 | } 41 | ## Check for more nested properties 42 | if ($iProperty -lt $Property.Count - 1) { 43 | $InputObject = $PropertyValue 44 | if ($null -eq $InputObject) { break } 45 | } 46 | else { 47 | Write-Output $PropertyValue 48 | } 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/internal/Invoke-Graph.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .SYNOPSIS 3 | Run a Microsoft Graph Command 4 | #> 5 | function Invoke-Graph{ 6 | [CmdletBinding()] 7 | param( 8 | # Graph endpoint such as "users". 9 | [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)] 10 | [string[]] $RelativeUri, 11 | # Specifies unique Id(s) for the URI endpoint. For example, users endpoint accepts Id or UPN. 12 | [Parameter(Mandatory = $false)] 13 | [string[]] $UniqueId, 14 | # Filters properties (columns). 15 | [Parameter(Mandatory = $false)] 16 | [string[]] $Select, 17 | # Filters results (rows). https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter 18 | [Parameter(Mandatory = $false)] 19 | [string] $Filter, 20 | # Parameters such as "$top". 21 | [Parameter(Mandatory = $false)] 22 | [hashtable] $QueryParameters, 23 | # API Version. 24 | [Parameter(Mandatory = $false)] 25 | [ValidateSet('v1.0', 'beta')] 26 | [string] $ApiVersion = 'v1.0', 27 | # Specifies consistency level. 28 | [Parameter(Mandatory = $false)] 29 | [string] $ConsistencyLevel = 'eventual', 30 | # Only return first page of results. 31 | [Parameter(Mandatory = $false)] 32 | [switch] $DisablePaging, 33 | # Force individual requests to MS Graph. 34 | [Parameter(Mandatory = $false)] 35 | [switch] $DisableBatching, 36 | # Specify Batch size. 37 | [Parameter(Mandatory = $false)] 38 | [int] $BatchSize = 20, 39 | # Base URL for Microsoft Graph API. 40 | [Parameter(Mandatory = $false)] 41 | [uri] $GraphBaseUri 42 | ) 43 | 44 | begin { 45 | if(!$GraphBaseUri){ 46 | if(!(Test-Path variable:global:GraphBaseUri)){ 47 | $global:GraphBaseUri = $((Get-MgEnvironment -Name (Get-MgContext).Environment).GraphEndpoint) 48 | } 49 | $GraphBaseUri = $global:GraphBaseUri 50 | } 51 | $listRequests = New-Object 'System.Collections.Generic.List[psobject]' 52 | 53 | function Format-Result ($results, $RawOutput) { 54 | if (!$RawOutput -and $results -and (Get-ObjectProperty $results 'value')) { 55 | foreach ($result in $results.value) { 56 | if ($result -is [hashtable]) { 57 | $result.Add('@odata.context', ('{0}/$entity' -f $results.'@odata.context')) 58 | } 59 | else { 60 | $result | Add-Member -MemberType NoteProperty -Name '@odata.context' -Value ('{0}/$entity' -f $results.'@odata.context') 61 | } 62 | Write-Output $result 63 | } 64 | } 65 | else { Write-Output $results } 66 | } 67 | 68 | function Complete-Result ($results, $DisablePaging) { 69 | if (!$DisablePaging -and $results) { 70 | while (Get-ObjectProperty $results '@odata.nextLink') { 71 | $results = Invoke-MgGraphRequest -Method GET -Uri $results.'@odata.nextLink' -Headers @{ ConsistencyLevel = $ConsistencyLevel } -OutputType PSObject 72 | Format-Result $results $DisablePaging 73 | } 74 | } 75 | } 76 | } 77 | 78 | process { 79 | ## Initialize 80 | $results = $null 81 | if (!$UniqueId) { [string[]] $UniqueId = '' } 82 | if ($DisableBatching -and ($RelativeUri.Count -gt 1 -or $UniqueId.Count -gt 1)) { 83 | Write-Warning ('This command is invoking {0} individual Graph requests. For better performance, remove the -DisableBatching parameter.' -f ($RelativeUri.Count * $UniqueId.Count)) 84 | } 85 | 86 | ## Process Each RelativeUri 87 | foreach ($uri in $RelativeUri) { 88 | $uriQueryEndpoint = New-Object System.UriBuilder -ArgumentList ([IO.Path]::Combine($GraphBaseUri.AbsoluteUri, $ApiVersion, $uri)) 89 | 90 | ## Combine query parameters from URI and cmdlet parameters 91 | if ($uriQueryEndpoint.Query) { 92 | [hashtable] $finalQueryParameters = ConvertFrom-QueryString $uriQueryEndpoint.Query -AsHashtable 93 | if ($QueryParameters) { 94 | foreach ($ParameterName in $QueryParameters.Keys) { 95 | $finalQueryParameters[$ParameterName] = $QueryParameters[$ParameterName] 96 | } 97 | } 98 | } 99 | elseif ($QueryParameters) { [hashtable] $finalQueryParameters = $QueryParameters } 100 | else { [hashtable] $finalQueryParameters = @{ } } 101 | if ($Select) { $finalQueryParameters['$select'] = $Select -join ',' } 102 | if ($Filter) { $finalQueryParameters['$filter'] = $Filter } 103 | $uriQueryEndpoint.Query = ConvertTo-QueryString $finalQueryParameters 104 | 105 | ## Invoke graph requests individually or save for single batch request 106 | foreach ($id in $UniqueId) { 107 | $uriQueryEndpointFinal = New-Object System.UriBuilder -ArgumentList $uriQueryEndpoint.Uri 108 | $uriQueryEndpointFinal.Path = ([IO.Path]::Combine($uriQueryEndpointFinal.Path, $id)) 109 | 110 | if (!$DisableBatching -and ($RelativeUri.Count -gt 1 -or $UniqueId.Count -gt 1)) { 111 | ## Create batch request entry 112 | $request = New-Object PSObject -Property @{ 113 | id = $listRequests.Count #(New-Guid).ToString() 114 | method = 'GET' 115 | url = $uriQueryEndpointFinal.Uri.AbsoluteUri -replace ('{0}{1}/' -f $GraphBaseUri.AbsoluteUri, $ApiVersion) 116 | headers = @{ ConsistencyLevel = $ConsistencyLevel } 117 | } 118 | $listRequests.Add($request) 119 | } 120 | else { 121 | ## Get results 122 | $results = Invoke-MgGraphRequest -Method GET -Uri $uriQueryEndpointFinal.Uri.AbsoluteUri -Headers @{ ConsistencyLevel = $ConsistencyLevel } -OutputType PSObject 123 | Format-Result $results $DisablePaging 124 | Complete-Result $results $DisablePaging 125 | } 126 | } 127 | } 128 | } 129 | 130 | end { 131 | if ($listRequests.Count -gt 0) { 132 | $uriQueryEndpoint = New-Object System.UriBuilder -ArgumentList ([IO.Path]::Combine($GraphBaseUri.AbsoluteUri, $ApiVersion, '$batch')) 133 | for ($iRequest = 0; $iRequest -lt $listRequests.Count; $iRequest += $BatchSize) { 134 | $indexEnd = [System.Math]::Min($iRequest + $BatchSize - 1, $listRequests.Count - 1) 135 | $jsonRequests = New-Object psobject -Property @{ requests = $listRequests[$iRequest..$indexEnd] } | ConvertTo-Json -Depth 5 136 | Write-Debug $jsonRequests 137 | 138 | $resultsBatch = Invoke-MgGraphRequest -Method POST -Uri $uriQueryEndpoint.Uri.AbsoluteUri -Body $jsonRequests -OutputType PSObject 139 | $resultsBatch = $resultsBatch.responses | Sort-Object -Property id 140 | 141 | foreach ($results in ($resultsBatch.body)) { 142 | Format-Result $results $DisablePaging 143 | Complete-Result $results $DisablePaging 144 | } 145 | } 146 | } 147 | } 148 | } --------------------------------------------------------------------------------