mirror of
https://github.com/cisagov/ScubaGoggles.git
synced 2026-02-07 00:36:07 +01:00
Initial commit
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
**/*.psd1 diff
|
||||
34
.github/workflows/run_opa_tests.yaml
vendored
Normal file
34
.github/workflows/run_opa_tests.yaml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Run OPA Tests
|
||||
on:
|
||||
# Run tests on each commit, newly opened/reopened PR, and
|
||||
# PR review submission (e.g. approval)
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "**.rego"
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "**.rego"
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
Run-OPA-Tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup OPA
|
||||
uses: open-policy-agent/setup-opa@v2
|
||||
with:
|
||||
version: <0.50
|
||||
|
||||
- name: Run OPA Check
|
||||
run: opa check Rego Testing/Unit/Rego --strict
|
||||
|
||||
- name: Run OPA Tests
|
||||
run: opa test Rego/*.rego Testing/Unit/Rego/**/*.rego -v
|
||||
42
.github/workflows/run_powershell_tests.yaml
vendored
Normal file
42
.github/workflows/run_powershell_tests.yaml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Run PowerShell Tests
|
||||
on:
|
||||
# Run tests on each commit, newly opened/reopened PR, and
|
||||
# PR review submission (e.g. approval)
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "**.ps1"
|
||||
- "**.psm1"
|
||||
- ".github/workflows/run_powershell_tests.yaml"
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "**.ps1"
|
||||
- "**.psm1"
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
Run-PowerShell-Tests:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Remove Graph 2.0
|
||||
shell: powershell
|
||||
run: |
|
||||
# Remove Microsoft.Graph module(s) from image until SCUBA steps up to 2.0+
|
||||
Write-Output "NOTICE: Removing Microsoft.Graph version 2.0. Remove this step when SCuBA steps up to this version."
|
||||
Uninstall-Module Microsoft.Graph -ErrorAction SilentlyContinue
|
||||
Get-InstalledModule Microsoft.Graph.* | %{ if($_.Name -ne "Microsoft.Graph.Authentication"){ Write-Output "Removing: $($_.Name)"; Uninstall-Module $_.Name -AllowPrerelease -AllVersions } }
|
||||
Uninstall-Module Microsoft.Graph.Authentication -AllowPrerelease -AllVersions
|
||||
|
||||
- name: Run Pester Tests
|
||||
if: '!cancelled()'
|
||||
shell: powershell
|
||||
run: |
|
||||
./SetUp.ps1
|
||||
Invoke-Pester -Output 'Detailed' -Path './Testing/Unit/PowerShell'
|
||||
34
.github/workflows/run_psscriptanalyzer.yaml
vendored
Normal file
34
.github/workflows/run_psscriptanalyzer.yaml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Run PS Linter
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "**.ps1"
|
||||
- "**.psm1"
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "**.ps1"
|
||||
- "**.psm1"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Run PS Linter
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: lint
|
||||
uses: docker://devblackops/github-action-psscriptanalyzer:2.4.0
|
||||
with:
|
||||
repoToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
settingsPath: PSScriptAnalyzerSettings.psd1
|
||||
failOnErrors: true
|
||||
failOnWarnings: true
|
||||
failOnInfos: true
|
||||
sendComment: true
|
||||
66
.github/workflows/run_release.yaml
vendored
Normal file
66
.github/workflows/run_release.yaml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
releaseName:
|
||||
description: "Release Name"
|
||||
required: true
|
||||
type: string
|
||||
version:
|
||||
description: "Release Version (e.g., 1.2.4)"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
name: Build and Sign Release
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
CODESIGN_PW: ${{ secrets.CODESIGN_PW }}
|
||||
CODESIGN_PFX: ${{ secrets.CODESIGN_PFX }}
|
||||
RELEASE_VERSION: ${{ inputs.version }}
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: repo
|
||||
- name: Sign Scripts
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$PSDefaultParameterValues['*:ErrorAction']='Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
|
||||
New-Item -ItemType directory -Path certificate
|
||||
Set-Content -Path certificate\cert.txt -Value $env:CODESIGN_PFX
|
||||
certutil -decode certificate\cert.txt certificate\cert.pfx
|
||||
|
||||
$cert = Get-PfxCertificate -FilePath certificate\cert.pfx -Password (ConvertTo-SecureString -String $env:CODESIGN_PW -Force -AsPlainText)
|
||||
|
||||
Get-ChildItem -Recurse -Path repo -Include **.ps1,**.psm1,**.psd1 | ForEach-Object {
|
||||
$path = $_.FullName
|
||||
Set-AuthenticodeSignature -Certificate $cert -FilePath $path -TimestampServer "http://timestamp.digicert.com/" -IncludeChain NotRoot -HashAlgorithm SHA256
|
||||
# Delay for 4 seconds to avoid exceeding rate limits (1000 / 5 minutes, 100 / 5 seconds)
|
||||
Start-Sleep -Seconds 4
|
||||
}
|
||||
|
||||
Remove-Item -Recurse -Force certificate
|
||||
Remove-Item -Recurse -Force repo -Include .git*
|
||||
|
||||
Move-Item -Path repo -Destination "ScubaGear-${env:RELEASE_VERSION}" -Force
|
||||
Compress-Archive -Path "ScubaGear-${env:RELEASE_VERSION}" -DestinationPath "ScubaGear-${env:RELEASE_VERSION}.zip"
|
||||
|
||||
Get-ChildItem -Path . | Write-Output
|
||||
- name: release
|
||||
uses: softprops/action-gh-release@v1
|
||||
id: create_release
|
||||
with:
|
||||
draft: true
|
||||
prerelease: false
|
||||
name: ${{ inputs.releaseName }}
|
||||
tag_name: v${{ inputs.version }}
|
||||
files: ScubaGear-${{ inputs.version }}.zip
|
||||
generate_release_notes: true
|
||||
fail_on_unmatched_files: true
|
||||
86
.github/workflows/run_smoke_test.yaml
vendored
Normal file
86
.github/workflows/run_smoke_test.yaml
vendored
Normal file
@@ -0,0 +1,86 @@
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
branches:
|
||||
- "main"
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
push:
|
||||
paths:
|
||||
- ".github/workflows/run_smoke_test.yaml"
|
||||
branches:
|
||||
- "main"
|
||||
- "*smoke*"
|
||||
|
||||
name: Smoke Test
|
||||
|
||||
jobs:
|
||||
Run-Smoke-Test:
|
||||
runs-on: windows-latest
|
||||
env:
|
||||
SCUBA_GITHUB_AUTOMATION_CREDS: ${{ secrets.SCUBA_GITHUB_AUTOMATION_CREDS }}
|
||||
defaults:
|
||||
run:
|
||||
shell: powershell
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repo code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Remove Graph 2.0
|
||||
shell: powershell
|
||||
run: |
|
||||
# Remove Microsoft.Graph module(s) from image until SCUBA steps up to 2.0+
|
||||
Write-Output "NOTICE: Removing Microsoft.Graph version 2.0. Remove this step when SCuBA steps up to this version."
|
||||
Uninstall-Module Microsoft.Graph -ErrorAction SilentlyContinue
|
||||
Get-InstalledModule Microsoft.Graph.* | %{ if($_.Name -ne "Microsoft.Graph.Authentication"){ Write-Output "Removing: $($_.Name)"; Uninstall-Module $_.Name -AllowPrerelease -AllVersions } }
|
||||
Uninstall-Module Microsoft.Graph.Authentication -AllowPrerelease -AllVersions
|
||||
|
||||
- name: Execute ScubaGear and Check Outputs
|
||||
run: |
|
||||
. Testing/Functional/SmokeTest/SmokeTestUtils.ps1
|
||||
./AllowBasicAuthentication.ps1 -RunAsAdmin
|
||||
|
||||
##### Install all the dependencies
|
||||
Install-SmokeTestExternalDependencies
|
||||
|
||||
# ScubaGear currently requires the provisioning of a certificate for using a ServicePrinicpal, rather than
|
||||
# using Workload Identity Federation, which would ordinarily be preferred for calling Microsoft APIs from
|
||||
# GitHub actions.
|
||||
$AUTOMATION_CREDS = $env:SCUBA_GITHUB_AUTOMATION_CREDS | ConvertFrom-Json
|
||||
$TestTenants = $AUTOMATION_CREDS.TestTenants
|
||||
Write-Output "Identified $($TestTenants.Count) Test Tenants"
|
||||
|
||||
$TestContainers = @()
|
||||
ForEach ($TestTenantObj in $TestTenants){
|
||||
$Properties = Get-Member -InputObject $TestTenantObj -MemberType NoteProperty
|
||||
$TestTenant = $TestTenantObj | Select-Object -ExpandProperty $Properties.Name
|
||||
$OrgName = $TestTenant.DisplayName
|
||||
$DomainName = $TestTenant.DomainName
|
||||
$AppId = $TestTenant.AppId
|
||||
$PlainTextPassword = $TestTenant.CertificatePassword
|
||||
$CertPwd = ConvertTo-SecureString -String $PlainTextPassword -Force -AsPlainText
|
||||
$M365Env = $TestTenant.M365Env
|
||||
try {
|
||||
$Result = New-ServicePrincipalCertificate `
|
||||
-EncodedCertificate $TestTenant.CertificateB64 `
|
||||
-CertificatePassword $CertPwd
|
||||
$Thumbprint = $Result[-1]
|
||||
}
|
||||
catch {
|
||||
Write-Output "Failed to install certificate for $OrgName"
|
||||
}
|
||||
|
||||
$TestContainers += New-PesterContainer `
|
||||
-Path "Testing/Functional/SmokeTest/SmokeTest001.Tests.ps1" `
|
||||
-Data @{ Thumbprint = $Thumbprint; Organization = $DomainName; AppId = $AppId; M365Environment = $M365Env }
|
||||
$TestContainers += New-PesterContainer `
|
||||
-Path "Testing/Functional/SmokeTest/SmokeTest002.Tests.ps1" `
|
||||
-Data @{ OrganizationDomain = $DomainName; OrganizationName = $OrgName }
|
||||
}
|
||||
|
||||
Invoke-Pester -Container $TestContainers -Output Detailed
|
||||
|
||||
Remove-MyCertificates
|
||||
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# dependencies
|
||||
|
||||
# testing
|
||||
|
||||
# production
|
||||
|
||||
# msc
|
||||
*.xml
|
||||
*.cer
|
||||
*.exe
|
||||
/M365/*.xml
|
||||
/PowerShell/output
|
||||
/PowerShell/example
|
||||
/PowerShell/M365Baseline*
|
||||
/output
|
||||
/example
|
||||
/M365Baseline*
|
||||
/Reports*
|
||||
/utils/Reports*
|
||||
/utils/output
|
||||
/utils/M365Baseline*
|
||||
|
||||
# IDE
|
||||
/.vscode
|
||||
|
||||
# Reports
|
||||
**/M365BaselineConformance*
|
||||
/Testing/Functional/Reports*
|
||||
/Testing/Functional/Archive*
|
||||
57
AllowBasicAuthentication.ps1
Normal file
57
AllowBasicAuthentication.ps1
Normal file
@@ -0,0 +1,57 @@
|
||||
#Requires -RunAsAdministrator
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Set Registry to allow basic authentication for WinRM Client
|
||||
|
||||
.DESCRIPTION
|
||||
Run this script to enable basic authentication on your local desktop if you get an error when connecting to Exchange Online.
|
||||
|
||||
.NOTES
|
||||
See README file Troubleshooting section for details.
|
||||
This script requires administrative privileges on your local desktop and updates a registry key.
|
||||
#>
|
||||
|
||||
function Test-RegistryKey {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Test if registry key exists
|
||||
#>
|
||||
param (
|
||||
[parameter (Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]$Path,
|
||||
[parameter (Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]$Key
|
||||
)
|
||||
|
||||
try {
|
||||
Get-ItemProperty -Path $Path -Name $Key -ErrorAction Stop | Out-Null
|
||||
return $true
|
||||
}
|
||||
catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
$regPath = 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Client'
|
||||
$regKey = 'AllowBasic'
|
||||
|
||||
if (-Not $(Test-Path -LiteralPath $regPath)) {
|
||||
New-Item -Path $regPath -Force | Out-Null
|
||||
New-ItemProperty -Path $regPath -Name $regKey | Out-Null
|
||||
} elseif (-Not $(Test-RegistryKey -Path $regPath -Key $regKey)) {
|
||||
New-ItemProperty -Path $regPath -Name $regKey | Out-Null
|
||||
}
|
||||
|
||||
try {
|
||||
$allowBasic = Get-ItemPropertyValue -Path $regPath -Name $regKey -ErrorAction Stop
|
||||
|
||||
if ($allowBasic -ne '1') {
|
||||
Set-ItemProperty -Path $regPath -Name $regKey -Type DWord -Value '1'
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error -Message "Unexpected error occured attempting to update registry key, $regKey."
|
||||
}
|
||||
|
||||
|
||||
349
CONTENTSTYLEGUIDE.md
Normal file
349
CONTENTSTYLEGUIDE.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# Content style guide for SCuBA <!-- omit in toc -->
|
||||
|
||||
Welcome to the content style guide for ScubaGear
|
||||
|
||||
These guidelines are specific to style rules for PowerShell and OPA Rego code. For general style questions or guidance on topics not covered here, ask or go with best guess and bring up at a later meeting.
|
||||
|
||||
Use menu icon on the top left corner of this document to get to a specific section of this guide quickly.
|
||||
|
||||
## The SCuBA approach to style
|
||||
|
||||
- Our style guide aims for simplicity. Guidelines should be easy to apply to a range of scenarios.
|
||||
- Decisions aren’t about what’s right or wrong according to the rules, but about what’s best practice and improves readability. We're flexible and open to change while maintaining consistency.
|
||||
- When making a style or structure decision, we consider the readability, maintainability and ability for consitancy in a range of situations.
|
||||
- When a question specific to help documentation isn’t covered by the style guide, we think it through using these principles, then make a decision and bring it up in the next meeting for deliberation.
|
||||
|
||||
## OPA Rego
|
||||
|
||||
Because there isn't a standard style guide for the Rego language, we are creating one from scratch. For consistency, we will be using many of the same style rules as PowerShell. There are also a few best practice rules that this program will follow. These best practices were deliberated on and chosen to enhance readability. We recognize that the code is in a constant state of improvement, so the best practices are subject to change.
|
||||
|
||||
### Test Cases
|
||||
|
||||
Test names will use the syntax `test_mainVar_In/correct_*V#` to support brevity in naming that highlights the primary variable being tested. Furthermore, for tests with more than one version, the first test will also include a version as `_V1`. Consistent use of a version number promotes clarity and signals the presence of multiple test versions to reviewers. Version numbers are not used if there is only a single test of a given variable and type (Correct/Incorrect)
|
||||
|
||||
```
|
||||
test_ExampleVar_Correct_V1 if {
|
||||
PolicyId := "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>"
|
||||
|
||||
Output := tests with input as {
|
||||
"example_policies" : [
|
||||
{
|
||||
"Example3" : "ExampleString",
|
||||
"Example2" : false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
RuleOutput := [Result | Result = Output[_]; Result.PolicyId == PolicyId]
|
||||
|
||||
count(RuleOutput) == 1
|
||||
RuleOutput[0].RequirementMet
|
||||
RuleOutput[0].ReportDetails == "Example output"
|
||||
}
|
||||
|
||||
test_ExampleVar_Correct_V2 if {
|
||||
...
|
||||
}
|
||||
|
||||
test_ExampleVar_Incorrect if {
|
||||
PolicyId := "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>"
|
||||
|
||||
Output := tests with input as {
|
||||
"example_policies" : [
|
||||
{
|
||||
"Example3" : "ExampleString",
|
||||
"Example2" : true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
RuleOutput := [Result | Result = Output[_]; Result.PolicyId == PolicyId]
|
||||
|
||||
count(RuleOutput) == 1
|
||||
not RuleOutput[0].RequirementMet
|
||||
RuleOutput[0].ReportDetails == "Example output"
|
||||
}
|
||||
```
|
||||
|
||||
### Not Implemented
|
||||
|
||||
If the policy bullet point is untestable at this time, use the templates below.
|
||||
|
||||
#### Config
|
||||
|
||||
The first one directs the user to the baseline document for manual checking. The second instructs the user to run a different script because the test is in another version. However, if they are unable to run the other script, they are also directed to the baseline like in the first template.
|
||||
|
||||
```
|
||||
# At this time we are unable to test for X because of Y
|
||||
tests[{
|
||||
"PolicyId" : PolicyId,
|
||||
"Criticality" : "Shall/Not-Implemented",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : NotCheckedDetails(PolicyId),
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
PolicyId := "MS.<Product>.<Control Group #>.<Control #>v<Version #>"
|
||||
true
|
||||
}
|
||||
```
|
||||
|
||||
```
|
||||
# At this time we are unable to test for X because of Y
|
||||
tests[{
|
||||
"PolicyId" : "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
```
|
||||
#### Testing
|
||||
|
||||
```
|
||||
test_NotImplemented_Correct if {
|
||||
PolicyId := "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>"
|
||||
|
||||
Output := tests with input as { }
|
||||
|
||||
RuleOutput := [Result | Result = Output[_]; Result.PolicyId == PolicyId]
|
||||
|
||||
count(RuleOutput) == 1
|
||||
not RuleOutput[0].RequirementMet
|
||||
RuleOutput[0].ReportDetails == NotCheckedDetails(PolicyId)
|
||||
}
|
||||
```
|
||||
```
|
||||
test_3rdParty_Correct_V1 if {
|
||||
PolicyId := "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>"
|
||||
|
||||
Output := tests with input as { }
|
||||
|
||||
RuleOutput := [Result | Result = Output[_]; Result.PolicyId == PolicyId]
|
||||
|
||||
count(RuleOutput) == 1
|
||||
not RuleOutput[0].RequirementMet
|
||||
RuleOutput[0].ReportDetails == "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check"
|
||||
}
|
||||
```
|
||||
|
||||
### Naming
|
||||
|
||||
PascalCase - capitalize the first letter of each word. This is the same naming convention that is used for PowerShell.
|
||||
|
||||
```
|
||||
ExampleVariable := true
|
||||
```
|
||||
|
||||
### Brackets
|
||||
|
||||
One True Brace - requires that every braceable statement should have the opening brace on the end of a line, and the closing brace at the beginning of a line. This is the same bracket style that is used for PowerShell.
|
||||
|
||||
```
|
||||
test_Example_Correct if {
|
||||
PolicyId := "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>"
|
||||
|
||||
Output := tests with input as {
|
||||
"example_tag" : {
|
||||
"ExampleVar" : false
|
||||
}
|
||||
}
|
||||
|
||||
RuleOutput := [Result | Result = Output[_]; Result.PolicyId == PolicyId]
|
||||
|
||||
count(RuleOutput) == 1
|
||||
RuleOutput[0].RequirementMet
|
||||
RuleOutput[0].ReportDetails == "Requirement met"
|
||||
}
|
||||
```
|
||||
|
||||
### Indentation
|
||||
|
||||
Indentation will be set at 4 spaces, make sure your Tabs == 4 spaces. We are working on finding a tool that will replace Tabs with spaces and clean up additional spacing mistakes. Until then it is checked manually in code review. Be kind to your reviewer!
|
||||
|
||||
### Spacing
|
||||
|
||||
1) A blank line between each major variable: references & rules
|
||||
|
||||
```
|
||||
Example[Example.Id] {
|
||||
Example := input.ExampleVar[_]
|
||||
Example.State == "Enabled"
|
||||
}
|
||||
|
||||
tests[{
|
||||
"PolicyId" : "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : "Example-Command",
|
||||
"ActualValue" : ExampleVar.ExampleSetting,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
ExampleVar := input.ExampleVar
|
||||
Status := ExampleVar == 15
|
||||
}
|
||||
|
||||
tests[{
|
||||
"PolicyId" : "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>",,
|
||||
...
|
||||
```
|
||||
|
||||
2) Two blank lines between subsections
|
||||
|
||||
```
|
||||
tests[{
|
||||
"PolicyId" : "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>",,
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : "Example-Command",
|
||||
"ActualValue" : ExampleVar.ExampleSetting,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
ExampleVar := input.ExampleVar
|
||||
Status := ExampleVar == 15
|
||||
}
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.2 #
|
||||
################
|
||||
...
|
||||
```
|
||||
|
||||
### Comments
|
||||
|
||||
1) Indicate beginning of every policy: 1, 2, etc.
|
||||
|
||||
```
|
||||
###################
|
||||
# MS.<Product>.1 #
|
||||
###################
|
||||
```
|
||||
2) Indicate the beginning of every policy bullet point.
|
||||
|
||||
```
|
||||
#
|
||||
# MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>
|
||||
#--
|
||||
```
|
||||
|
||||
3) Indicate the end of every policy bullet point.
|
||||
|
||||
```
|
||||
#--
|
||||
```
|
||||
|
||||
4) Indicate why placeholder test is blank/untestable
|
||||
|
||||
```
|
||||
# At this time we are unable to test for X because of Y
|
||||
```
|
||||
|
||||
### Booleans
|
||||
|
||||
In the interest of consistency across policy tests and human readability of the test, boolean-valued variables should be set via a comparison test against a boolean constant (true/false).
|
||||
|
||||
#### Correct
|
||||
|
||||
```
|
||||
tests[{
|
||||
"PolicyId" : "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>",,
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : "Example-Command",
|
||||
"ActualValue" : ExampleVar.ExampleSetting,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
ExampleVar := input.ExampleVar
|
||||
Status := ExampleVar == true
|
||||
}
|
||||
|
||||
tests[{
|
||||
"PolicyId" : "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>",,
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : "Example-Command",
|
||||
"ActualValue" : ExampleVar.ExampleSetting,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
ExampleVar := input.ExampleVar
|
||||
Status := ExampleVar == false
|
||||
}
|
||||
```
|
||||
|
||||
#### Incorrect
|
||||
|
||||
```
|
||||
tests[{
|
||||
...
|
||||
}] {
|
||||
ExampleVar := input.ExampleVar
|
||||
Status := ExampleVar # Mising == true
|
||||
}
|
||||
|
||||
tests[{
|
||||
...
|
||||
}] {
|
||||
ExampleVar := input.ExampleVar
|
||||
Status := ExampleVar == false
|
||||
}
|
||||
```
|
||||
|
||||
### Taking input
|
||||
|
||||
We will always store the input in a variable first thing. It can sometimes be easier to only use `input.ExampleVar` repeatedly, but for consistancy this is best practice. the `[_]` is added to the end for when you are anticipating an array, so the program has to loop through the input. There are other ways to take in input, but OPA Documents states `input.VariableName` is recommended. As such we will only use this method for consistancy. If there is a problem, it can be taken up on a case by case basis for disscussion.
|
||||
|
||||
```
|
||||
tests[{
|
||||
...
|
||||
}] {
|
||||
ExampleVar := input.ExampleVar[_]
|
||||
Status := "Example" in ExampleVar
|
||||
}
|
||||
|
||||
tests[{
|
||||
...
|
||||
}] {
|
||||
ExampleVar := input.ExampleVar
|
||||
Status := ExampleVar == true
|
||||
}
|
||||
```
|
||||
|
||||
### ActualValue
|
||||
|
||||
It can be tempting to put the status variable in the ActualValue spot when you are anticipating a boolean. DON'T! For consistancy and as best practice put `ExampleVar.ExampleSetting`.
|
||||
|
||||
#### InCorrect
|
||||
```
|
||||
tests[{
|
||||
"PolicyId" : "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>",,
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : "Example-Command",
|
||||
"ActualValue" : Status,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
ExampleVar := input.ExampleVar
|
||||
Status := ExampleVar == true
|
||||
}
|
||||
```
|
||||
|
||||
#### Correct
|
||||
```
|
||||
tests[{
|
||||
"PolicyId" : "MS.<Product>.<Policy #>.<Bulletpoint #>v<Version #>",,
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : "Example-Command",
|
||||
"ActualValue" : ExampleVar.ExampleSetting,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
ExampleVar := input.ExampleVar
|
||||
Status := ExampleVar == true
|
||||
}
|
||||
```
|
||||
## PowerShell
|
||||
[PoshCode's The PowerShell Best Practices and Style Guide](https://github.com/PoshCode/PowerShellPracticeAndStyle)
|
||||
121
LICENSE
Normal file
121
LICENSE
Normal file
@@ -0,0 +1,121 @@
|
||||
Creative Commons Legal Code
|
||||
|
||||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
64
OPA.ps1
Normal file
64
OPA.ps1
Normal file
@@ -0,0 +1,64 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
This script installs the required OPA executable used by the
|
||||
assessment tool
|
||||
.DESCRIPTION
|
||||
Installs the OPA executable required to support SCuBAGear.
|
||||
.EXAMPLE
|
||||
.\OPA.ps1
|
||||
#>
|
||||
# Set prefernces for writing messages
|
||||
$DebugPreference = "Continue"
|
||||
$InformationPreference = "Continue"
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# Set expected version and OutFile path
|
||||
$ExpectedVersion = "0.42.1"
|
||||
$OPAExe = "opa_windows_amd64.exe"
|
||||
$InstallUrl = "https://openpolicyagent.org/downloads/v$($ExpectedVersion)/$OPAExe"
|
||||
$OutFile=(Join-Path (Get-Location).Path $InstallUrl.SubString($InstallUrl.LastIndexOf('/')))
|
||||
$ExpectedHash ="5D71028FED935DC98B9D69369D42D2C03CE84A7720D61ED777E10AAE7528F399"
|
||||
|
||||
# Download files
|
||||
try {
|
||||
Write-Information "Downloading $InstallUrl"
|
||||
$WebClient = New-Object System.Net.WebClient
|
||||
$WebClient.DownloadFile($InstallUrl, $OutFile)
|
||||
Write-Information ""
|
||||
Write-Information "`nDownload of `"$OutFile`" finished."
|
||||
}
|
||||
catch {
|
||||
Write-Error "An error has occurred: Unable to download OPA executable. To try manually downloading, see details in README under 'Download the required OPA executable'"
|
||||
}
|
||||
finally {
|
||||
$WebClient.Dispose()
|
||||
}
|
||||
|
||||
# Hash checks
|
||||
if ((Get-FileHash .\opa_windows_amd64.exe).Hash -eq $ExpectedHash)
|
||||
{
|
||||
Write-Information "SHA256 verified successfully"
|
||||
}
|
||||
else {
|
||||
Write-Information "SHA256 verification failed, retry download or install manually. See README under 'Download the required OPA executable' for instructions."
|
||||
}
|
||||
# Version checks
|
||||
Try {
|
||||
$OPAArgs = @('version')
|
||||
$InstalledVersion= $(& "./$($OPAExe)" @OPAArgs) | Select-Object -First 1
|
||||
if ($InstalledVersion -eq "Version: $($ExpectedVersion)")
|
||||
{
|
||||
Write-Information "`Downloaded OPA version` `"$InstalledVersion`" meets the ScubaGear requirement"
|
||||
}
|
||||
else {
|
||||
Write-Information "`Downloaded OPA version` `"$InstalledVersion`" does not meet the ScubaGear requirement of` `"$ExpectedVersion`""
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error "Unable to verify the current OPA version: please see details on manual installation in the README under 'Download the required OPA executable'"
|
||||
}
|
||||
|
||||
$DebugPreference = "SilientlyContinue"
|
||||
$InformationPreference = "SilientlyContinue"
|
||||
$ErrorActionPreference = "Continue"
|
||||
8
PSScriptAnalyzerSettings.psd1
Normal file
8
PSScriptAnalyzerSettings.psd1
Normal file
@@ -0,0 +1,8 @@
|
||||
# PSScriptAnalyzerSettings.psd1
|
||||
@{
|
||||
Severity=@('Error','Warning','Information')
|
||||
ExcludeRules=@(
|
||||
'PSUseSingularNouns',
|
||||
'PSUseShouldProcessForStateChangingFunctions',
|
||||
'PSUseOutputTypeCorrectly')
|
||||
}
|
||||
41
PowerShell/ScubaGear/Dependencies.ps1
Normal file
41
PowerShell/ScubaGear/Dependencies.ps1
Normal file
@@ -0,0 +1,41 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
This script verifies the required Powershell modules used by the
|
||||
assessment tool are installed.
|
||||
.DESCRIPTION
|
||||
Verifies a supported version of the modules required to support SCuBAGear are installed.
|
||||
#>
|
||||
|
||||
$RequiredModulesPath = Join-Path -Path $PSScriptRoot -ChildPath "RequiredVersions.ps1"
|
||||
if (Test-Path -Path $RequiredModulesPath){
|
||||
. $RequiredModulesPath
|
||||
}
|
||||
|
||||
if (!$ModuleList){
|
||||
throw "Required modules list is required."
|
||||
}
|
||||
|
||||
foreach ($Module in $ModuleList) {
|
||||
$InstalledModuleVersions = Get-Module -ListAvailable -Name $($Module.ModuleName)
|
||||
$FoundAcceptableVersion = $false
|
||||
|
||||
foreach ($ModuleVersion in $InstalledModuleVersions){
|
||||
|
||||
if (($ModuleVersion.Version -ge $Module.ModuleVersion) -and ($ModuleVersion.Version -le $Module.MaximumVersion)){
|
||||
$FoundAcceptableVersion = $true
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $FoundAcceptableVersion) {
|
||||
throw [System.IO.FileNotFoundException] "No acceptable installed version found for module: $($Module.ModuleName)
|
||||
Required Min Version: $($Module.ModuleVersion) | Max Version: $($Module.MaximumVersion)
|
||||
Run Get-InstalledModule to see a list of currently installed modules
|
||||
Run SetUp.ps1 or Install-Module $($Module.ModuleName) -Force -MaximumVersion $($Module.MaximumVersion) to install the latest acceptable version of $($Module.ModuleName)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
82
PowerShell/ScubaGear/Modules/Connection/ConnectHelpers.psm1
Normal file
82
PowerShell/ScubaGear/Modules/Connection/ConnectHelpers.psm1
Normal file
@@ -0,0 +1,82 @@
|
||||
function Connect-EXOHelper {
|
||||
<#
|
||||
.Description
|
||||
This function is used for assisting in connecting to different M365 Environments for EXO.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$M365Environment,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[hashtable]
|
||||
$ServicePrincipalParams
|
||||
)
|
||||
$EXOParams = @{
|
||||
ErrorAction = "Stop";
|
||||
ShowBanner = $false;
|
||||
}
|
||||
switch ($M365Environment) {
|
||||
"gcchigh" {
|
||||
$EXOParams += @{'ExchangeEnvironmentName' = "O365USGovGCCHigh";}
|
||||
}
|
||||
"dod" {
|
||||
$EXOParams += @{'ExchangeEnvironmentName' = "O365USGovDoD";}
|
||||
}
|
||||
}
|
||||
|
||||
if ($ServicePrincipalParams.CertThumbprintParams) {
|
||||
$EXOParams += $ServicePrincipalParams.CertThumbprintParams
|
||||
}
|
||||
Connect-ExchangeOnline @EXOParams | Out-Null
|
||||
}
|
||||
|
||||
function Connect-DefenderHelper {
|
||||
<#
|
||||
.Description
|
||||
This function is used for assisting in connecting to different M365 Environments for EXO.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$M365Environment,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[hashtable]
|
||||
$ServicePrincipalParams
|
||||
)
|
||||
$IPPSParams = @{
|
||||
'ErrorAction' = 'Stop';
|
||||
}
|
||||
switch ($M365Environment) {
|
||||
"gcchigh" {
|
||||
$IPPSParams += @{'ConnectionUri' = "https://ps.compliance.protection.office365.us/powershell-liveid";}
|
||||
$IPPSParams += @{'AzureADAuthorizationEndpointUri' = "https://login.microsoftonline.us/common";}
|
||||
}
|
||||
"dod" {
|
||||
$IPPSParams += @{'ConnectionUri' = "https://l5.ps.compliance.protection.office365.us/powershell-liveid";}
|
||||
$IPPSParams += @{'AzureADAuthorizationEndpointUri' = "https://login.microsoftonline.us/common";}
|
||||
}
|
||||
}
|
||||
if ($ServicePrincipalParams.CertThumbprintParams) {
|
||||
$IPPSParams += $ServicePrincipalParams.CertThumbprintParams
|
||||
}
|
||||
Connect-IPPSSession @IPPSParams | Out-Null
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
'Connect-EXOHelper',
|
||||
'Connect-DefenderHelper'
|
||||
)
|
||||
320
PowerShell/ScubaGear/Modules/Connection/Connection.psm1
Normal file
320
PowerShell/ScubaGear/Modules/Connection/Connection.psm1
Normal file
@@ -0,0 +1,320 @@
|
||||
function Connect-Tenant {
|
||||
<#
|
||||
.Description
|
||||
This function uses the various PowerShell modules to establish
|
||||
a connection to an M365 Tenant associated with provided
|
||||
credentials
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive", IgnoreCase = $false)]
|
||||
[string[]]
|
||||
$ProductNames,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
|
||||
[string]
|
||||
$M365Environment,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[hashtable]
|
||||
$ServicePrincipalParams
|
||||
)
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "ConnectHelpers.psm1")
|
||||
|
||||
# Prevent duplicate sign ins
|
||||
$EXOAuthRequired = $true
|
||||
$SPOAuthRequired = $true
|
||||
$AADAuthRequired = $true
|
||||
|
||||
$ProdAuthFailed = @()
|
||||
|
||||
$N = 0
|
||||
$Len = $ProductNames.Length
|
||||
|
||||
foreach ($Product in $ProductNames) {
|
||||
$N += 1
|
||||
$Percent = $N*100/$Len
|
||||
$ProgressParams = @{
|
||||
'Activity' = "Authenticating to each Product";
|
||||
'Status' = "Authenticating to $($Product); $($N) of $($Len) Products authenticated to.";
|
||||
'PercentComplete' = $Percent;
|
||||
}
|
||||
Write-Progress @ProgressParams
|
||||
try {
|
||||
switch ($Product) {
|
||||
"aad" {
|
||||
$GraphScopes = (
|
||||
'User.Read.All',
|
||||
'Policy.Read.All',
|
||||
'Organization.Read.All',
|
||||
'UserAuthenticationMethod.Read.All',
|
||||
'RoleManagement.Read.Directory',
|
||||
'GroupMember.Read.All',
|
||||
'Directory.Read.All'
|
||||
)
|
||||
$GraphParams = @{
|
||||
'ErrorAction' = 'Stop';
|
||||
}
|
||||
if ($ServicePrincipalParams.CertThumbprintParams) {
|
||||
$GraphParams += @{
|
||||
CertificateThumbprint = $ServicePrincipalParams.CertThumbprintParams.CertificateThumbprint;
|
||||
ClientID = $ServicePrincipalParams.CertThumbprintParams.AppID;
|
||||
TenantId = $ServicePrincipalParams.CertThumbprintParams.Organization; # Organization also works here
|
||||
}
|
||||
}
|
||||
else {
|
||||
$GraphParams += @{Scopes = $GraphScopes;}
|
||||
}
|
||||
switch ($M365Environment) {
|
||||
"gcchigh" {
|
||||
$GraphParams += @{'Environment' = "USGov";}
|
||||
}
|
||||
"dod" {
|
||||
$GraphParams += @{'Environment' = "USGovDoD";}
|
||||
}
|
||||
}
|
||||
Connect-MgGraph @GraphParams | Out-Null
|
||||
$GraphProfile = (Get-MgProfile -ErrorAction "Stop").Name
|
||||
if ($GraphProfile.ToLower() -ne "beta") {
|
||||
Select-MgProfile -Name "Beta" -ErrorAction "Stop" | Out-Null
|
||||
}
|
||||
$AADAuthRequired = $false
|
||||
}
|
||||
{($_ -eq "exo") -or ($_ -eq "defender")} {
|
||||
if ($EXOAuthRequired) {
|
||||
$EXOHelperParams = @{
|
||||
M365Environment = $M365Environment;
|
||||
}
|
||||
if ($ServicePrincipalParams) {
|
||||
$EXOHelperParams += @{ServicePrincipalParams = $ServicePrincipalParams}
|
||||
}
|
||||
Write-Verbose "Defender will require a sign in every single run regardless of what the LogIn parameter is set"
|
||||
Connect-EXOHelper @EXOHelperParams
|
||||
$EXOAuthRequired = $false
|
||||
}
|
||||
}
|
||||
"powerplatform" {
|
||||
$AddPowerAppsParams = @{
|
||||
'ErrorAction' = 'Stop';
|
||||
}
|
||||
if ($ServicePrincipalParams.CertThumbprintParams) {
|
||||
$AddPowerAppsParams += @{
|
||||
CertificateThumbprint = $ServicePrincipalParams.CertThumbprintParams.CertificateThumbprint;
|
||||
ApplicationId = $ServicePrincipalParams.CertThumbprintParams.AppID;
|
||||
TenantID = $ServicePrincipalParams.CertThumbprintParams.Organization; # Organization also works here
|
||||
}
|
||||
}
|
||||
switch ($M365Environment) {
|
||||
"commercial" {
|
||||
$AddPowerAppsParams += @{'Endpoint'='prod';}
|
||||
}
|
||||
"gcc" {
|
||||
$AddPowerAppsParams += @{'Endpoint'='usgov';}
|
||||
}
|
||||
"gcchigh" {
|
||||
$AddPowerAppsParams += @{'Endpoint'='usgovhigh';}
|
||||
}
|
||||
"dod" {
|
||||
$AddPowerAppsParams += @{'Endpoint'='dod';}
|
||||
}
|
||||
}
|
||||
Add-PowerAppsAccount @AddPowerAppsParams | Out-Null
|
||||
}
|
||||
{($_ -eq "onedrive") -or ($_ -eq "sharepoint")} {
|
||||
if ($AADAuthRequired) {
|
||||
$LimitedGraphParams = @{
|
||||
'ErrorAction' = 'Stop';
|
||||
}
|
||||
if ($ServicePrincipalParams.CertThumbprintParams) {
|
||||
$LimitedGraphParams += @{
|
||||
CertificateThumbprint = $ServicePrincipalParams.CertThumbprintParams.CertificateThumbprint;
|
||||
ClientID = $ServicePrincipalParams.CertThumbprintParams.AppID;
|
||||
TenantId = $ServicePrincipalParams.CertThumbprintParams.Organization; # Organization also works here
|
||||
}
|
||||
}
|
||||
switch ($M365Environment) {
|
||||
"gcchigh" {
|
||||
$LimitedGraphParams += @{'Environment' = "USGov";}
|
||||
}
|
||||
"dod" {
|
||||
$LimitedGraphParams += @{'Environment' = "USGovDoD";}
|
||||
}
|
||||
}
|
||||
Connect-MgGraph @LimitedGraphParams | Out-Null
|
||||
$GraphProfile = (Get-MgProfile -ErrorAction "Stop").Name
|
||||
if ($GraphProfile.ToLower() -ne "beta") {
|
||||
Select-MgProfile -Name "Beta" -ErrorAction "Stop" | Out-Null
|
||||
}
|
||||
$AADAuthRequired = $false
|
||||
}
|
||||
if ($SPOAuthRequired) {
|
||||
$InitialDomain = (Get-MgOrganization).VerifiedDomains | Where-Object {$_.isInitial}
|
||||
$InitialDomainPrefix = $InitialDomain.Name.split(".")[0]
|
||||
$SPOParams = @{
|
||||
'ErrorAction' = 'Stop';
|
||||
}
|
||||
$PnPParams = @{
|
||||
'ErrorAction' = 'Stop';
|
||||
}
|
||||
switch ($M365Environment) {
|
||||
{($_ -eq "commercial") -or ($_ -eq "gcc")} {
|
||||
$SPOParams += @{
|
||||
'Url'= "https://$($InitialDomainPrefix)-admin.sharepoint.com";
|
||||
}
|
||||
$PnPParams += @{
|
||||
'Url'= "$($InitialDomainPrefix)-admin.sharepoint.com";
|
||||
}
|
||||
}
|
||||
"gcchigh" {
|
||||
$SPOParams += @{
|
||||
'Url'= "https://$($InitialDomainPrefix)-admin.sharepoint.us";
|
||||
'Region' = "ITAR";
|
||||
}
|
||||
$PnPParams += @{
|
||||
'Url'= "$($InitialDomainPrefix)-admin.sharepoint.us";
|
||||
'AzureEnvironment' = 'USGovernmentHigh'
|
||||
}
|
||||
}
|
||||
"dod" {
|
||||
$SPOParams += @{
|
||||
'Url'= "https://$($InitialDomainPrefix)-admin.sharepoint-mil.us";
|
||||
'Region' = "ITAR";
|
||||
}
|
||||
$PnPParams += @{
|
||||
'Url'= "$($InitialDomainPrefix)-admin.sharepoint-mil.us";
|
||||
'AzureEnvironment' = 'USGovernmentDoD'
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($ServicePrincipalParams.CertThumbprintParams) {
|
||||
$PnPParams += @{
|
||||
Thumbprint = $ServicePrincipalParams.CertThumbprintParams.CertificateThumbprint;
|
||||
ClientId = $ServicePrincipalParams.CertThumbprintParams.AppID;
|
||||
Tenant = $ServicePrincipalParams.CertThumbprintParams.Organization; # Organization Domain is actually required here.
|
||||
}
|
||||
Connect-PnPOnline @PnPParams | Out-Null
|
||||
}
|
||||
else {
|
||||
Connect-SPOService @SPOParams | Out-Null
|
||||
}
|
||||
$SPOAuthRequired = $false
|
||||
}
|
||||
}
|
||||
"teams" {
|
||||
$TeamsParams = @{'ErrorAction'= 'Stop'}
|
||||
if ($ServicePrincipalParams.CertThumbprintParams) {
|
||||
$TeamsConnectToTenant = @{
|
||||
CertificateThumbprint = $ServicePrincipalParams.CertThumbprintParams.CertificateThumbprint;
|
||||
ApplicationId = $ServicePrincipalParams.CertThumbprintParams.AppID;
|
||||
TenantId = $ServicePrincipalParams.CertThumbprintParams.Organization; # Organization Domain is actually required here.
|
||||
}
|
||||
$TeamsParams += $TeamsConnectToTenant
|
||||
}
|
||||
switch ($M365Environment) {
|
||||
"gcchigh" {
|
||||
$TeamsParams += @{'TeamsEnvironmentName'= 'TeamsGCCH';}
|
||||
}
|
||||
"dod" {
|
||||
$TeamsParams += @{'TeamsEnvironmentName'= 'TeamsDOD';}
|
||||
}
|
||||
}
|
||||
Connect-MicrosoftTeams @TeamsParams | Out-Null
|
||||
}
|
||||
default {
|
||||
throw "Invalid ProductName argument"
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error "Error establishing a connection with $($Product). $($_)"
|
||||
$ProdAuthFailed += $Product
|
||||
Write-Warning "$($Product) will be omitted from the output because of failed authentication"
|
||||
}
|
||||
}
|
||||
Write-Progress -Activity "Authenticating to each service" -Status "Ready" -Completed
|
||||
$ProdAuthFailed
|
||||
}
|
||||
|
||||
function Disconnect-SCuBATenant {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Disconnect all active M365 connection sessions made by ScubaGear
|
||||
.DESCRIPTION
|
||||
Forces disconnect of all outstanding open sessions associated with
|
||||
M365 product APIs within the current PowerShell session.
|
||||
Best used after an ScubaGear run to ensure a new tenant connection is
|
||||
used for future ScubaGear runs.
|
||||
.Parameter ProductNames
|
||||
A list of one or more M365 shortened product names this function will disconnect from. By default this function will disconnect from all possible products ScubaGear can run against.
|
||||
.EXAMPLE
|
||||
Disconnect-SCuBATenant
|
||||
.EXAMPLE
|
||||
Disconnect-SCuBATenant -ProductNames teams
|
||||
.EXAMPLE
|
||||
Disconnect-SCuBATenant -ProductNames aad, exo
|
||||
.Functionality
|
||||
Public
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet("aad", "defender", "exo", "onedrive","powerplatform", "sharepoint", "teams", IgnoreCase = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string[]]
|
||||
$ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams")
|
||||
)
|
||||
$ErrorActionPreference = "SilentlyContinue"
|
||||
|
||||
try {
|
||||
$N = 0
|
||||
$Len = $ProductNames.Length
|
||||
|
||||
foreach ($Product in $ProductNames) {
|
||||
$N += 1
|
||||
$Percent = $N*100/$Len
|
||||
Write-Progress -Activity "Disconnecting from each service" -Status "Disconnecting from $($Product); $($n) of $($Len) disconnected." -PercentComplete $Percent
|
||||
Write-Verbose "Disconnecting from $Product."
|
||||
if (($Product -eq "aad") -or ($Product -eq "onedrive") -or ($Product -eq "sharepoint")) {
|
||||
Disconnect-MgGraph -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
if($Product -eq "sharepoint") {
|
||||
Disconnect-SPOService -ErrorAction SilentlyContinue
|
||||
Disconnect-PnPOnline -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
elseif ($Product -eq "teams") {
|
||||
Disconnect-MicrosoftTeams -Confirm:$false -ErrorAction SilentlyContinue
|
||||
}
|
||||
elseif ($Product -eq "powerplatform") {
|
||||
Remove-PowerAppsAccount -ErrorAction SilentlyContinue -WarningAction SilentlyContinue
|
||||
}
|
||||
elseif (($Product -eq "exo") -or ($Product -eq "defender")) {
|
||||
Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue -InformationAction SilentlyContinue | Out-Null
|
||||
}
|
||||
else {
|
||||
Write-Warning "Product $Product not recognized, skipping..."
|
||||
}
|
||||
}
|
||||
Write-Progress -Activity "Disconnecting from each service" -Status "Done" -Completed
|
||||
|
||||
} catch [System.InvalidOperationException] {
|
||||
# Suppress error due to disconnect from service with no active connection
|
||||
continue
|
||||
} catch {
|
||||
Write-Error "ERRROR: Could not disconnect from $Product`n$($Error[0]): "
|
||||
} finally {
|
||||
$ErrorActionPreference = "Continue"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
'Connect-Tenant',
|
||||
'Disconnect-SCuBATenant'
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"Teams": [
|
||||
{"Number": "Teams 2.1", "Title": "External Participants SHOULD NOT Be Enabled to Request Control of Shared Desktops or Windows in Meetings"},
|
||||
{"Number": "Teams 2.2", "Title": "Anonymous Users SHALL NOT Be Enabled to Start Meetings"},
|
||||
{"Number": "Teams 2.3", "Title": "Automatic Admittance to Meetings SHOULD Be Restricted"},
|
||||
{"Number": "Teams 2.4", "Title": "External User Access SHALL Be Restricted"},
|
||||
{"Number": "Teams 2.5", "Title": "Unmanaged User Access SHALL Be Restricted"},
|
||||
{"Number": "Teams 2.6", "Title": "Contact with Skype Users SHALL Be Blocked"},
|
||||
{"Number": "Teams 2.7", "Title": "Teams Email Integration SHALL Be Disabled"},
|
||||
{"Number": "Teams 2.8", "Title": "Only Approved Apps SHOULD Be Installed"},
|
||||
{"Number": "Teams 2.9", "Title": "Cloud Recording of Teams Meetings SHOULD Be Disabled for Unapproved Users"},
|
||||
{"Number": "Teams 2.10", "Title": "Only the Meeting Organizer SHOULD Be Able to Record Live Events"},
|
||||
{"Number": "Teams 2.11", "Title": "Data Loss Prevention Solutions SHALL Be Enabled"},
|
||||
{"Number": "Teams 2.12", "Title": "Attachments SHOULD Be Scanned for Malware"},
|
||||
{"Number": "Teams 2.13", "Title": "Link Protection SHOULD Be Enabled"}],
|
||||
"EXO": [
|
||||
{"Number": "EXO 2.1", "Title": "Automatic Forwarding to External Domains SHALL Be Disabled"},
|
||||
{"Number": "EXO 2.2", "Title": "Sender Policy Framework SHALL Be Enabled"},
|
||||
{"Number": "EXO 2.3", "Title": "DomainKeys Identified Mail SHOULD Be Enabled"},
|
||||
{"Number": "EXO 2.4", "Title": "Domain-based Message Authentication, Reporting, and Conformance SHALL Be Enabled"},
|
||||
{"Number": "EXO 2.5", "Title": "Simple Mail Transfer Protocol Authentication SHALL Be Disabled"},
|
||||
{"Number": "EXO 2.6", "Title": "Calendar and Contact Sharing SHALL Be Restricted"},
|
||||
{"Number": "EXO 2.7", "Title": "External Sender Warnings SHALL Be Implemented"},
|
||||
{"Number": "EXO 2.8", "Title": "Data Loss Prevention Solutions SHALL Be Enabled"},
|
||||
{"Number": "EXO 2.9", "Title": "Emails SHALL Be Filtered by Attachment File Type"},
|
||||
{"Number": "EXO 2.10", "Title": "Emails SHALL Be Scanned for Malware"},
|
||||
{"Number": "EXO 2.11", "Title":"Phishing Protections SHOULD Be Enabled"},
|
||||
{"Number": "EXO 2.12", "Title": "IP Allow Lists SHOULD NOT be Implemented"},
|
||||
{"Number": "EXO 2.13", "Title": "Mailbox Auditing SHALL Be Enabled"},
|
||||
{"Number": "EXO 2.14", "Title": "Inbound Anti-Spam Protections SHALL Be Enabled"},
|
||||
{"Number": "EXO 2.15", "Title": "Link Protection SHOULD Be Enabled"},
|
||||
{"Number": "EXO 2.16", "Title": "Alerts SHALL Be Enabled"},
|
||||
{"Number": "EXO 2.17", "Title": "Audit Logging SHALL Be Enabled"}],
|
||||
"Defender": [
|
||||
{"Number" : "Defender 2.1", "Title" : "Preset Security Profiles SHOULD NOT Be Used"},
|
||||
{"Number" : "Defender 2.2", "Title" : "Data Loss Prevention SHALL Be Enabled"},
|
||||
{"Number" : "Defender 2.3", "Title" : "Common Attachments Filter SHALL Be Enabled"},
|
||||
{"Number" : "Defender 2.4", "Title" : "Zero-hour Auto Purge for Malware SHOULD Be Enabled"},
|
||||
{"Number" : "Defender 2.5", "Title" : "Phishing Protections SHOULD Be Enabled"},
|
||||
{"Number" : "Defender 2.6", "Title" : "Inbound Anti-Spam Protections SHALL Be Enabled"},
|
||||
{"Number" : "Defender 2.7", "Title" : "Safe Link Policies SHOULD Be Enabled"},
|
||||
{"Number" : "Defender 2.8", "Title" : "Safe-Attachments SHALL Be Enabled"},
|
||||
{"Number" : "Defender 2.9", "Title" : "Alerts SHALL Be Enabled"},
|
||||
{"Number" : "Defender 2.10", "Title" : "Audit Logging SHALL Be Enabled"}],
|
||||
"AAD": [
|
||||
{"Number" : "AAD 2.1", "Title" : "Legacy Authentication SHALL be Blocked" },
|
||||
{"Number" : "AAD 2.2", "Title" : "High Risk Users SHALL be Blocked"},
|
||||
{"Number" : "AAD 2.3", "Title" : "High Risk Sign-ins SHALL be Blocked"},
|
||||
{"Number" : "AAD 2.4", "Title" : "Phishing-Resistant Multifactor Authentication SHALL be required for all users"},
|
||||
{"Number" : "AAD 2.5", "Title": "Azure AD logs SHALL be collected"},
|
||||
{"Number" : "AAD 2.6", "Title": "Only administrators SHALL be allowed to register third-party applications"},
|
||||
{"Number" : "AAD 2.7", "Title": "Non-admin users SHALL be prevented from providing consent to third-party applications"},
|
||||
{"Number" : "AAD 2.8", "Title": "Passwords SHALL NOT expire"},
|
||||
{"Number" : "AAD 2.9", "Title": "Session Length SHALL be Limited"},
|
||||
{"Number" : "AAD 2.10", "Title": "Browser Sessions SHALL NOT be Persistent"},
|
||||
{"Number" : "AAD 2.11", "Title": "The Number of Users with the Highest Privilege Roles SHALL be limited"},
|
||||
{"Number" : "AAD 2.12", "Title": "Highly Privileged User Accounts SHALL be Cloud-Only"},
|
||||
{"Number" : "AAD 2.13", "Title": "Multifactor Authentication SHALL be required for Highly Privileged Roles"},
|
||||
{"Number" : "AAD 2.14", "Title": "Users Assigned to Highly Privileged Roles SHALL NOT Have Permanent Permissions"},
|
||||
{"Number" : "AAD 2.15", "Title": "Activation of Highly Privileged Roles SHOULD Require Approval"},
|
||||
{"Number" : "AAD 2.16", "Title": "Highly Privileged Role Assignment and Activation SHALL be Monitored"},
|
||||
{"Number" : "AAD 2.17", "Title": "Managed Devices SHOULD be Required for Authentication"},
|
||||
{"Number" : "AAD 2.18", "Title": "Guest User Access SHOULD be Restricted"}
|
||||
],
|
||||
"PowerPlatform": [
|
||||
{"Number" : "Power Platform 2.1", "Title" : "Creation of Power Platform Environments SHALL be restricted"},
|
||||
{"Number" : "Power Platform 2.2", "Title" : "Data Loss Prevention Policy for Power Platform environments SHALL be created"},
|
||||
{"Number" : "Power Platform 2.3", "Title" : "Tenant isolation SHALL be enabled to prevent cross tenant access of Power Platform environments"},
|
||||
{"Number" : "Power Platform 2.4", "Title" : "Content Security Policy SHALL be Enabled"}],
|
||||
"OneDrive": [
|
||||
{"Number" : "OneDrive 2.1", "Title" : "Anyone Links SHOULD Be Turned Off"},
|
||||
{"Number" : "OneDrive 2.2", "Title" : "Expiration Date SHOULD Be Set for Anyone Links"},
|
||||
{"Number" : "OneDrive 2.3", "Title" : "Link Permissions SHOULD Be Set to Enabled Anyone Links to View"},
|
||||
{"Number" : "OneDrive 2.4", "Title" : "OneDrive Client SHALL be restricted to Windows for Agency-Defined Domain(s)"},
|
||||
{"Number" : "OneDrive 2.5", "Title" : "OneDrive Client SHALL be restricted to Sync with Mac for Agency-Defined Devices"},
|
||||
{"Number" : "OneDrive 2.6", "Title" : "OneDrive Client Sync SHALL Only Be Allowed Within the Local Domain"},
|
||||
{"Number" : "OneDrive 2.7", "Title" : "Legacy Authentication SHALL Be Blocked"}
|
||||
],
|
||||
"SharePoint": [
|
||||
{"Number" : "SharePoint 2.1", "Title" : "File and folder links default sharing settings SHALL be set to \"Specific People (only the people the user specifies)\""},
|
||||
{"Number" : "SharePoint 2.2", "Title" : "External sharing SHOULD be set to \"New and Existing Guests\" and managed through approved domains and/or security groups per interagency collaboration needs"},
|
||||
{"Number" : "SharePoint 2.3", "Title" : "Sensitive SharePoint sites SHOULD adjust their default sharing settings to those best aligning to their sensitivity level"},
|
||||
{"Number" : "SharePoint 2.4", "Title" : "Expiration times for guest access to a site or OneDrive, and reauthentication expiration times for people who use a verification code, SHOULD be determined by mission needs / Agency policy or else defaulted to 30 days"},
|
||||
{"Number" : "SharePoint 2.5", "Title" : "Users SHALL be prevented from running custom scripts"}
|
||||
]
|
||||
}
|
||||
182
PowerShell/ScubaGear/Modules/CreateReport/CreateReport.psm1
Normal file
182
PowerShell/ScubaGear/Modules/CreateReport/CreateReport.psm1
Normal file
@@ -0,0 +1,182 @@
|
||||
function New-Report {
|
||||
<#
|
||||
.Description
|
||||
This function creates the individual HTML report using the TestResults.json.
|
||||
Output will be stored as an HTML file in the InvidualReports folder in the OutPath Folder.
|
||||
The report Home page and link tree will be named BaselineReports.html
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet("Teams", "EXO", "Defender", "AAD", "PowerPlatform", "SharePoint", "OneDrive", IgnoreCase = $false)]
|
||||
[string]
|
||||
$BaselineName,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet("Microsoft Teams", "Exchange Online", "Microsoft 365 Defender", "Azure Active Directory", "Microsoft Power Platform", "SharePoint Online", "OneDrive for Business", IgnoreCase = $false)]
|
||||
[string]
|
||||
$FullName,
|
||||
|
||||
# The location to save the html report in.
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateScript({Test-Path -PathType Container $_})]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$IndividualReportPath,
|
||||
|
||||
# The location to save the html report in.
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateScript({Test-Path -PathType Container $_})]
|
||||
[ValidateScript({Test-Path -IsValid $_})]
|
||||
[string]
|
||||
$OutPath,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$OutProviderFileName,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateScript({Test-Path -IsValid $_})]
|
||||
[string]
|
||||
$OutRegoFileName,
|
||||
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[switch]
|
||||
$DarkMode
|
||||
)
|
||||
|
||||
$FileName = Join-Path -Path $PSScriptRoot -ChildPath "BaselineTitles.json"
|
||||
$AllTitles = Get-Content $FileName | ConvertFrom-Json
|
||||
$Titles = $AllTitles.$BaselineName
|
||||
|
||||
$FileName = Join-Path -Path $OutPath -ChildPath "$($OutProviderFileName).json"
|
||||
$SettingsExport = Get-Content $FileName | ConvertFrom-Json
|
||||
|
||||
$FileName = Join-Path -Path $OutPath -ChildPath "$($OutRegoFileName).json"
|
||||
$TestResults = Get-Content $FileName | ConvertFrom-Json
|
||||
|
||||
$Fragments = @()
|
||||
|
||||
$MetaData += [pscustomobject]@{
|
||||
"Tenant Display Name" = $SettingsExport.tenant_details.DisplayName;
|
||||
"Report Date" = $SettingsExport.date;
|
||||
"Baseline Version" = $SettingsExport.baseline_version;
|
||||
"Module Version" = $SettingsExport.module_version
|
||||
}
|
||||
|
||||
$MetaDataTable = $MetaData | ConvertTo-HTML -Fragment
|
||||
$MetaDataTable = $MetaDataTable -replace '^(.*?)<table>','<table style = "text-align:center;">'
|
||||
$Fragments += $MetaDataTable
|
||||
$ReportSummary = @{
|
||||
"Warnings" = 0;
|
||||
"Failures" = 0;
|
||||
"Passes" = 0;
|
||||
"Manual" = 0;
|
||||
"Errors" = 0;
|
||||
"Date" = $SettingsExport.date;
|
||||
}
|
||||
|
||||
foreach ($Title in $Titles) {
|
||||
$Fragment = @()
|
||||
foreach ($test in $TestResults | Where-Object -Property Control -eq $Title.Number) {
|
||||
$MissingCommands = @()
|
||||
|
||||
if ($SettingsExport."$($BaselineName)_successful_commands" -or $SettingsExport."$($BaselineName)_unsuccessful_commands") {
|
||||
# If neither of these keys are present, it means the provider for that baseline
|
||||
# hasn't been updated to the updated error handling method. This check
|
||||
# here ensures backwards compatibility until all providers are udpated.
|
||||
$MissingCommands = $test.Commandlet | Where-Object {$SettingsExport."$($BaselineName)_successful_commands" -notcontains $_}
|
||||
}
|
||||
|
||||
if ($MissingCommands.Count -gt 0) {
|
||||
$Result = "Error"
|
||||
$ReportSummary.Errors += 1
|
||||
$MissingString = $MissingCommands -Join ", "
|
||||
$test.ReportDetails = "This test depends on the following command(s) which did not execute successfully: $($MissingString). See terminal output for more details."
|
||||
}
|
||||
elseif ($test.RequirementMet) {
|
||||
$Result = "Pass"
|
||||
$ReportSummary.Passes += 1
|
||||
}
|
||||
elseif ($test.Criticality -eq "Should") {
|
||||
$Result = "Warning"
|
||||
$ReportSummary.Warnings += 1
|
||||
}
|
||||
elseif ($test.Criticality.EndsWith('3rd Party') -or $test.Criticality.EndsWith('Not-Implemented')) {
|
||||
$Result = "N/A"
|
||||
$ReportSummary.Manual += 1
|
||||
}
|
||||
else {
|
||||
$Result = "Fail"
|
||||
$ReportSummary.Failures += 1
|
||||
}
|
||||
|
||||
$Fragment += [pscustomobject]@{
|
||||
"Requirement"=$test.Requirement;
|
||||
"Result"=$Result;
|
||||
"Criticality"=$test.Criticality;
|
||||
"Details"=$test.ReportDetails}
|
||||
}
|
||||
|
||||
$Number = $Title.Number
|
||||
$Name = $Title.Title
|
||||
$Fragments += $Fragment | ConvertTo-Html -PreContent "<h2>$Number $Name</h2>" -Fragment
|
||||
|
||||
}
|
||||
|
||||
$Title = "$($FullName) Baseline Report"
|
||||
$AADWarning = "<p> Note: Conditional Access (CA) Policy exclusions and additional policy conditions
|
||||
may limit a policy's scope more narrowly than desired. Recommend reviewing matching policies
|
||||
against the baseline statement to ensure a match between intent and implementation. </p>"
|
||||
$NoWarning = "<p><br/></p>"
|
||||
Add-Type -AssemblyName System.Web
|
||||
|
||||
$ReporterPath = $PSScriptRoot
|
||||
$ReportHTMLPath = Join-Path -Path $ReporterPath -ChildPath "IndividualReport"
|
||||
$ReportHTML = (Get-Content $(Join-Path -Path $ReportHTMLPath -ChildPath "IndividualReport.html")) -Join "`n"
|
||||
$ReportHTML = $ReportHTML.Replace("{TITLE}", $Title)
|
||||
|
||||
# Handle AAD-specific reporting
|
||||
if ($BaselineName -eq "aad") {
|
||||
$ReportHTML = $ReportHTML.Replace("{AADWARNING}", $AADWarning)
|
||||
$ReportHTML = $ReportHTML.Replace("{CAPTABLES}", "")
|
||||
$CapJson = ConvertTo-Json $SettingsExport.cap_table_data
|
||||
}
|
||||
else {
|
||||
$ReportHTML = $ReportHTML.Replace("{AADWARNING}", $NoWarning)
|
||||
$ReportHTML = $ReportHTML.Replace("{CAPTABLES}", "")
|
||||
$CapJson = "null"
|
||||
}
|
||||
|
||||
$CssPath = Join-Path -Path $ReporterPath -ChildPath "styles"
|
||||
$MainCSS = (Get-Content $(Join-Path -Path $CssPath -ChildPath "main.css")) -Join "`n"
|
||||
$ReportHTML = $ReportHTML.Replace("{MAIN_CSS}", "<style>
|
||||
$($MainCSS)
|
||||
</style>")
|
||||
|
||||
$ScriptsPath = Join-Path -Path $ReporterPath -ChildPath "scripts"
|
||||
$MainJS = (Get-Content $(Join-Path -Path $ScriptsPath -ChildPath "main.js")) -Join "`n"
|
||||
$MainJS = "const caps = $($CapJson);`n$($MainJS)"
|
||||
$UtilsJS = (Get-Content $(Join-Path -Path $ScriptsPath -ChildPath "utils.js")) -Join "`n"
|
||||
$MainJS = "$($MainJS)`n$($UtilsJS)"
|
||||
$ReportHTML = $ReportHTML.Replace("{MAIN_JS}", "<script>
|
||||
let darkMode = $($DarkMode.ToString().ToLower());
|
||||
$($MainJS)
|
||||
</script>")
|
||||
|
||||
$ReportHTML = $ReportHTML.Replace("{TABLES}", $Fragments)
|
||||
$FileName = Join-Path -Path $IndividualReportPath -ChildPath "$($BaselineName)Report.html"
|
||||
[System.Web.HttpUtility]::HtmlDecode($ReportHTML) | Out-File $FileName
|
||||
|
||||
$ReportSummary
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
'New-Report'
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>{TITLE}</title>
|
||||
{MAIN_CSS}
|
||||
{MAIN_JS}
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<a href="../BaselineReports.html" title="Return to the report summary"><img src="images/cisa_logo.png" alt="Return to the report summary"></a>
|
||||
<div class="links">
|
||||
<a href="https://www.cisa.gov/scuba" target="_blank"><h3 style="width: 210px;">Secure Cloud Business Applications (SCuBA)</h3></a>
|
||||
<div style="width:10px;"></div>
|
||||
<a href="https://github.com/cisagov/ScubaGear/tree/main/baselines" target="_blank"><h3 style="width: 100px;">Baseline Documents</h3></a>
|
||||
</div>
|
||||
</header>
|
||||
<p id="toggle-text">Light mode</p>
|
||||
<label class="switch">
|
||||
<input id="toggle" type="checkbox" onclick="toggleDarkMode()">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<h1>{TITLE}</h1>
|
||||
<h4>{AADWARNING}</h4>
|
||||
{TABLES}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>SCuBA M365 Security Baseline Conformance Reports</title>
|
||||
{MAIN_CSS}
|
||||
{PARENT_CSS}
|
||||
{MAIN_JS}
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<a href=""><img src="./IndividualReports/images/cisa_logo.png" alt="Return to the report summary"></a>
|
||||
<div class="links">
|
||||
<a href="https://www.cisa.gov/scuba" target="_blank"><h3 style="width: 210px;">Secure Cloud Business Applications (SCuBA)</h3></a>
|
||||
<div style="width:10px;"></div>
|
||||
<a href="https://github.com/cisagov/ScubaGear/tree/main/baselines" target="_blank"><h3 style="width: 100px;">Baseline Documents</h3></a>
|
||||
</div>
|
||||
</header>
|
||||
<p id="toggle-text">Light mode</p>
|
||||
<label class="switch">
|
||||
<input id="toggle" type="checkbox" onclick="toggleDarkMode()">
|
||||
<span class="slider round"></span>
|
||||
</label>
|
||||
<h1>SCuBA M365 Security Baseline Conformance Reports</h1>
|
||||
{TENANT_DETAILS}
|
||||
<br> <br/>
|
||||
{TABLES}
|
||||
<footer>
|
||||
Report generated with <a class="individual_reports" href="https://github.com/cisagov/ScubaGear">CISA's ScubaGear</a> tool {MODULE_VERSION}
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="#3F6078" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M201.4 374.6c12.5 12.5 32.8 12.5 45.3 0l160-160c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L224 306.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160z"/></svg>
|
||||
|
After Width: | Height: | Size: 431 B |
@@ -0,0 +1 @@
|
||||
<svg fill="#3F6078" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path d="M278.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-160 160c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L210.7 256 73.4 118.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l160 160z"/></svg>
|
||||
|
After Width: | Height: | Size: 432 B |
BIN
PowerShell/ScubaGear/Modules/CreateReport/images/cisa_logo.png
Normal file
BIN
PowerShell/ScubaGear/Modules/CreateReport/images/cisa_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 322 KiB |
@@ -0,0 +1,3 @@
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
mountDarkMode("Parent Report");
|
||||
});
|
||||
295
PowerShell/ScubaGear/Modules/CreateReport/scripts/main.js
Normal file
295
PowerShell/ScubaGear/Modules/CreateReport/scripts/main.js
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* Adds the red, green, yellow, and gray coloring to the individual report pages.
|
||||
*/
|
||||
const colorRows = () => {
|
||||
let rows = document.querySelectorAll('tr');
|
||||
const statusCol = 1;
|
||||
const criticalityCol = 2;
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
try {
|
||||
if (rows[i].children[statusCol].innerHTML === "Fail") {
|
||||
rows[i].style.background = "var(--test-fail)";
|
||||
}
|
||||
else if (rows[i].children[statusCol].innerHTML === "Warning") {
|
||||
rows[i].style.background = "var(--test-warning)";
|
||||
}
|
||||
else if (rows[i].children[statusCol].innerHTML === "Pass") {
|
||||
rows[i].style.background = "var(--test-pass)";
|
||||
}
|
||||
else if (rows[i].children[criticalityCol].innerHTML.includes("Not-Implemented")) {
|
||||
rows[i].style.background = "var(--test-other)";
|
||||
}
|
||||
else if (rows[i].children[criticalityCol].innerHTML.includes("3rd Party")) {
|
||||
rows[i].style.background = "var(--test-other)";
|
||||
}
|
||||
else if (rows[i].children[statusCol].innerHTML.includes("Error")) {
|
||||
rows[i].style.background = "var(--test-fail)";
|
||||
rows[i].querySelectorAll('td')[1].style.borderColor = "var(--border-color)";
|
||||
rows[i].querySelectorAll('td')[1].style.color = "#d10000";
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Error in colorRows, i = ${i}`);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For the conditional access policy table. For AAD only.
|
||||
* The "" column is used for the nameless column that holds the
|
||||
* "Show more" / "Show less" buttons.
|
||||
*/
|
||||
const capColNames = ["", "Name", "State", "Users", "Apps/Actions", "Conditions", "Block/Grant Access", "Session Controls"];
|
||||
|
||||
/**
|
||||
* Creates the conditional access policy table at the end of the AAD report.
|
||||
* For all other reports (e.g., teams), this function does nothing.
|
||||
*/
|
||||
const fillCAPTable = () => {
|
||||
if (caps === undefined || caps === null) {
|
||||
/* The CAP table is only displayed for the AAD baseline, but
|
||||
this js file applies to all baselines. If caps is null,
|
||||
then the current baseline is not AAD and we don't need to
|
||||
do anything.
|
||||
|
||||
Also, note that caps isn't declared in the static version of
|
||||
this file. It is prepended to the version rendered in the html
|
||||
by CreateReport.ps1.
|
||||
*/
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let capDiv = document.createElement("div");
|
||||
capDiv.setAttribute("id", "caps");
|
||||
document.querySelector("main").appendChild(capDiv);
|
||||
|
||||
capDiv.appendChild(document.createElement("hr"));
|
||||
h2 = document.createElement("h2");
|
||||
h2.innerHTML = "Conditional Access Policies";
|
||||
capDiv.appendChild(h2);
|
||||
|
||||
let buttons = document.createElement("div");
|
||||
buttons.classList.add("buttons");
|
||||
capDiv.appendChild(buttons);
|
||||
|
||||
let expandAll = document.createElement("button");
|
||||
expandAll.appendChild(document.createTextNode("+ Expand all"));
|
||||
expandAll.title = "Expand all";
|
||||
expandAll.addEventListener("click", expandAllCAPs);
|
||||
buttons.appendChild(expandAll);
|
||||
|
||||
let collapseAll = document.createElement("button");
|
||||
collapseAll.appendChild(document.createTextNode("− Collapse all"));
|
||||
collapseAll.title = "Collapse all";
|
||||
collapseAll.addEventListener("click", collapseAllCAPs);
|
||||
buttons.appendChild(collapseAll);
|
||||
|
||||
let table = document.createElement("table");
|
||||
table.setAttribute("class", "caps_table");
|
||||
capDiv.appendChild(table);
|
||||
|
||||
let header = document.createElement("tr");
|
||||
for (let i = 0; i < capColNames.length; i++) {
|
||||
let th = document.createElement("th");
|
||||
if (capColNames[i] === "Apps/Actions") {
|
||||
th.setAttribute("class", "apps_actions");
|
||||
}
|
||||
else if (capColNames[i] === "State") {
|
||||
th.setAttribute("class", "state");
|
||||
}
|
||||
else if (capColNames[i] === "Users") {
|
||||
th.setAttribute("class", "users");
|
||||
}
|
||||
else if (capColNames[i] === "Conditions") {
|
||||
th.setAttribute("class", "conditions");
|
||||
}
|
||||
th.innerHTML = capColNames[i];
|
||||
header.appendChild(th);
|
||||
}
|
||||
table.appendChild(header);
|
||||
|
||||
for (let i = 0; i < caps.length; i++) {
|
||||
let tr = document.createElement("tr");
|
||||
for (let j = 0; j < capColNames.length; j++) {
|
||||
let td = document.createElement("td");
|
||||
fillTruncatedCell(td, i,j);
|
||||
tr.appendChild(td);
|
||||
}
|
||||
|
||||
let img = document.createElement("img");
|
||||
img.setAttribute('src', 'images/angle-right-solid.svg');
|
||||
img.setAttribute('alt', 'Show more');
|
||||
img.setAttribute('title', 'Show more');
|
||||
img.style.width = '10px';
|
||||
img.rowNumber = i;
|
||||
img.addEventListener("click", expandCAPRow);
|
||||
tr.querySelectorAll('td')[0].appendChild(img);
|
||||
table.appendChild(tr);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error in fillCAPTable");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills in the truncated version of the given cell of the AAD conditional
|
||||
* access policy table. For AAD only.
|
||||
* @param {HTMLElement} td The specific td that will be populated.
|
||||
* @param {number} i The row number (0-indexed, not counting the header row).
|
||||
* @param {number} j The the column number (0-indexed).
|
||||
*/
|
||||
const fillTruncatedCell = (td, i, j) => {
|
||||
try {
|
||||
const charLimit = 50;
|
||||
let content = "";
|
||||
let truncated = false;
|
||||
if (capColNames[j] === "") {
|
||||
content = ""
|
||||
}
|
||||
else if (caps[i][capColNames[j]].constructor === Array && caps[i][capColNames[j]].length > 1) {
|
||||
content = caps[i][capColNames[j]][0];
|
||||
truncated = true;
|
||||
}
|
||||
else {
|
||||
content = caps[i][capColNames[j]];
|
||||
}
|
||||
|
||||
if (content.length > charLimit) {
|
||||
td.innerHTML = content.substring(0, charLimit);
|
||||
truncated = true;
|
||||
}
|
||||
else {
|
||||
td.innerHTML = content;
|
||||
}
|
||||
|
||||
if (truncated) {
|
||||
let span = document.createElement("span");
|
||||
span.appendChild(document.createTextNode("..."));
|
||||
span.title = "Show more";
|
||||
span.rowNumber = i;
|
||||
span.addEventListener("click", expandCAPRow);
|
||||
td.appendChild(span);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`Error in fillTruncatedCell, i = ${i}, j = ${j}`);
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills in the row of the conditional access policy table indicated by the
|
||||
* event with the truncated version of the row. For AAD only.
|
||||
* @param {HTMLElement} event The target of the event.
|
||||
*/
|
||||
const hideCAPRow = (event) => {
|
||||
try {
|
||||
let i = event.currentTarget.rowNumber;
|
||||
let tr = document.querySelector("#caps tr:nth-of-type(" + (i+2).toString() + ")"); /*i+2
|
||||
because nth-of-type is indexed from 1 and to account for the header row */
|
||||
for (let j = 0; j < capColNames.length; j++) {
|
||||
let td = tr.querySelector("td:nth-of-type(" + (j+1).toString() + ")");
|
||||
fillTruncatedCell(td, i, j);
|
||||
}
|
||||
let img = document.createElement("img");
|
||||
img.setAttribute('src', 'images/angle-right-solid.svg');
|
||||
img.style.width = '10px';
|
||||
img.setAttribute('alt', 'Show more');
|
||||
img.setAttribute('title', 'Show more');
|
||||
img.rowNumber = i;
|
||||
img.addEventListener("click", expandCAPRow);
|
||||
tr.querySelectorAll('td')[0].appendChild(img);
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error in hideCAPRow");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands all rows of the conditional access policy table to the full version.
|
||||
* For AAD only.
|
||||
*/
|
||||
const expandAllCAPs = () => {
|
||||
try {
|
||||
let buttons = document.querySelectorAll("img[src*='angle-right-solid.svg']");
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
buttons[i].click();
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error in expandAllCAPs");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shrinks all rows of the conditional access policy table to the truncated
|
||||
* version. For AAD only.
|
||||
*/
|
||||
const collapseAllCAPs = () => {
|
||||
try {
|
||||
let buttons = document.querySelectorAll("img[src*='angle-down-solid.svg']");
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
buttons[i].click();
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error in collapseAllCAPs");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills in the row of the conditional access policy table indicated by the
|
||||
* event with the full version of the row. For AAD only.
|
||||
* @param {HTMLElement} event The target of the event.
|
||||
*/
|
||||
const expandCAPRow = (event) => {
|
||||
try {
|
||||
let i = event.currentTarget.rowNumber;
|
||||
let tr = document.querySelector("#caps tr:nth-of-type(" + (i+2).toString() + ")"); /*i+2
|
||||
because nth-of-type is indexed from 1 and to account for the header row */
|
||||
for (let j = 0; j < capColNames.length; j++) {
|
||||
let td = tr.querySelector("td:nth-of-type(" + (j+1).toString() + ")");
|
||||
fillTruncatedCell(td, i, j);
|
||||
td.innerHTML = "";
|
||||
if (capColNames[j] === "") {
|
||||
td.innerHTML = "";
|
||||
let img = document.createElement("img");
|
||||
img.setAttribute('src', 'images/angle-down-solid.svg');
|
||||
img.setAttribute('alt', 'Show less');
|
||||
img.setAttribute('title', 'Show less');
|
||||
img.style.width = '14px';
|
||||
img.rowNumber = i;
|
||||
img.addEventListener("click", hideCAPRow);
|
||||
tr.querySelectorAll('td')[0].appendChild(img);
|
||||
}
|
||||
else if (caps[i][capColNames[j]].constructor === Array && caps[i][capColNames[j]].length > 1) {
|
||||
let ul = document.createElement("ul");
|
||||
for (let k = 0; k < caps[i][capColNames[j]].length; k++) {
|
||||
let li = document.createElement("li");
|
||||
li.innerHTML = caps[i][capColNames[j]][k];
|
||||
ul.appendChild(li);
|
||||
}
|
||||
td.appendChild(ul);
|
||||
}
|
||||
else {
|
||||
td.innerHTML = caps[i][capColNames[j]];
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error in expandCAPRow");
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', (event) => {
|
||||
colorRows();
|
||||
fillCAPTable();
|
||||
mountDarkMode("Individual Report");
|
||||
});
|
||||
53
PowerShell/ScubaGear/Modules/CreateReport/scripts/utils.js
Normal file
53
PowerShell/ScubaGear/Modules/CreateReport/scripts/utils.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Checks if Dark Mode session storage variable exists. Creates one if it does not exist.
|
||||
* Sets the report's default Dark Mode state using the $DarkMode (JavaScript darkMode) PowerShell variable.
|
||||
* @param {string} pageLocation The page where this function is called.
|
||||
*/
|
||||
const mountDarkMode = (pageLocation) => {
|
||||
try {
|
||||
let darkModeCookie = sessionStorage.getItem("darkMode");
|
||||
if (darkModeCookie === undefined || darkModeCookie === null) {
|
||||
if (darkMode) {
|
||||
sessionStorage.setItem("darkMode", 'true');
|
||||
}
|
||||
else {
|
||||
sessionStorage.setItem("darkMode", 'false');
|
||||
}
|
||||
darkModeCookie = sessionStorage.getItem("darkMode");
|
||||
}
|
||||
setDarkMode(darkModeCookie);
|
||||
document.getElementById('toggle').checked = (darkModeCookie === 'true');
|
||||
}
|
||||
catch (error) {
|
||||
console.error("Error applying dark mode to the " + pageLocation + ": " + error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the report CSS to light mode or dark mode.
|
||||
* @param {string} state true for Dark Mode or false for Light Mode
|
||||
*/
|
||||
const setDarkMode = (state) => {
|
||||
if (state === 'true') {
|
||||
document.getElementsByTagName('html')[0].dataset.theme = "dark";
|
||||
document.querySelector("#toggle-text").innerHTML = "Dark Mode";
|
||||
sessionStorage.setItem("darkMode", 'true');
|
||||
}
|
||||
else {
|
||||
document.getElementsByTagName('html')[0].dataset.theme = "light";
|
||||
document.querySelector("#toggle-text").innerHTML = "Light Mode";
|
||||
sessionStorage.setItem("darkMode", 'false');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles light and dark mode
|
||||
*/
|
||||
const toggleDarkMode = () => {
|
||||
if (document.getElementById('toggle').checked) {
|
||||
setDarkMode('true');
|
||||
}
|
||||
else {
|
||||
setDarkMode('false');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
main {
|
||||
height: 100vh;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: inline-block;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
min-width: 140px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a.individual_reports:link {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: var(--header-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a.individual_reports:visited {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: var(--link-color);
|
||||
}
|
||||
|
||||
a.individual_reports:hover {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.individual_reports:active {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
table.tenantdata tr:first-child {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.failure { background-color: var(--test-fail); }
|
||||
.warning { background-color: var(--test-warning); }
|
||||
.pass { background-color: var(--test-pass); }
|
||||
.manual { background-color: var(--test-other); }
|
||||
|
||||
.error {
|
||||
background-color: var(--test-fail);
|
||||
color: #d10000;
|
||||
}
|
||||
289
PowerShell/ScubaGear/Modules/CreateReport/styles/main.css
Normal file
289
PowerShell/ScubaGear/Modules/CreateReport/styles/main.css
Normal file
@@ -0,0 +1,289 @@
|
||||
:root {
|
||||
--background-primary: white;
|
||||
--background-secondary: #b9bec2;
|
||||
--test-pass: #d5ebd5;
|
||||
--test-fail: #deb8b8;
|
||||
--test-warning: #fff7d6;
|
||||
--test-other: #ebebf2;
|
||||
--cap-even: #0052882d;
|
||||
--cap-hover: #00528850;
|
||||
--header-color: #005288;
|
||||
--note-color: #ee4e04;
|
||||
--header-bottom: black;
|
||||
--link-color: #85B065;
|
||||
--text-color: black;
|
||||
--border-color: black;
|
||||
--toggle-height: 25px;
|
||||
--toggle-width: 46px;
|
||||
--toggle-radius: 18px;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] {
|
||||
--background-primary: #1f1b24;
|
||||
--background-secondary: #121212;
|
||||
--test-pass: #1d3b1d;
|
||||
--test-fail: #501414;
|
||||
--test-warning: #5a3b00;
|
||||
--test-other: #414141;
|
||||
--cap-even: #0052882d;
|
||||
--cap-hover: #007ccf50;
|
||||
--header-color: #b7c8d2;
|
||||
--note-color: #ee4e04;
|
||||
--header-bottom: rgb(221, 221, 221);
|
||||
--link-color: #85B065;
|
||||
--text-color: #bdbdbd;
|
||||
--border-color: #7b7b7b;
|
||||
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-secondary);
|
||||
-webkit-print-color-adjust:exact !important;
|
||||
print-color-adjust:exact !important;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
table {
|
||||
margin: auto;
|
||||
font-size: 12px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
border-collapse: collapse;
|
||||
width: 1000px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
text-align: center;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: var(--header-color);
|
||||
}
|
||||
|
||||
h4 {
|
||||
text-align: center;
|
||||
justify-content: start;
|
||||
font-size: 10px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: var(--note-color);
|
||||
margin-left:20%;
|
||||
margin-right: 20%;
|
||||
margin-bottom:5px;
|
||||
}
|
||||
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
header {
|
||||
width: 1000px;
|
||||
margin: auto;
|
||||
border-bottom: 1px solid var(--header-bottom);
|
||||
margin-bottom: 25px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: end;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
header h3 {
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
border-bottom: 5px solid rgba(0, 0, 0, 0);
|
||||
color: var(--header-color);
|
||||
display: table-cell;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
header a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
header h3:hover {
|
||||
border-bottom: 5px solid var(--header-color);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 4px;
|
||||
margin: 0px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
td a {
|
||||
color: var(--header-color);
|
||||
}
|
||||
|
||||
table, th, td {
|
||||
border: 1px solid;
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
main {
|
||||
background-color: var(--background-primary);
|
||||
width: 1100px;
|
||||
margin: auto;
|
||||
position: relative;
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: var(--header-color);
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
color: var(--header-color);
|
||||
font-size: 16px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
#caps {
|
||||
width: 1000px;
|
||||
margin: auto;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
#caps h2 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#caps tr:nth-child(even){
|
||||
background-color: var(--cap-even);
|
||||
}
|
||||
|
||||
#caps tr:hover{
|
||||
background-color: var(--cap-hover);
|
||||
}
|
||||
|
||||
#caps ul {
|
||||
padding: 0;
|
||||
padding-left: 2px;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
#caps img:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#caps td:nth-child(1), #caps th:nth-child(1) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
#caps td:nth-child(2), #caps th:nth-child(2) {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
#caps span {
|
||||
cursor: pointer;
|
||||
color: var(--header-color);
|
||||
text-decoration:underline;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
width: 200px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
table.caps_table {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
th.state {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th.apps_actions {
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
th.users {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
th.conditions {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
#toggle-text {
|
||||
color: #7b7b7b;
|
||||
margin-left: 50px;
|
||||
margin-bottom: 5px;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* The switch - the box around the slider */
|
||||
.switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: var(--toggle-width);
|
||||
height: var(--toggle-height);
|
||||
left: 50px;
|
||||
}
|
||||
|
||||
/* Hide default HTML checkbox */
|
||||
.switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* The slider */
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #b9bec2;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: var(--toggle-radius);
|
||||
width: var(--toggle-radius);
|
||||
left: 4px;
|
||||
bottom: 3.5px;
|
||||
background-color: white;
|
||||
-webkit-transition: .4s;
|
||||
transition: .4s;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #005288ad;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #005288ad;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(var(--toggle-radius));
|
||||
-ms-transform: translateX(var(--toggle-radius));
|
||||
transform: translateX(var(--toggle-radius));
|
||||
}
|
||||
|
||||
.slider.round {
|
||||
border-radius: var(--toggle-height);
|
||||
}
|
||||
|
||||
.slider.round:before {
|
||||
border-radius: 50%;
|
||||
}
|
||||
1379
PowerShell/ScubaGear/Modules/Orchestrator.psm1
Normal file
1379
PowerShell/ScubaGear/Modules/Orchestrator.psm1
Normal file
File diff suppressed because it is too large
Load Diff
325
PowerShell/ScubaGear/Modules/Providers/ExportAADProvider.psm1
Normal file
325
PowerShell/ScubaGear/Modules/Providers/ExportAADProvider.psm1
Normal file
@@ -0,0 +1,325 @@
|
||||
function Export-AADProvider {
|
||||
<#
|
||||
.Description
|
||||
Gets the Azure Active Directory (AAD) settings that are relevant
|
||||
to the SCuBA AAD baselines using a subset of the modules under the
|
||||
overall Microsoft Graph PowerShell Module
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
|
||||
Import-Module $PSScriptRoot/ProviderHelpers/CommandTracker.psm1
|
||||
$Tracker = Get-CommandTracker
|
||||
|
||||
# The below cmdlet covers the following baselines
|
||||
# - 2.1
|
||||
# - 2.2
|
||||
# - 2.3 First Policy bullet
|
||||
# - 2.4 First Policy bullet
|
||||
# - 2.9
|
||||
# - 2.10
|
||||
# - 2.17 first part
|
||||
$AllPolicies = $Tracker.TryCommand("Get-MgIdentityConditionalAccessPolicy")
|
||||
|
||||
Import-Module $PSScriptRoot/ProviderHelpers/AADConditionalAccessHelper.psm1
|
||||
$CapHelper = Get-CapTracker
|
||||
$CapTableData = $CapHelper.ExportCapPolicies($AllPolicies) # pre-processed version of the CAPs used in generating
|
||||
# the CAP html in the report
|
||||
|
||||
if ($CapTableData -eq "") {
|
||||
# Quick sanity check, did ExportCapPolicies return something?
|
||||
Write-Warning "Error parsing CAP data, empty json returned from ExportCapPolicies."
|
||||
$CapTableData = "[]"
|
||||
}
|
||||
try {
|
||||
# Final sanity check, did ExportCapPolicies return valid json?
|
||||
ConvertFrom-Json $CapTableData -ErrorAction "Stop" | Out-Null
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error parsing CAP data, invalid json returned from ExportCapPolicies."
|
||||
$CapTableData = "[]"
|
||||
}
|
||||
|
||||
$AllPolicies = ConvertTo-Json -Depth 10 @($AllPolicies)
|
||||
|
||||
# Get a list of the tenant's provisioned service plans - used to see if the tenant has AAD premium p2 license required for some checks
|
||||
# The Rego looks at the service_plans in the JSON
|
||||
$ServicePlans = $Tracker.TryCommand("Get-MgSubscribedSku").ServicePlans | Where-Object -Property ProvisioningStatus -eq -Value "Success"
|
||||
|
||||
if ($ServicePlans) {
|
||||
# The RequiredServicePlan variable is used so that PIM Cmdlets are only executed if the tenant has the premium license
|
||||
$RequiredServicePlan = $ServicePlans | Where-Object -Property ServicePlanName -eq -Value "AAD_PREMIUM_P2"
|
||||
|
||||
# Get-PrivilegedUser provides a list of privileged users and their role assignments. Used for 2.11 and 2.12
|
||||
if ($RequiredServicePlan) {
|
||||
# If the tenant has the premium license then we want to also include PIM Eligible role assignments - otherwise we don't to avoid an API error
|
||||
$PrivilegedUsers = $Tracker.TryCommand("Get-PrivilegedUser", @{"TenantHasPremiumLicense"=$true})
|
||||
}
|
||||
else{
|
||||
$PrivilegedUsers = $Tracker.TryCommand("Get-PrivilegedUser")
|
||||
}
|
||||
$PrivilegedUsers = $PrivilegedUsers | ConvertTo-Json
|
||||
# The above Converto-Json call doesn't need to have the input wrapped in an
|
||||
# array (e.g, "ConvertTo-Json (@PrivilegedUsers)") because $PrivilegedUsers is
|
||||
# a dictionary, not an array, and ConvertTo-Json doesn't mess up dictionaries
|
||||
# like it does arrays (just observe the difference in output between
|
||||
# "@{} | ConvertTo-Json" and
|
||||
# "@() | ConvertTo-Json" )
|
||||
$PrivilegedUsers = if ($null -eq $PrivilegedUsers) {"{}"} else {$PrivilegedUsers}
|
||||
# While ConvertTo-Json won't mess up a dict as described in the above comment,
|
||||
# on error, $TryCommand returns an empty list, not a dictionary. The if/else
|
||||
# above corrects the $null ConvertTo-Json would return in that case to an empty
|
||||
# dictionary
|
||||
|
||||
# Get-PrivilegedRole provides a list of privileged roles referenced in 2.13 when checking if MFA is required for those roles
|
||||
# Get-PrivilegedRole provides data for 2.14 - 2.16, policies that evaluate conditions related to Azure AD PIM
|
||||
if ($RequiredServicePlan){
|
||||
# If the tenant has the premium license then we want to also include PIM Eligible role assignments - otherwise we don't to avoid an API error
|
||||
$PrivilegedRoles = $Tracker.TryCommand("Get-PrivilegedRole", @{"TenantHasPremiumLicense"=$true})
|
||||
}
|
||||
else {
|
||||
$PrivilegedRoles = $Tracker.TryCommand("Get-PrivilegedRole")
|
||||
}
|
||||
$PrivilegedRoles = ConvertTo-Json -Depth 10 @($PrivilegedRoles) # Depth required to get policy rule object details
|
||||
}
|
||||
else {
|
||||
Write-Warning "Omitting calls to Get-PrivilegedRole and Get-PrivilegedUser."
|
||||
$PrivilegedUsers = ConvertTo-Json @()
|
||||
$PrivilegedRoles = ConvertTo-Json @()
|
||||
$Tracker.AddUnSuccessfulCommand("Get-PrivilegedRole")
|
||||
$Tracker.AddUnSuccessfulCommand("Get-PrivilegedUser")
|
||||
}
|
||||
$ServicePlans = ConvertTo-Json -Depth 3 @($ServicePlans)
|
||||
|
||||
# 2.6, 2.7, & 2.18 1st/3rd Policy Bullets
|
||||
$AuthZPolicies = ConvertTo-Json @($Tracker.TryCommand("Get-MgPolicyAuthorizationPolicy"))
|
||||
|
||||
# 2.7 third bullet
|
||||
$DirectorySettings = ConvertTo-Json -Depth 10 @($Tracker.TryCommand("Get-MgDirectorySetting"))
|
||||
|
||||
# 2.7 Policy Bullet 2]
|
||||
$AdminConsentReqPolicies = ConvertTo-Json @($Tracker.TryCommand("Get-MgPolicyAdminConsentRequestPolicy"))
|
||||
|
||||
$SuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands())
|
||||
$UnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands())
|
||||
|
||||
# Note the spacing and the last comma in the json is important
|
||||
$json = @"
|
||||
"conditional_access_policies": $AllPolicies,
|
||||
"cap_table_data": $CapTableData,
|
||||
"authorization_policies": $AuthZPolicies,
|
||||
"admin_consent_policies": $AdminConsentReqPolicies,
|
||||
"privileged_users": $PrivilegedUsers,
|
||||
"privileged_roles": $PrivilegedRoles,
|
||||
"service_plans": $ServicePlans,
|
||||
"directory_settings": $DirectorySettings,
|
||||
"aad_successful_commands": $SuccessfulCommands,
|
||||
"aad_unsuccessful_commands": $UnSuccessfulCommands,
|
||||
"@
|
||||
|
||||
# We need to remove the backslash characters from the
|
||||
# json, otherwise rego gets mad.
|
||||
$json = $json.replace("\`"", "'")
|
||||
$json = $json.replace("\", "")
|
||||
|
||||
$json
|
||||
}
|
||||
|
||||
function Get-AADTenantDetail {
|
||||
<#
|
||||
.Description
|
||||
Gets the tenant details using the Microsoft Graph PowerShell Module
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
try {
|
||||
$OrgInfo = Get-MgOrganization -ErrorAction "Stop"
|
||||
$InitialDomain = $OrgInfo.VerifiedDomains | Where-Object {$_.isInitial}
|
||||
if (-not $InitialDomain) {
|
||||
$InitialDomain = "AAD: Domain Unretrievable"
|
||||
}
|
||||
$AADTenantInfo = @{
|
||||
"DisplayName" = $OrgInfo.DisplayName;
|
||||
"DomainName" = $InitialDomain.Name;
|
||||
"TenantId" = $OrgInfo.Id;
|
||||
"AADAdditionalData" = $OrgInfo;
|
||||
}
|
||||
$AADTenantInfo = ConvertTo-Json @($AADTenantInfo) -Depth 4
|
||||
$AADTenantInfo
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error retrieving Tenant details using Get-AADTenantDetail $($_)"
|
||||
$AADTenantInfo = @{
|
||||
"DisplayName" = "Error retrieving Display name";
|
||||
"DomainName" = "Error retrieving Domain name";
|
||||
"TenantId" = "Error retrieving Tenant ID";
|
||||
"AADAdditionalData" = "Error retrieving additional data";
|
||||
}
|
||||
$AADTenantInfo = ConvertTo-Json @($AADTenantInfo) -Depth 4
|
||||
$AADTenantInfo
|
||||
}
|
||||
}
|
||||
|
||||
function Get-PrivilegedUser {
|
||||
<#
|
||||
.Description
|
||||
Gets the array of the highly privileged users
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
param (
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[switch]
|
||||
$TenantHasPremiumLicense
|
||||
)
|
||||
|
||||
$PrivilegedUsers = @{}
|
||||
$PrivilegedRoles = @("Global Administrator", "Privileged Role Administrator", "User Administrator", "SharePoint Administrator", "Exchange Administrator", "Hybrid identity administrator", "Application Administrator", "Cloud Application Administrator")
|
||||
# Get a list of the Id values for the privileged roles in the list above.
|
||||
# The Id value is passed to other cmdlets to construct a list of users assigned to privileged roles.
|
||||
$AADRoles = Get-MgDirectoryRole -All -ErrorAction Stop | Where-Object { $_.DisplayName -in $PrivilegedRoles }
|
||||
|
||||
# Construct a list of privileged users based on the Active role assignments
|
||||
foreach ($Role in $AADRoles) {
|
||||
|
||||
# Get a list of all the users and groups Actively assigned to this role
|
||||
$UsersAssignedRole = Get-MgDirectoryRoleMember -All -ErrorAction Stop -DirectoryRoleId $Role.Id
|
||||
|
||||
foreach ($User in $UsersAssignedRole) {
|
||||
|
||||
$Objecttype = $User.AdditionalProperties."@odata.type" -replace "#microsoft.graph."
|
||||
|
||||
if ($Objecttype -eq "user") {
|
||||
if (-Not $PrivilegedUsers.ContainsKey($User.Id)) {
|
||||
$AADUser = Get-MgUser -ErrorAction Stop -UserId $User.Id
|
||||
$PrivilegedUsers[$AADUser.Id] = @{"DisplayName"=$AADUser.DisplayName; "OnPremisesImmutableId"=$AADUser.OnPremisesImmutableId; "roles"=@()}
|
||||
}
|
||||
$PrivilegedUsers[$User.Id].roles += $Role.DisplayName
|
||||
}
|
||||
|
||||
elseif ($Objecttype -eq "group") {
|
||||
# In this context $User.Id is a group identifier
|
||||
$GroupMembers = Get-MgGroupMember -All -ErrorAction Stop -GroupId $User.Id
|
||||
|
||||
foreach ($GroupMember in $GroupMembers) {
|
||||
$Membertype = $GroupMember.AdditionalProperties."@odata.type" -replace "#microsoft.graph."
|
||||
if ($Membertype -eq "user") {
|
||||
if (-Not $PrivilegedUsers.ContainsKey($GroupMember.Id)) {
|
||||
$AADUser = Get-MgUser -ErrorAction Stop -UserId $GroupMember.Id
|
||||
$PrivilegedUsers[$AADUser.Id] = @{"DisplayName"=$AADUser.DisplayName; "OnPremisesImmutableId"=$AADUser.OnPremisesImmutableId; "roles"=@()}
|
||||
}
|
||||
$PrivilegedUsers[$GroupMember.Id].roles += $Role.DisplayName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Process the Eligible role assignments if the premium license for PIM is there
|
||||
if ($TenantHasPremiumLicense) {
|
||||
# Get a list of all the users and groups that have Eligible assignments
|
||||
$AllPIMRoleAssignments = Get-MgRoleManagementDirectoryRoleEligibilityScheduleInstance -All -ErrorAction Stop
|
||||
|
||||
# Add to the list of privileged users based on Eligible assignments
|
||||
foreach ($Role in $AADRoles) {
|
||||
$PrivRoleId = $Role.RoleTemplateId
|
||||
# Get a list of all the users and groups Eligible assigned to this role
|
||||
$PIMRoleAssignments = $AllPIMRoleAssignments | Where-Object { $_.RoleDefinitionId -eq $PrivRoleId }
|
||||
|
||||
foreach ($PIMRoleAssignment in $PIMRoleAssignments) {
|
||||
$UserObjectId = $PIMRoleAssignment.PrincipalId
|
||||
try {
|
||||
$UserType = "user"
|
||||
|
||||
if (-Not $PrivilegedUsers.ContainsKey($UserObjectId)) {
|
||||
$AADUser = Get-MgUser -ErrorAction Stop -Filter "Id eq '$UserObjectId'"
|
||||
$PrivilegedUsers[$AADUser.Id] = @{"DisplayName"=$AADUser.DisplayName; "OnPremisesImmutableId"=$AADUser.OnPremisesImmutableId; "roles"=@()}
|
||||
}
|
||||
$PrivilegedUsers[$UserObjectId].roles += $Role.DisplayName
|
||||
}
|
||||
# Catch the specific error which indicates Get-MgUser does not find the user, therefore it is a group
|
||||
catch {
|
||||
if ($_.FullyQualifiedErrorId.Contains("Request_ResourceNotFound")) {
|
||||
$UserType = "group"
|
||||
}
|
||||
else {
|
||||
throw $_
|
||||
}
|
||||
}
|
||||
|
||||
# This if statement handles when the object eligible assigned is a Group
|
||||
if ($UserType -eq "group") {
|
||||
$GroupMembers = Get-MgGroupMember -All -ErrorAction Stop -GroupId $UserObjectId
|
||||
foreach ($GroupMember in $GroupMembers) {
|
||||
$Membertype = $GroupMember.AdditionalProperties."@odata.type" -replace "#microsoft.graph."
|
||||
if ($Membertype -eq "user") {
|
||||
if (-Not $PrivilegedUsers.ContainsKey($GroupMember.Id)) {
|
||||
$AADUser = Get-MgUser -ErrorAction Stop -UserId $GroupMember.Id
|
||||
$PrivilegedUsers[$AADUser.Id] = @{"DisplayName"=$AADUser.DisplayName; "OnPremisesImmutableId"=$AADUser.OnPremisesImmutableId; "roles"=@()}
|
||||
}
|
||||
$PrivilegedUsers[$GroupMember.Id].roles += $Role.DisplayName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$PrivilegedUsers
|
||||
}
|
||||
|
||||
function Get-PrivilegedRole {
|
||||
<#
|
||||
.Description
|
||||
Creates an array of the highly privileged roles along with the users assigned to the role and the security policies (aka rules) applied to it
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
param (
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[switch]
|
||||
$TenantHasPremiumLicense
|
||||
)
|
||||
|
||||
$PrivilegedRoles = @("Global Administrator", "Privileged Role Administrator", "User Administrator", "SharePoint Administrator", "Exchange Administrator", "Hybrid identity administrator", "Application Administrator", "Cloud Application Administrator")
|
||||
# Get a list of the RoleTemplateId values for the privileged roles in the list above.
|
||||
# The RoleTemplateId value is passed to other cmdlets to retrieve role security policies and user assignments.
|
||||
$AADRoles = Get-MgDirectoryRoleTemplate -All -ErrorAction Stop | Where-Object { $_.DisplayName -in $PrivilegedRoles } | Select-Object "DisplayName", @{Name='RoleTemplateId'; Expression={$_.Id}}
|
||||
|
||||
# If the tenant has the premium license then you can access the PIM service to get the role configuration policies and the active role assigments
|
||||
if ($TenantHasPremiumLicense) {
|
||||
# Get all the roles and policies (rules) assigned to them
|
||||
$RolePolicyAssignments = Get-MgPolicyRoleManagementPolicyAssignment -All -ErrorAction Stop -Filter "scopeId eq '/' and scopeType eq 'Directory'"
|
||||
|
||||
# Get ALL the roles and users actively assigned to them
|
||||
$AllRoleAssignments = Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -All -ErrorAction Stop
|
||||
|
||||
foreach ($Role in $AADRoles) {
|
||||
$RolePolicies = @()
|
||||
$RoleTemplateId = $Role.RoleTemplateId
|
||||
|
||||
# Get a list of the rules (aka policies) assigned to this role
|
||||
$PolicyAssignment = $RolePolicyAssignments | Where-Object -Property RoleDefinitionId -eq -Value $RoleTemplateId
|
||||
|
||||
# Get the details of policy (rule)
|
||||
if ($PolicyAssignment.length -eq 1) {
|
||||
$RolePolicies = Get-MgPolicyRoleManagementPolicyRule -All -ErrorAction Stop -UnifiedRoleManagementPolicyId $PolicyAssignment.PolicyId
|
||||
}
|
||||
elseif ($PolicyAssignment.length -gt 1) {
|
||||
$RolePolicies = "Too many policies found"
|
||||
}
|
||||
else {
|
||||
$RolePolicies = "No policies found"
|
||||
}
|
||||
|
||||
# Get a list of the users / groups assigned to this role
|
||||
$RoleAssignments = @($AllRoleAssignments | Where-Object { $_.RoleDefinitionId -eq $RoleTemplateId })
|
||||
|
||||
# Store the data that we retrieved in the Role object that will be returned from this function
|
||||
$Role | Add-Member -Name "Rules" -Value $RolePolicies -MemberType NoteProperty
|
||||
$Role | Add-Member -Name "Assignments" -Value $RoleAssignments -MemberType NoteProperty
|
||||
}
|
||||
}
|
||||
|
||||
$AADRoles
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
function Export-DefenderProvider {
|
||||
<#
|
||||
.Description
|
||||
Gets the Microsoft 365 Defender settings that are relevant
|
||||
to the SCuBA Microsft 365 Defender baselines using the EXO PowerShell Module
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$M365Environment,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[hashtable]
|
||||
$ServicePrincipalParams
|
||||
)
|
||||
$ParentPath = Split-Path $PSScriptRoot -Parent
|
||||
$ConnectionFolderPath = Join-Path -Path $ParentPath -ChildPath "Connection"
|
||||
Import-Module (Join-Path -Path $ConnectionFolderPath -ChildPath "ConnectHelpers.psm1")
|
||||
|
||||
$HelperFolderPath = Join-Path -Path $PSScriptRoot -ChildPath "ProviderHelpers"
|
||||
Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "CommandTracker.psm1")
|
||||
$Tracker = Get-CommandTracker
|
||||
|
||||
# Manually importing the module name here to bypass cmdlet name conflicts
|
||||
# There are conflicting PowerShell Cmdlet names in EXO and Power Platform
|
||||
Import-Module ExchangeOnlineManagement
|
||||
|
||||
# Sign in for the Defender Provider if not connected
|
||||
$ExchangeConnected = Get-Command Get-OrganizationConfig -ErrorAction SilentlyContinue
|
||||
if(-not $ExchangeConnected) {
|
||||
try {
|
||||
$EXOHelperParams = @{
|
||||
M365Environment = $M365Environment;
|
||||
}
|
||||
if ($ServicePrincipalParams) {
|
||||
$EXOHelperParams += @{ServicePrincipalParams = $ServicePrincipalParams}
|
||||
}
|
||||
Connect-EXOHelper @ServicePrincipalParams;
|
||||
}
|
||||
catch {
|
||||
Write-Error "Error connecting to ExchangeOnline. $($_)"
|
||||
}
|
||||
}
|
||||
|
||||
# Regular Exchange i.e non IPPSSession cmdlets
|
||||
$AdminAuditLogConfig = ConvertTo-Json @($Tracker.TryCommand("Get-AdminAuditLogConfig"))
|
||||
$ProtectionPolicyRule = ConvertTo-Json @($Tracker.TryCommand("Get-EOPProtectionPolicyRule"))
|
||||
$MalwareFilterPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-MalwareFilterPolicy"))
|
||||
$AntiPhishPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-AntiPhishPolicy"))
|
||||
$HostedContentFilterPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-HostedContentFilterPolicy"))
|
||||
$AllDomains = ConvertTo-Json @($Tracker.TryCommand("Get-AcceptedDomain"))
|
||||
|
||||
# Test if Defender specific commands are available. If the tenant does
|
||||
# not have a defender license (plan 1 or plan 2), the following
|
||||
# commandlets will fail with "The term [Cmdlet name] is not recognized
|
||||
# as the name of a cmdlet, function, script file, or operable program,"
|
||||
# so we can test for this using Get-Command.
|
||||
if (Get-Command Get-SafeAttachmentPolicy -ErrorAction SilentlyContinue) {
|
||||
$SafeAttachmentPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-SafeAttachmentPolicy"))
|
||||
$SafeAttachmentRule = ConvertTo-Json @($Tracker.TryCommand("Get-SafeAttachmentRule"))
|
||||
$SafeLinksPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-SafeLinksPolicy"))
|
||||
$SafeLinksRule = ConvertTo-Json @($Tracker.TryCommand("Get-SafeLinksRule"))
|
||||
$ATPPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-AtpPolicyForO365"))
|
||||
$DefenderLicense = ConvertTo-Json $true
|
||||
}
|
||||
else {
|
||||
# The tenant can't make use of the defender commands
|
||||
Write-Warning "Defender license not available in tenant. Omitting the following commands: Get-SafeAttachmentPolicy, Get-SafeAttachmentRule, Get-SafeLinksPolicy, Get-SafeLinksRule, and Get-AtpPolicyForO365."
|
||||
$SafeAttachmentPolicy = ConvertTo-Json @()
|
||||
$SafeAttachmentRule = ConvertTo-Json @()
|
||||
$SafeLinksPolicy = ConvertTo-Json @()
|
||||
$SafeLinksRule = ConvertTo-Json @()
|
||||
$ATPPolicy = ConvertTo-Json @()
|
||||
$DefenderLicense = ConvertTo-Json $false
|
||||
|
||||
# While it is counter-intuitive to add these to SuccessfulCommands
|
||||
# and UnSuccessfulCommands, this is a unique error case that is
|
||||
# handled within the Rego.
|
||||
$Tracker.AddSuccessfulCommand("Get-SafeAttachmentPolicy")
|
||||
$Tracker.AddSuccessfulCommand("Get-SafeAttachmentRule")
|
||||
$Tracker.AddSuccessfulCommand("Get-SafeLinksPolicy")
|
||||
$Tracker.AddSuccessfulCommand("Get-SafeLinksRule")
|
||||
$Tracker.AddSuccessfulCommand("Get-AtpPolicyForO365")
|
||||
|
||||
$Tracker.AddUnSuccessfulCommand("Get-SafeAttachmentPolicy")
|
||||
$Tracker.AddUnSuccessfulCommand("Get-SafeAttachmentRule")
|
||||
$Tracker.AddUnSuccessfulCommand("Get-SafeLinksPolicy")
|
||||
$Tracker.AddUnSuccessfulCommand("Get-SafeLinksRule")
|
||||
$Tracker.AddUnSuccessfulCommand("Get-AtpPolicyForO365")
|
||||
}
|
||||
|
||||
# Connect to Security & Compliance
|
||||
$IPPSConnected = $false
|
||||
try {
|
||||
$DefenderHelperParams = @{
|
||||
M365Environment = $M365Environment;
|
||||
}
|
||||
|
||||
if ($ServicePrincipalParams) {
|
||||
$DefenderHelperParams += @{ServicePrincipalParams = $ServicePrincipalParams}
|
||||
}
|
||||
Connect-DefenderHelper @DefenderHelperParams
|
||||
$IPPSConnected = $true
|
||||
}
|
||||
catch {
|
||||
Write-Error "Error running Connect-IPPSSession. $($_)"
|
||||
Write-Warning "Omitting the following commands: Get-DlpCompliancePolicy, Get-DlpComplianceRule, and Get-ProtectionAlert."
|
||||
$Tracker.AddUnSuccessfulCommand("Get-DlpCompliancePolicy")
|
||||
$Tracker.AddUnSuccessfulCommand("Get-DlpComplianceRule")
|
||||
$Tracker.AddUnSuccessfulCommand("Get-ProtectionAlert")
|
||||
}
|
||||
if ($IPPSConnected) {
|
||||
$DLPCompliancePolicy = ConvertTo-Json @($Tracker.TryCommand("Get-DlpCompliancePolicy"))
|
||||
$ProtectionAlert = ConvertTo-Json @($Tracker.TryCommand("Get-ProtectionAlert"))
|
||||
$DLPComplianceRules = @($Tracker.TryCommand("Get-DlpComplianceRule"))
|
||||
|
||||
# Powershell is inconsistent with how it saves lists to json.
|
||||
# This loop ensures that the format of ContentContainsSensitiveInformation
|
||||
# will *always* be a list.
|
||||
|
||||
foreach($Rule in $DLPComplianceRules) {
|
||||
if ($Rule.Count -gt 0) {
|
||||
$Rule.ContentContainsSensitiveInformation = @($Rule.ContentContainsSensitiveInformation)
|
||||
}
|
||||
}
|
||||
|
||||
# We need to specify the depth because the data contains some
|
||||
# nested tables.
|
||||
$DLPComplianceRules = ConvertTo-Json -Depth 3 $DLPComplianceRules
|
||||
}
|
||||
else {
|
||||
$DLPCompliancePolicy = ConvertTo-Json @()
|
||||
$DLPComplianceRules = ConvertTo-Json @()
|
||||
$ProtectionAlert = ConvertTo-Json @()
|
||||
$DLPComplianceRules = ConvertTo-Json @()
|
||||
}
|
||||
|
||||
$SuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands())
|
||||
$UnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands())
|
||||
|
||||
# Note the spacing and the last comma in the json is important
|
||||
$json = @"
|
||||
"protection_policy_rules": $ProtectionPolicyRule,
|
||||
"dlp_compliance_policies": $DLPCompliancePolicy,
|
||||
"dlp_compliance_rules": $DLPComplianceRules,
|
||||
"malware_filter_policies": $MalwareFilterPolicy,
|
||||
"anti_phish_policies": $AntiPhishPolicy,
|
||||
"hosted_content_filter_policies": $HostedContentFilterPolicy,
|
||||
"safe_attachment_policies": $SafeAttachmentPolicy,
|
||||
"safe_attachment_rules": $SafeAttachmentRule,
|
||||
"all_domains": $AllDomains,
|
||||
"protection_alerts": $ProtectionAlert,
|
||||
"admin_audit_log_config": $AdminAuditLogConfig,
|
||||
"safe_links_policies": $SafeLinksPolicy,
|
||||
"safe_links_rules": $SafeLinksRule,
|
||||
"atp_policy_for_o365": $ATPPolicy,
|
||||
"defender_license": $DefenderLicense,
|
||||
"defender_successful_commands": $SuccessfulCommands,
|
||||
"defender_unsuccessful_commands": $UnSuccessfulCommands,
|
||||
"@
|
||||
|
||||
# We need to remove the backslash characters from the
|
||||
# json, otherwise rego gets mad.
|
||||
$json = $json.replace("\`"", "'")
|
||||
$json = $json.replace("\", "")
|
||||
$json
|
||||
}
|
||||
428
PowerShell/ScubaGear/Modules/Providers/ExportEXOProvider.psm1
Normal file
428
PowerShell/ScubaGear/Modules/Providers/ExportEXOProvider.psm1
Normal file
@@ -0,0 +1,428 @@
|
||||
function Export-EXOProvider {
|
||||
<#
|
||||
.Description
|
||||
Gets the Exchange Online (EXO) settings that are relevant
|
||||
to the SCuBA EXO baselines using the EXO PowerShell Module
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param()
|
||||
|
||||
# Manually importing the module name here to bypass cmdlet name conflicts
|
||||
# There are conflicting PowerShell Cmdlet names in EXO and Power Platform
|
||||
Import-Module ExchangeOnlineManagement
|
||||
|
||||
Import-Module $PSScriptRoot/ProviderHelpers/CommandTracker.psm1
|
||||
$Tracker = Get-CommandTracker
|
||||
<#
|
||||
2.1
|
||||
#>
|
||||
$RemoteDomains = ConvertTo-Json @($Tracker.TryCommand("Get-RemoteDomain"))
|
||||
|
||||
<#
|
||||
2.2 SPF
|
||||
#>
|
||||
$domains = $Tracker.TryCommand("Get-AcceptedDomain")
|
||||
$SPFRecords = ConvertTo-Json @($Tracker.TryCommand("Get-ScubaSpfRecords", @{"Domains"=$domains})) -Depth 3
|
||||
|
||||
<#
|
||||
2.3 DKIM
|
||||
#>
|
||||
$DKIMConfig = ConvertTo-Json @($Tracker.TryCommand("Get-DkimSigningConfig"))
|
||||
$DKIMRecords = ConvertTo-Json @($Tracker.TryCommand("Get-ScubaDkimRecords", @{"Domains"=$domains})) -Depth 3
|
||||
|
||||
<#
|
||||
2.4 DMARC
|
||||
#>
|
||||
$DMARCRecords = ConvertTo-Json @($Tracker.TryCommand("Get-ScubaDmarcRecords", @{"Domains"=$domains})) -Depth 3
|
||||
|
||||
<#
|
||||
2.5
|
||||
#>
|
||||
|
||||
$TransportConfig = ConvertTo-Json @($Tracker.TryCommand("Get-TransportConfig"))
|
||||
|
||||
<#
|
||||
2.6
|
||||
#>
|
||||
$SharingPolicy = ConvertTo-Json @($Tracker.TryCommand("Get-SharingPolicy"))
|
||||
|
||||
<#
|
||||
2.7
|
||||
#>
|
||||
|
||||
$TransportRules = ConvertTo-Json @($Tracker.TryCommand("Get-TransportRule"))
|
||||
|
||||
<#
|
||||
2.12
|
||||
#>
|
||||
|
||||
$ConnectionFilter = ConvertTo-Json @($Tracker.TryCommand("Get-HostedConnectionFilterPolicy"))
|
||||
|
||||
<#
|
||||
2.13
|
||||
#>
|
||||
$Config = $Tracker.TryCommand("Get-OrganizationConfig") | Select-Object Name, DisplayName, AuditDisabled
|
||||
$Config = ConvertTo-Json @($Config)
|
||||
|
||||
|
||||
$SuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands())
|
||||
$UnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands())
|
||||
|
||||
<#
|
||||
Save output
|
||||
#>
|
||||
$json = @"
|
||||
"remote_domains": $RemoteDomains,
|
||||
"spf_records": $SPFRecords,
|
||||
"dkim_config": $DKIMConfig,
|
||||
"dkim_records": $DKIMRecords,
|
||||
"dmarc_records": $DMARCRecords,
|
||||
"transport_config": $TransportConfig,
|
||||
"sharing_policy": $SharingPolicy,
|
||||
"transport_rule": $TransportRules,
|
||||
"conn_filter": $ConnectionFilter,
|
||||
"org_config": $Config,
|
||||
"exo_successful_commands": $SuccessfulCommands,
|
||||
"exo_unsuccessful_commands": $UnSuccessfulCommands,
|
||||
"@
|
||||
|
||||
# We need to remove the backslash characters from the
|
||||
# json, otherwise rego gets mad.
|
||||
$json = $json.replace("\`"", "'")
|
||||
$json = $json.replace("\", "")
|
||||
$json
|
||||
}
|
||||
|
||||
function Get-EXOTenantDetail {
|
||||
<#
|
||||
.Description
|
||||
Gets the tenant details using the EXO PowerShell Module
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$M365Environment
|
||||
)
|
||||
try {
|
||||
Import-Module ExchangeOnlineManagement
|
||||
$OrgConfig = Get-OrganizationConfig -ErrorAction "Stop"
|
||||
$DomainName = $OrgConfig.Name
|
||||
$TenantId = "Error retrieving Tenant ID"
|
||||
$Uri = "https://login.microsoftonline.com/$($DomainName)/.well-known/openid-configuration"
|
||||
|
||||
if (($M365Environment -eq "gcchigh") -or ($M365Environment -eq "dod")) {
|
||||
$TLD = ".us"
|
||||
$Uri = "https://login.microsoftonline$($TLD)/$($DomainName)/.well-known/openid-configuration"
|
||||
}
|
||||
try {
|
||||
$Content = (Invoke-WebRequest -Uri $Uri -ErrorAction "Stop").Content
|
||||
$TenantId = (ConvertFrom-Json $Content).token_endpoint.Split("/")[3]
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Unable to retrieve EXO Tenant ID with URI. This may be caused by proxy error see 'Running the Script Behind Some Proxies' in the README for a solution. $($_)"
|
||||
}
|
||||
|
||||
$EXOTenantInfo = @{
|
||||
"DisplayName"= $OrgConfig.DisplayName;
|
||||
"DomainName" = $DomainName;
|
||||
"TenantId" = $TenantId;
|
||||
"EXOAdditionalData" = "Unable to safely retrieve due to EXO API changes";
|
||||
}
|
||||
$EXOTenantInfo = ConvertTo-Json @($EXOTenantInfo) -Depth 4
|
||||
$EXOTenantInfo
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error retrieving Tenant details using Get-EXOTenantDetail $($_)"
|
||||
$EXOTenantInfo = @{
|
||||
"DisplayName" = "Error retrieving Display name";
|
||||
"DomainName" = "Error retrieving Domain name";
|
||||
"TenantId" = "Error retrieving Tenant ID";
|
||||
"EXOAdditionalData" = "Error retrieving additional data";
|
||||
}
|
||||
$EXOTenantInfo = ConvertTo-Json @($EXOTenantInfo) -Depth 4
|
||||
$EXOTenantInfo
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-RobustDnsTxt {
|
||||
<#
|
||||
.Description
|
||||
Requests the TXT record for the given qname. First tries to make the query over traditional DNS
|
||||
but retries over DoH in the event of failure.
|
||||
.Parameter Qname
|
||||
The fully-qualified domain name to request.
|
||||
.Parameter MaxTries
|
||||
The number of times to retry each kind of query. If all queries are unsuccessful, the traditional
|
||||
queries and the DoH queries will each be made $MaxTries times. Default is 2.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$Qname,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[ValidateRange(1, [int]::MaxValue)]
|
||||
[int]
|
||||
$MaxTries = 2
|
||||
)
|
||||
$Answers = @()
|
||||
$LogEntries = @()
|
||||
|
||||
$TryNumber = 0
|
||||
$Success = $false
|
||||
$TradEmptyOrNx = $false
|
||||
while ($TryNumber -lt $MaxTries) {
|
||||
$TryNumber += 1
|
||||
try {
|
||||
$Response = Resolve-DnsName $Qname txt -ErrorAction Stop | Where-Object {$_.Section -eq "Answer"}
|
||||
if ($Response.Strings.Length -gt 0) {
|
||||
# We got our answer, so break out of the retry loop and set $Success to $true, no
|
||||
# need to retry the traditional query or retry with DoH.
|
||||
$LogEntries += @{"query_name"=$Qname; "query_method"="traditional"; "query_result"="Query returned $($Response.Strings.Length) txt records"}
|
||||
$Answers += $Response.Strings
|
||||
$Success = $true
|
||||
break
|
||||
}
|
||||
else {
|
||||
# The answer section was empty. This usually means that while the domain exists, but
|
||||
# there are no records of the requested type. No need to retry the traditional query,
|
||||
# this was not a transient failure. Don't set $Success to $true though, as we want to
|
||||
# retry this query from a public resolver, in case the internal DNS server returns a
|
||||
# different answer than what is served to the public (i.e., split horizon DNS).
|
||||
$LogEntries += @{"query_name"=$Qname; "query_method"="traditional"; "query_result"="Query returned 0 txt records"}
|
||||
$TradEmptyOrNx = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
catch {
|
||||
if ($_.FullyQualifiedErrorId -eq "DNS_ERROR_RCODE_NAME_ERROR,Microsoft.DnsClient.Commands.ResolveDnsName") {
|
||||
# The server returned NXDomain, no need to retry the traditional query, this was not
|
||||
# a transient failure. Don't set $Success to $true though, as we want to retry this
|
||||
# query from a public resolver, in case the internal DNS server returns a different
|
||||
# answer than what is served to the public (i.e., split horizon DNS).
|
||||
$LogEntries += @{"query_name"=$Qname; "query_method"="traditional"; "query_result"="Query returned NXDomain"}
|
||||
$TradEmptyOrNx = $true
|
||||
break
|
||||
}
|
||||
else {
|
||||
# The query failed, possibly a transient failure. Retry if we haven't reached $MaxsTries.
|
||||
$LogEntries += @{"query_name"=$Qname; "query_method"="traditional"; "query_result"="Query resulted in exception, $($_.FullyQualifiedErrorId)"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $Success) {
|
||||
# The traditional DNS query(ies) failed. Retry with DoH
|
||||
$TryNumber = 0
|
||||
while ($TryNumber -lt $MaxTries) {
|
||||
$TryNumber += 1
|
||||
try {
|
||||
$Uri = "https://1.1.1.1/dns-query?name=$($Qname)&type=txt"
|
||||
$RawResponse = $(Invoke-WebRequest -H @{"accept"="application/dns-json"} -Uri $Uri -ErrorAction Stop).RawContent
|
||||
$ResponseLines = $RawResponse -Split "`n"
|
||||
$LastLine = $ResponseLines[$ResponseLines.Length - 1]
|
||||
$ResponseBody = ConvertFrom-Json $LastLine
|
||||
if ($ResponseBody.Status -eq 0) {
|
||||
# 0 indicates there was no error
|
||||
$LogEntries += @{"query_name"=$Qname; "query_method"="DoH"; "query_result"="Query returned $($ResponseBody.Answer.data.Length) txt records"}
|
||||
$Answers += ($ResponseBody.Answer.data | ForEach-Object {$_.Replace('"', '')})
|
||||
$Success = $true
|
||||
break
|
||||
}
|
||||
elseif ($ResponseBody.Status -eq 3) {
|
||||
# 3 indicates NXDomain. The DNS query succeeded, but the domain did not exist.
|
||||
# Set $Success to $true, because event though the domain does not exist, the
|
||||
# query succeeded, and this came from an external resolver so split horizon is
|
||||
# not an issue here.
|
||||
$LogEntries += @{"query_name"=$Qname; "query_method"="DoH"; "query_result"="Query returned NXDomain"}
|
||||
$Success = $true
|
||||
break
|
||||
}
|
||||
else {
|
||||
# The remainder of the response codes indicate that the query did not succeed.
|
||||
# Retry if we haven't reached $MaxTries.
|
||||
$LogEntries += @{"query_name"=$Qname; "query_method"="DoH"; "query_result"="Query returned response code $($ResponseBody.Status)"}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
# The DoH query failed, likely due to a network issue. Retry if we haven't reached
|
||||
# $MaxTries.
|
||||
$LogEntries += @{"query_name"=$Qname; "query_method"="DoH"; "query_result"="Query resulted in exception, $($_.FullyQualifiedErrorId)"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# There are three possible outcomes of this function:
|
||||
# - Full confidence: we know conclusively that the domain exists or not, either via an answer
|
||||
# from traditional DNS, an answer from DoH, or NXDomain from DoH.
|
||||
# - Medium confidence: domain likely doesn't exist, but there is some doubt (NXDomain from
|
||||
# traditonal DNS and DoH failed).
|
||||
# No confidence: all queries failed. Throw an exception in this case.
|
||||
if ($Success) {
|
||||
@{"Answers" = $Answers; "HighConfidence" = $true; "LogEntries" = $LogEntries}
|
||||
}
|
||||
elseif ($TradEmptyOrNx) {
|
||||
@{"Answers" = $Answers; "HighConfidence" = $false; "LogEntries" = $LogEntries}
|
||||
}
|
||||
else {
|
||||
$Log = ($LogEntries | ForEach-Object {ConvertTo-Json $_ -Compress}) -Join "`n"
|
||||
throw "Failed to resolve $($Qname). `n$($Log)"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ScubaSpfRecords {
|
||||
<#
|
||||
.Description
|
||||
Gets the SPF records for each domain in $Domains
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[System.Object[]]
|
||||
$Domains
|
||||
)
|
||||
|
||||
$SPFRecords = @()
|
||||
$NLowConf = 0
|
||||
|
||||
foreach ($d in $Domains) {
|
||||
$Response = Invoke-RobustDnsTxt $d.DomainName
|
||||
if (-not $Response.HighConfidence) {
|
||||
$NLowConf += 1
|
||||
}
|
||||
$DomainName = $d.DomainName
|
||||
$SPFRecords += [PSCustomObject]@{
|
||||
"domain" = $DomainName;
|
||||
"rdata" = $Response.Answers;
|
||||
"log" = $Response.LogEntries;
|
||||
}
|
||||
}
|
||||
|
||||
if ($NLowConf -gt 0) {
|
||||
Write-Warning "Get-ScubaSpfRecords: for $($NLowConf) domain(s), the tradtional DNS queries returned either NXDomain or an empty answer section and the DoH queries failed. Will assume SPF not configured, but can't guarantee that failure isn't due to something like split horizon DNS. See ProviderSettingsExport.json under 'spf_records' for more details."
|
||||
}
|
||||
$DnsLog += $Response.LogEntries
|
||||
$SPFRecords
|
||||
}
|
||||
|
||||
function Get-ScubaDkimRecords {
|
||||
<#
|
||||
.Description
|
||||
Gets the DKIM records for each domain in $Domains
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[System.Object[]]
|
||||
$Domains
|
||||
)
|
||||
|
||||
$DKIMRecords = @()
|
||||
$NLowConf = 0
|
||||
|
||||
foreach ($d in $domains) {
|
||||
$DomainName = $d.DomainName
|
||||
$selectors = "selector1", "selector2"
|
||||
$selectors += "selector1.$DomainName" -replace "\.", "-"
|
||||
$selectors += "selector2.$DomainName" -replace "\.", "-"
|
||||
|
||||
$LogEntries = @()
|
||||
foreach ($s in $selectors) {
|
||||
$Response = Invoke-RobustDnsTxt "$s._domainkey.$DomainName"
|
||||
$LogEntries += $Response.LogEntries
|
||||
if ($Response.Answers.Length -eq 0) {
|
||||
# The DKIM record does not exist with this selector, we need to try again with
|
||||
# a different one
|
||||
continue
|
||||
}
|
||||
else {
|
||||
# The DKIM record exists with this selector, no need to try the rest
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $Response.HighConfidence) {
|
||||
$NLowConf += 1
|
||||
}
|
||||
|
||||
$DKIMRecords += [PSCustomObject]@{
|
||||
"domain" = $DomainName;
|
||||
"rdata" = $Response.Answers;
|
||||
"log" = $LogEntries;
|
||||
}
|
||||
}
|
||||
|
||||
if ($NLowConf -gt 0) {
|
||||
Write-Warning "Get-ScubaDkimRecords: for $($NLowConf) domain(s), the tradtional DNS queries returned either NXDomain or an empty answer section and the DoH queries failed. Will assume DKIM not configured, but can't guarantee that failure isn't due to something like split horizon DNS. See ProviderSettingsExport.json under 'dkim_records' for more details."
|
||||
}
|
||||
$DKIMRecords
|
||||
}
|
||||
|
||||
function Get-ScubaDmarcRecords {
|
||||
<#
|
||||
.Description
|
||||
Gets the DMARC records for each domain in $Domains
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory=$true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[System.Object[]]
|
||||
$Domains
|
||||
)
|
||||
|
||||
$DMARCRecords = @()
|
||||
$NLowConf = 0
|
||||
|
||||
foreach ($d in $Domains) {
|
||||
$LogEntries = @()
|
||||
# First check to see if the record is available at the full domain level
|
||||
$DomainName = $d.DomainName
|
||||
$Response = Invoke-RobustDnsTxt "_dmarc.$DomainName"
|
||||
$LogEntries += $Response.LogEntries
|
||||
if ($Response.Answers.Length -eq 0) {
|
||||
# The domain does not exist. If the record is not available at the full domain
|
||||
# level, we need to check at the organizational domain level.
|
||||
$Labels = $d.DomainName.Split(".")
|
||||
$Labels = $d.DomainName.Split(".")
|
||||
$OrgDomain = $Labels[-2] + "." + $Labels[-1]
|
||||
$Response = Invoke-RobustDnsTxt "_dmarc.$OrgDomain"
|
||||
$LogEntries += $Response.LogEntries
|
||||
}
|
||||
|
||||
$DomainName = $d.DomainName
|
||||
if (-not $Response.HighConfidence) {
|
||||
$NLowConf += 1
|
||||
}
|
||||
$DMARCRecords += [PSCustomObject]@{
|
||||
"domain" = $DomainName;
|
||||
"rdata" = $Response.Answers;
|
||||
"log" = $LogEntries;
|
||||
}
|
||||
}
|
||||
|
||||
if ($NLowConf -gt 0) {
|
||||
Write-Warning "Get-ScubaDmarcRecords: for $($NLowConf) domain(s), the tradtional DNS queries returned either NXDomain or an empty answer section and the DoH queries failed. Will assume DMARC not configured, but can't guarantee that failure isn't due to something like split horizon DNS. See ProviderSettingsExport.json under 'dmarc_records' for more details."
|
||||
}
|
||||
$DMARCRecords
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
function Export-OneDriveProvider {
|
||||
<#
|
||||
.Description
|
||||
Gets the OneDrive settings that are relevant
|
||||
to the SCuBA OneDrive baselines using the SharePoint PowerShell Module
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[switch]
|
||||
$PnPFlag
|
||||
)
|
||||
$HelperFolderPath = Join-Path -Path $PSScriptRoot -ChildPath "ProviderHelpers"
|
||||
Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "CommandTracker.psm1")
|
||||
$Tracker = Get-CommandTracker
|
||||
|
||||
$SPOTenantInfo = ConvertTo-Json @()
|
||||
$TenantSyncInfo = ConvertTo-Json @()
|
||||
$UsedPnP = ConvertTo-Json $false
|
||||
if ($PnPFlag) {
|
||||
$SPOTenantInfo = ConvertTo-Json @($Tracker.TryCommand("Get-PnPTenant"))
|
||||
$TenantSyncInfo = ConvertTo-Json @($Tracker.TryCommand("Get-PnPTenantSyncClientRestriction"))
|
||||
$Tracker.AddSuccessfulCommand("Get-SPOTenant")
|
||||
$Tracker.AddSuccessfulCommand("Get-SPOTenantSyncClientRestriction")
|
||||
$UsedPnP = ConvertTo-Json $true
|
||||
}
|
||||
else {
|
||||
$SPOTenantInfo = ConvertTo-Json @($Tracker.TryCommand("Get-SPOTenant"))
|
||||
$TenantSyncInfo = ConvertTo-Json @($Tracker.TryCommand("Get-SPOTenantSyncClientRestriction"))
|
||||
$Tracker.AddSuccessfulCommand("Get-PnPTenant")
|
||||
$Tracker.AddSuccessfulCommand("Get-PnPTenantSyncClientRestriction")
|
||||
}
|
||||
|
||||
$SuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands())
|
||||
$UnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands())
|
||||
|
||||
# Note the spacing and the last comma in the json is important
|
||||
$json = @"
|
||||
"SPO_tenant_info": $SPOTenantInfo,
|
||||
"Tenant_sync_info": $TenantSyncInfo,
|
||||
"OneDrive_PnP_Flag": $UsedPnp,
|
||||
"OneDrive_successful_commands": $SuccessfulCommands,
|
||||
"OneDrive_unsuccessful_commands": $UnSuccessfulCommands,
|
||||
"@
|
||||
|
||||
# We need to remove the backslash characters from the json, otherwise rego gets mad.
|
||||
$json = $json.replace("\`"", "'")
|
||||
$json = $json.replace("\", "")
|
||||
$json
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
function Export-PowerPlatformProvider {
|
||||
<#
|
||||
.Description
|
||||
Gets the Power Platform settings that are relevant
|
||||
to the SCuBA Power Platform baselines using the Power Platform Administartion
|
||||
PowerShell Module
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$M365Environment
|
||||
)
|
||||
|
||||
$HelperFolderPath = Join-Path -Path $PSScriptRoot -ChildPath "ProviderHelpers"
|
||||
Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "CommandTracker.psm1")
|
||||
$Tracker = Get-CommandTracker
|
||||
|
||||
# Manually importing the module name here to bypass cmdlet name conflicts
|
||||
# There are conflicting PowerShell Cmdlet names in EXO and Power Platform
|
||||
Import-Module Microsoft.PowerApps.Administration.PowerShell -DisableNameChecking
|
||||
|
||||
|
||||
$TenantDetails = $Tracker.TryCommand("Get-TenantDetailsFromGraph")
|
||||
if ($TenantDetails.Count -gt 0) {
|
||||
$TenantID = $TenantDetails.TenantId
|
||||
}
|
||||
else {
|
||||
$TenantID = ""
|
||||
}
|
||||
|
||||
# Check if M365Enviromment is set correctly
|
||||
$TenantIdConfig = ""
|
||||
try {
|
||||
$Domains = $TenantDetails.Domains
|
||||
$TenantDomain = "Unretrievable"
|
||||
$TLD = ".com"
|
||||
if (($M365Environment -eq "gcchigh") -or ($M365Environment -eq "dod")) {
|
||||
$TLD = ".us"
|
||||
}
|
||||
foreach ($Domain in $Domains) {
|
||||
$Name = $Domain.Name
|
||||
$IsInitial = $Domain.initial
|
||||
$DomainChecker = $Name.EndsWith(".onmicrosoft$($TLD)") -and !$Name.EndsWith(".mail.onmicrosoft$($TLD)") -and $IsInitial
|
||||
if ($DomainChecker){
|
||||
$TenantDomain = $Name
|
||||
}
|
||||
}
|
||||
$Uri = "https://login.microsoftonline$($TLD)/$($TenantDomain)/.well-known/openid-configuration"
|
||||
$TenantIdConfig = (Invoke-WebRequest -Uri $Uri -ErrorAction "Stop").Content
|
||||
}
|
||||
catch {
|
||||
$EnvCheckWarning = @"
|
||||
Power Platform Provider Warning: $($_). Unable to check if M365Environment is set correctly in the Power Platform Provider. This MAY impact the output of the Power Platform Baseline report.
|
||||
See the 'Running the Script Behind Some Proxies' in the README.md for a possible solution to this warning.
|
||||
"@
|
||||
Write-Warning $EnvCheckWarning
|
||||
}
|
||||
|
||||
# Commercial: "tenant_region_scope":"NA"
|
||||
# GCC: "tenant_region_scope":"NA","tenant_region_sub_scope":"GCC",
|
||||
# GCCHigh: "tenant_region_scope":"USGov","tenant_region_sub_scope":"DODCON"
|
||||
# DoD: "tenant_region_scope":"USGov","tenant_region_sub_scope":"DOD"
|
||||
try {
|
||||
if ($TenantIdConfig -ne "") {
|
||||
$TenantIdConfigJson = ConvertFrom-Json $TenantIdConfig
|
||||
$RegionScope = $TenantIdConfigJson.tenant_region_scope
|
||||
$RegionSubScope = $TenantIdConfigJson.tenant_region_sub_scope
|
||||
if (-not $RegionSubScope) {
|
||||
$RegionSubScope = ""
|
||||
}
|
||||
|
||||
$CheckRScope = $true
|
||||
$CheckRSubScope = $true
|
||||
if ($RegionScope -eq "NA" -or $RegionScope -eq "USGov" -or $RegionScope -eq "USG") {
|
||||
switch ($M365Environment) {
|
||||
"commercial" {
|
||||
$CheckRScope = $RegionScope -eq "NA"
|
||||
$CheckRSubScope = $RegionSubScope -eq ""
|
||||
}
|
||||
"gcc" {
|
||||
$CheckRScope = $RegionScope -eq "NA"
|
||||
$CheckRSubScope = $RegionSubScope -eq "GCC"
|
||||
}
|
||||
"gcchigh" {
|
||||
$CheckRScope = $RegionScope -eq "USGov" -or $RegionScope -eq "USG"
|
||||
$CheckRSubScope = $RegionSubScope -eq "DODCON"
|
||||
}
|
||||
"dod" {
|
||||
$CheckRScope = $RegionScope -eq "USGov" -or $RegionScope -eq "USG"
|
||||
$CheckRSubScope = $RegionSubScope -eq "DOD"
|
||||
}
|
||||
default {
|
||||
throw "Unsupported or invalid M365Environment argument"
|
||||
}
|
||||
}
|
||||
}
|
||||
# spacing is intentional
|
||||
$EnvErrorMessage = @"
|
||||
"Power Platform Provider ERROR: The M365Environment parameter value is not set correctly which WILL cause the Power Platform report to display incorrect values.
|
||||
---------------------------------------
|
||||
M365Environment Parameter value: $($M365Environment)
|
||||
Your tenant's OpenId-Configuration: tenant_region_scope: $($RegionScope), tenant_region_sub_scope: $($RegionSubScope)
|
||||
"@
|
||||
if (-not ($CheckRScope -and $CheckRSubScope)) {
|
||||
throw $EnvErrorMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
|
||||
$FullEnvErrorMessage = @"
|
||||
$($_)
|
||||
---------------------------------------
|
||||
Rerun ScubaGear with the correct M365Environment parameter value
|
||||
by looking at your tenant's OpenId-Configuration displayed above and
|
||||
contrast it with the mapped values in the table below
|
||||
M365Enviroment => OpenId-Configuration
|
||||
---------------------------------------
|
||||
commercial: tenant_region_scope:NA, tenant_region_sub_scope:
|
||||
gcc: tenant_region_scope:NA, tenant_region_sub_scope: GCC
|
||||
gcchigh : tenant_region_scope:USGov, tenant_region_sub_scope: DODCON
|
||||
dod: tenant_region_scope:USGov, tenant_region_sub_scope: DOD
|
||||
---------------------------------------
|
||||
Example Rerun for gcc tenants: Invoke-Scuba -M365Environment gcc
|
||||
"@
|
||||
throw $FullEnvErrorMessage
|
||||
}
|
||||
|
||||
# 2.1
|
||||
$EnvironmentCreation = ConvertTo-Json @($Tracker.TryCommand("Get-TenantSettings"))
|
||||
|
||||
# 2.2
|
||||
$EnvironmentList = ConvertTo-Json @($Tracker.TryCommand("Get-AdminPowerAppEnvironment"))
|
||||
|
||||
# Check for null return
|
||||
if (-not $EnvironmentList) {
|
||||
$EnvironmentList = ConvertTo-Json @()
|
||||
$Tracker.AddUnSuccessfulCommand("Get-AdminPowerAppEnvironment")
|
||||
}
|
||||
|
||||
# has to be tested manually because of http 403 errors
|
||||
$DLPPolicies = ConvertTo-Json @()
|
||||
try {
|
||||
$DLPPolicies = Get-DlpPolicy -ErrorAction "Stop"
|
||||
if ($DLPPolicies.StatusCode) {
|
||||
$Tracker.AddUnSuccessfulCommand("Get-DlpPolicy")
|
||||
$StatusCode = $DLPPolicies.StatusCode
|
||||
$Message = $DLPPolicies.Message
|
||||
$DLPPolicies = ConvertTo-Json @()
|
||||
throw "$($Message) HTTP $($StatusCode) ERROR"
|
||||
}
|
||||
else {
|
||||
$DLPPolicies = ConvertTo-Json -Depth 7 @($DLPPolicies)
|
||||
$Tracker.AddSuccessfulCommand("Get-DlpPolicy")
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error running Get-DlpPolicy: $($_). <= If a HTTP 403 ERROR is thrown then this is because you do not have the proper permissions. Necessary roles for running ScubaGear with Power Platform: Power Platform Administrator with a Power Apps License or Global Admininstrator"
|
||||
}
|
||||
|
||||
# 2.3
|
||||
# has to be tested manually because of http 403 errors
|
||||
$TenantIsolation = ConvertTo-Json @()
|
||||
try {
|
||||
$TenantIso = Get-PowerAppTenantIsolationPolicy -TenantID $TenantID -ErrorAction "Stop"
|
||||
if ($TenantIso.StatusCode) {
|
||||
$Tracker.AddUnSuccessfulCommand("Get-PowerAppTenantIsolationPolicy")
|
||||
$TenantIsolation = ConvertTo-Json @()
|
||||
$StatusCode = $DLPPolicies.StatusCode
|
||||
$Message = $DLPPolicies.Message
|
||||
throw "$($Message) HTTP $($StatusCode) ERROR"
|
||||
}
|
||||
else {
|
||||
$Tracker.AddSuccessfulCommand("Get-PowerAppTenantIsolationPolicy")
|
||||
$TenantIsolation = ConvertTo-Json @($TenantIso)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error running Get-PowerAppTenantIsolationPolicy: $($_). <= If a HTTP 403 ERROR is thrown then this is because you do not have the proper permissions. Necessary roles for running ScubaGear with Power Platform: Power Platform Administrator with a Power Apps License or Global Admininstrator"
|
||||
}
|
||||
|
||||
# 2.4 currently has no corresponding PowerShell Cmdlet
|
||||
|
||||
$PowerPlatformSuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands())
|
||||
$PowerPlatformUnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands())
|
||||
|
||||
# tenant_id added for testing purposes
|
||||
# Note the spacing and the last comma in the json is important
|
||||
$json = @"
|
||||
"tenant_id": "$TenantID",
|
||||
"environment_creation": $EnvironmentCreation,
|
||||
"dlp_policies": $DLPPolicies,
|
||||
"tenant_isolation": $TenantIsolation,
|
||||
"environment_list": $EnvironmentList,
|
||||
"powerplatform_successful_commands": $PowerPlatformSuccessfulCommands,
|
||||
"powerplatform_unsuccessful_commands": $PowerPlatformUnSuccessfulCommands,
|
||||
"@
|
||||
|
||||
# We need to remove the backslash characters from the
|
||||
# json, otherwise rego gets mad.
|
||||
$json = $json.replace("\`"", "'")
|
||||
$json = $json.replace("\", "")
|
||||
$json = $json -replace "[^\x00-\x7f]","" # remove all characters that are not utf-8
|
||||
$json
|
||||
}
|
||||
|
||||
function Get-PowerPlatformTenantDetail {
|
||||
<#
|
||||
.Description
|
||||
Gets the M365 tenant details using the Power Platform PowerShell Module
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$M365Environment
|
||||
)
|
||||
Import-Module Microsoft.PowerApps.Administration.PowerShell -DisableNameChecking
|
||||
|
||||
try {
|
||||
$PowerTenantDetails = Get-TenantDetailsFromGraph -ErrorAction "Stop"
|
||||
|
||||
$Domains = $PowerTenantDetails.Domains
|
||||
$TenantDomain = "PowerPlatform: Domain Unretrievable"
|
||||
$TLD = ".com"
|
||||
if (($M365Environment -eq "gcchigh") -or ($M365Environment -eq "dod")) {
|
||||
$TLD = ".us"
|
||||
}
|
||||
foreach ($Domain in $Domains) {
|
||||
$Name = $Domain.Name
|
||||
$IsInitial = $Domain.initial
|
||||
$DomainChecker = $Name.EndsWith(".onmicrosoft$($TLD)") -and !$Name.EndsWith(".mail.onmicrosoft$($TLD)") -and $IsInitial
|
||||
if ($DomainChecker){
|
||||
$TenantDomain = $Name
|
||||
}
|
||||
}
|
||||
|
||||
$PowerTenantInfo = @{
|
||||
"DisplayName" = $PowerTenantDetails.DisplayName;
|
||||
"DomainName" = $TenantDomain;
|
||||
"TenantId" = $PowerTenantDetails.TenantId
|
||||
"PowerPlatformAdditionalData" = $PowerTenantDetails;
|
||||
}
|
||||
$PowerTenantInfo = ConvertTo-Json @($PowerTenantInfo) -Depth 4
|
||||
$PowerTenantInfo
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error retrieving Tenant details using Get-PowerPlatformTenantDetail $($_)"
|
||||
$PowerTenantInfo = @{
|
||||
"DisplayName" = "Error retrieving Display name";
|
||||
"DomainName" = "Error retrieving Domain name";
|
||||
"TenantId" = "Error retrieving Tenant ID";
|
||||
"PowerPlatformAdditionalData" = "Error retrieving additional data";
|
||||
}
|
||||
$PowerTenantInfo = ConvertTo-Json @($PowerTenantInfo) -Depth 4
|
||||
$PowerTenantInfo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
function Export-SharePointProvider {
|
||||
<#
|
||||
.Description
|
||||
Gets the SharePoint settings that are relevant
|
||||
to the SCuBA SharePoint baselines using the SharePoint PowerShell Module
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
|
||||
#[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$M365Environment,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]
|
||||
$PnPFlag
|
||||
)
|
||||
$HelperFolderPath = Join-Path -Path $PSScriptRoot -ChildPath "ProviderHelpers"
|
||||
Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "CommandTracker.psm1")
|
||||
Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "SPOSiteHelper.psm1")
|
||||
$Tracker = Get-CommandTracker
|
||||
|
||||
#Get InitialDomainPrefix
|
||||
$InitialDomain = ($Tracker.TryCommand("Get-MgOrganization")).VerifiedDomains | Where-Object {$_.isInitial}
|
||||
$InitialDomainPrefix = $InitialDomain.Name.split(".")[0]
|
||||
|
||||
#Get SPOSiteIdentity
|
||||
$SPOSiteIdentity = Get-SPOSiteHelper -M365Environment $M365Environment -InitialDomainPrefix $InitialDomainPrefix
|
||||
|
||||
|
||||
$SPOTenant = ConvertTo-Json @()
|
||||
$SPOSite = ConvertTo-Json @()
|
||||
if ($PnPFlag) {
|
||||
$SPOTenant = ConvertTo-Json @($Tracker.TryCommand("Get-PnPTenant"))
|
||||
$SPOSite = ConvertTo-Json @($Tracker.TryCommand("Get-PnPTenantSite",@{"Identity"="$($SPOSiteIdentity)"; "Detailed"=$true}) | Select-Object -Property *)
|
||||
$Tracker.AddSuccessfulCommand("Get-SPOTenant")
|
||||
$Tracker.AddSuccessfulCommand("Get-SPOSite")
|
||||
}
|
||||
else {
|
||||
$SPOTenant = ConvertTo-Json @($Tracker.TryCommand("Get-SPOTenant"))
|
||||
$SPOSite = ConvertTo-Json @($Tracker.TryCommand("Get-SPOSite", @{"Identity"="$($SPOSiteIdentity)"; "Detailed"=$true}) | Select-Object -Property *)
|
||||
$Tracker.AddSuccessfulCommand("Get-PnPTenant")
|
||||
$Tracker.AddSuccessfulCommand("Get-PnPTenantSite")
|
||||
}
|
||||
|
||||
|
||||
$SuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands())
|
||||
$UnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands())
|
||||
|
||||
# Note the spacing and the last comma in the json is important
|
||||
$json = @"
|
||||
"SPO_tenant": $SPOTenant,
|
||||
"SPO_site": $SPOSite,
|
||||
"SharePoint_successful_commands": $SuccessfulCommands,
|
||||
"SharePoint_unsuccessful_commands": $UnSuccessfulCommands,
|
||||
"@
|
||||
|
||||
# We need to remove the backslash characters from the json, otherwise rego gets mad.
|
||||
$json = $json.replace("\`"", "'")
|
||||
$json = $json.replace("\", "")
|
||||
$json
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
function Export-TeamsProvider {
|
||||
<#
|
||||
.Description
|
||||
Gets the Teams settings that are relevant
|
||||
to the SCuBA Teams baselines using the Teams PowerShell Module
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
|
||||
$HelperFolderPath = Join-Path -Path $PSScriptRoot -ChildPath "ProviderHelpers"
|
||||
Import-Module (Join-Path -Path $HelperFolderPath -ChildPath "CommandTracker.psm1")
|
||||
$Tracker = Get-CommandTracker
|
||||
|
||||
$TenantInfo = ConvertTo-Json @($Tracker.TryCommand("Get-CsTenant"))
|
||||
$MeetingPolicies = ConvertTo-Json @($Tracker.TryCommand("Get-CsTeamsMeetingPolicy"))
|
||||
$FedConfig = ConvertTo-Json @($Tracker.TryCommand("Get-CsTenantFederationConfiguration"))
|
||||
$ClientConfig = ConvertTo-Json @($Tracker.TryCommand("Get-CsTeamsClientConfiguration"))
|
||||
$AppPolicies = ConvertTo-Json @($Tracker.TryCommand("Get-CsTeamsAppPermissionPolicy"))
|
||||
$BroadcastPolicies = ConvertTo-Json @($Tracker.TryCommand("Get-CsTeamsMeetingBroadcastPolicy"))
|
||||
|
||||
$TeamsSuccessfulCommands = ConvertTo-Json @($Tracker.GetSuccessfulCommands())
|
||||
$TeamsUnSuccessfulCommands = ConvertTo-Json @($Tracker.GetUnSuccessfulCommands())
|
||||
|
||||
# Note the spacing and the last comma in the json is important
|
||||
$json = @"
|
||||
"teams_tenant_info": $TenantInfo,
|
||||
"meeting_policies": $MeetingPolicies,
|
||||
"federation_configuration": $FedConfig,
|
||||
"client_configuration": $ClientConfig,
|
||||
"app_policies": $AppPolicies,
|
||||
"broadcast_policies": $BroadcastPolicies,
|
||||
"teams_successful_commands": $TeamsSuccessfulCommands,
|
||||
"teams_unsuccessful_commands": $TeamsUnSuccessfulCommands,
|
||||
"@
|
||||
|
||||
# We need to remove the backslash characters from the
|
||||
# json, otherwise rego gets mad.
|
||||
$json = $json.replace("\`"", "'")
|
||||
$json = $json.replace("\", "")
|
||||
$json
|
||||
}
|
||||
|
||||
function Get-TeamsTenantDetail {
|
||||
<#
|
||||
.Description
|
||||
Gets the M365 tenant details using the Teams PowerShell Module
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet("commercial", "gcc", "gcchigh", "dod", IgnoreCase = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$M365Environment
|
||||
)
|
||||
# Need to explicitly clear or convert these values to strings, otherwise
|
||||
# these fields contain values Rego can't parse.
|
||||
try {
|
||||
$TenantInfo = Get-CsTenant -ErrorAction "Stop"
|
||||
|
||||
$VerifiedDomains = $TenantInfo.VerifiedDomains
|
||||
$TenantDomain = "Teams: Domain Unretrievable"
|
||||
$TLD = ".com"
|
||||
if (($M365Environment -eq "gcchigh") -or ($M365Environment -eq "dod")) {
|
||||
$TLD = ".us"
|
||||
}
|
||||
foreach ($Domain in $VerifiedDomains.GetEnumerator()) {
|
||||
$Name = $Domain.Name
|
||||
$Status = $Domain.Status
|
||||
$DomainChecker = $Name.EndsWith(".onmicrosoft$($TLD)") -and !$Name.EndsWith(".mail.onmicrosoft$($TLD)") -and $Status -eq "Enabled"
|
||||
if ($DomainChecker) {
|
||||
$TenantDomain = $Name
|
||||
}
|
||||
}
|
||||
|
||||
$TeamsTenantInfo = @{
|
||||
"DisplayName" = $TenantInfo.DisplayName;
|
||||
"DomainName" = $TenantDomain;
|
||||
"TenantId" = $TenantInfo.TenantId;
|
||||
"TeamsAdditionalData" = $TenantInfo;
|
||||
}
|
||||
$TeamsTenantInfo = ConvertTo-Json @($TeamsTenantInfo) -Depth 4
|
||||
$TeamsTenantInfo
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error retrieving Tenant details using Get-TeamsTenantDetail $($_)"
|
||||
$TeamsTenantInfo = @{
|
||||
"DisplayName" = "Error retrieving Display name";
|
||||
"DomainName" = "Error retrieving Domain name";
|
||||
"TenantId" = "Error retrieving Tenant ID";
|
||||
"TeamsAdditionalData" = "Error retrieving additional data";
|
||||
}
|
||||
$TeamsTenantInfo = ConvertTo-Json @($TeamsTenantInfo) -Depth 4
|
||||
$TeamsTenantInfo
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
class CapHelper {
|
||||
<#
|
||||
.description
|
||||
Class for parsing conditional access policies (Caps) to generate a
|
||||
pre-processed version that can be used to generate the HTML table
|
||||
of the condiational access policies in the report.
|
||||
#>
|
||||
|
||||
<# The following hashtables are used to map the codes used in the
|
||||
API output to human-friendly strings #>
|
||||
[System.Collections.Hashtable] $ExternalUserStrings = @{"b2bCollaborationGuest" = "B2B collaboration guest users";
|
||||
"b2bCollaborationMember" = "B2B collaboration member users";
|
||||
"b2bDirectConnectUser" = "B2B direct connect users";
|
||||
"internalGuest" = "Local guest users";
|
||||
"serviceProvider" = "Service provider users";
|
||||
"otherExternalUser" = "Other external users"}
|
||||
|
||||
[System.Collections.Hashtable] $StateStrings = @{"enabled" = "On";
|
||||
"enabledForReportingButNotEnforced" = "Report-only";
|
||||
"disabled" = "Off"}
|
||||
|
||||
[System.Collections.Hashtable] $ActionStrings = @{"urn:user:registersecurityinfo" = "Register security info";
|
||||
"urn:user:registerdevice" = "Register or join devices"}
|
||||
|
||||
[System.Collections.Hashtable] $ClientAppStrings = @{"exchangeActiveSync" = "Exchange ActiveSync Clients";
|
||||
"browser" = "Browser";
|
||||
"mobileAppsAndDesktopClients" = "Mobile apps and desktop clients";
|
||||
"other" = "Other clients";
|
||||
"all" = "all"}
|
||||
|
||||
[System.Collections.Hashtable] $GrantControlStrings = @{"mfa" = "multifactor authentication";
|
||||
"compliantDevice" = "device to be marked compliant";
|
||||
"domainJoinedDevice" = "Hybrid Azure AD joined device";
|
||||
"approvedApplication" = "approved client app";
|
||||
"compliantApplication" = "app protection policy";
|
||||
"passwordChange" = "password change"}
|
||||
|
||||
[System.Collections.Hashtable] $CondAccessAppControlStrings = @{"monitorOnly" = "Monitor only";
|
||||
"blockDownloads" = "Block downloads";
|
||||
"mcasConfigured" = "Use custom policy"}
|
||||
|
||||
[string[]] GetMissingKeys([System.Object]$Obj, [string[]] $Keys) {
|
||||
<#
|
||||
.Description
|
||||
Returns a list of the keys in $Keys are not members of $Obj. Used
|
||||
to validate the structure of the conditonal access policies.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
$Missing = @()
|
||||
if ($null -eq $Obj) {
|
||||
# Note that $null needs to come first in the above check to keep the
|
||||
# linter happy. "$null should be on the left side of equality comparisons"
|
||||
return $Missing
|
||||
}
|
||||
foreach ($Key in $Keys) {
|
||||
$HasKey = [bool]($Obj.PSobject.Properties.name -match $Key)
|
||||
if (-not $HasKey) {
|
||||
$Missing += $Key
|
||||
}
|
||||
}
|
||||
return $Missing
|
||||
}
|
||||
|
||||
[string[]] GetIncludedUsers([System.Object]$Cap) {
|
||||
<#
|
||||
.Description
|
||||
Parses a given conditional access policy (Cap) to generate the list of included users/roles used in the policy.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
|
||||
# Perform some basic validation of the CAP. If some of these values
|
||||
# are missing it could indicate that the API has been restructured.
|
||||
$Missing = @()
|
||||
$Missing += $this.GetMissingKeys($Cap, @("Conditions"))
|
||||
$Missing += $this.GetMissingKeys($Cap.Conditions, @("Users"))
|
||||
$Missing += $this.GetMissingKeys($Cap.Conditions.Users, @("IncludeGroups",
|
||||
"IncludeGuestsOrExternalUsers", "IncludeRoles", "IncludeUsers"))
|
||||
if ($Missing.Length -gt 0) {
|
||||
Write-Warning "Conditional access policy structure not as expected. The following keys are missing: $($Missing -Join ', ')"
|
||||
return @()
|
||||
}
|
||||
|
||||
# Begin processing the CAP
|
||||
$Output = @()
|
||||
|
||||
$CapIncludedUsers = $Cap.Conditions.Users.IncludeUsers
|
||||
if ($CapIncludedUsers -Contains "All") {
|
||||
$Output += "All"
|
||||
}
|
||||
elseif ($CapIncludedUsers -Contains "None") {
|
||||
$Output += "None"
|
||||
}
|
||||
else {
|
||||
# Users
|
||||
if ($CapIncludedUsers.Length -eq 1) {
|
||||
$Output += "1 specific user"
|
||||
}
|
||||
elseif ($CapIncludedUsers.Length -gt 1) {
|
||||
$Output += "$($CapIncludedUsers.Length) specific users"
|
||||
}
|
||||
|
||||
# Roles
|
||||
$CapIncludedRoles = $Cap.Conditions.Users.IncludeRoles
|
||||
if ($Cap.Conditions.Users.IncludeRoles.Length -eq 1) {
|
||||
$Output += "1 specific role"
|
||||
}
|
||||
elseif ($CapIncludedRoles.Length -gt 1) {
|
||||
$Output += "$($CapIncludedRoles.Length) specific roles"
|
||||
}
|
||||
|
||||
# Groups
|
||||
$CapIncludedGroups = $Cap.Conditions.Users.IncludeGroups
|
||||
if ($CapIncludedGroups.Length -eq 1) {
|
||||
$Output += "1 specific group"
|
||||
}
|
||||
elseif ($CapIncludedGroups.Length -gt 1) {
|
||||
$Output += "$($CapIncludedGroups.Length) specific groups"
|
||||
}
|
||||
|
||||
# External/guests
|
||||
if ($null -ne $Cap.Conditions.Users.IncludeGuestsOrExternalUsers.ExternalTenants.MembershipKind) {
|
||||
$GuestOrExternalUserTypes = $Cap.Conditions.Users.IncludeGuestsOrExternalUsers.GuestOrExternalUserTypes -Split ","
|
||||
$Output += @($GuestOrExternalUserTypes | ForEach-Object {$this.ExternalUserStrings[$_]})
|
||||
}
|
||||
}
|
||||
return $Output
|
||||
}
|
||||
|
||||
[string[]] GetExcludedUsers([System.Object]$Cap) {
|
||||
<#
|
||||
.Description
|
||||
Parses a given conditional access policy (Cap) to generate the list of excluded users/roles used in the policy.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
|
||||
# Perform some basic validation of the CAP. If some of these values
|
||||
# are missing it could indicate that the API has been restructured.
|
||||
$Missing = @()
|
||||
$Missing += $this.GetMissingKeys($Cap, @("Conditions"))
|
||||
$Missing += $this.GetMissingKeys($Cap.Conditions, @("Users"))
|
||||
$Missing += $this.GetMissingKeys($Cap.Conditions.Users, @("ExcludeGroups",
|
||||
"ExcludeGuestsOrExternalUsers", "ExcludeRoles", "ExcludeUsers"))
|
||||
if ($Missing.Length -gt 0) {
|
||||
Write-Warning "Conditional access policy structure not as expected. The following keys are missing: $($Missing -Join ', ')"
|
||||
return @()
|
||||
}
|
||||
|
||||
# Begin processing the CAP
|
||||
$Output = @()
|
||||
|
||||
# Users
|
||||
$CapExcludedUsers = $Cap.Conditions.Users.ExcludeUsers
|
||||
if ($CapExcludedUsers.Length -eq 1) {
|
||||
$Output += "1 specific user"
|
||||
}
|
||||
elseif ($CapExcludedUsers.Length -gt 1) {
|
||||
$Output += "$($CapExcludedUsers.Length) specific users"
|
||||
}
|
||||
|
||||
# Roles
|
||||
$CapExcludedRoles = $Cap.Conditions.Users.ExcludeRoles
|
||||
if ($CapExcludedRoles.Length -eq 1) {
|
||||
$Output += "1 specific role"
|
||||
}
|
||||
elseif ($CapExcludedRoles.Length -gt 1) {
|
||||
$Output += "$($CapExcludedRoles.Length) specific roles"
|
||||
}
|
||||
|
||||
# Groups
|
||||
$CapExcludedGroups = $Cap.Conditions.Users.ExcludeGroups
|
||||
if ($CapExcludedGroups.Length -eq 1) {
|
||||
$Output += "1 specific group"
|
||||
}
|
||||
elseif ($CapExcludedGroups.Length -gt 1) {
|
||||
$Output += "$($CapExcludedGroups.Length) specific groups"
|
||||
}
|
||||
|
||||
# External/guests
|
||||
if ($null -ne $Cap.Conditions.Users.ExcludeGuestsOrExternalUsers.ExternalTenants.MembershipKind) {
|
||||
$GuestOrExternalUserTypes = $Cap.Conditions.Users.ExcludeGuestsOrExternalUsers.GuestOrExternalUserTypes -Split ","
|
||||
$Output += @($GuestOrExternalUserTypes | ForEach-Object {$this.ExternalUserStrings[$_]})
|
||||
}
|
||||
|
||||
# If no users are excluded, rather than display an empty cell, display "None"
|
||||
if ($Output.Length -eq 0) {
|
||||
$Output += "None"
|
||||
}
|
||||
return $Output
|
||||
}
|
||||
|
||||
[string[]] GetApplications([System.Object]$Cap) {
|
||||
<#
|
||||
.Description
|
||||
Parses a given conditional access policy (Cap) to generate the list of included/excluded applications/actions used in the policy.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
|
||||
# Perform some basic validation of the CAP. If some of these values
|
||||
# are missing it could indicate that the API has been restructured.
|
||||
$Missing = @()
|
||||
$Missing += $this.GetMissingKeys($Cap, @("Conditions"))
|
||||
$Missing += $this.GetMissingKeys($Cap.Conditions, @("Applications"))
|
||||
$Missing += $this.GetMissingKeys($Cap.Conditions.Applications, @("ApplicationFilter",
|
||||
"ExcludeApplications", "IncludeApplications",
|
||||
"IncludeAuthenticationContextClassReferences", "IncludeUserActions"))
|
||||
if ($Missing.Length -gt 0) {
|
||||
Write-Warning "Conditional access policy structure not as expected. The following keys are missing: $($Missing -Join ', ')"
|
||||
return @()
|
||||
}
|
||||
|
||||
# Begin processing the CAP
|
||||
$Output = @()
|
||||
|
||||
$CapIncludedActions = $Cap.Conditions.Applications.IncludeUserActions
|
||||
$CapAppFilterMode = $Cap.Conditions.Applications.ApplicationFilter.Mode
|
||||
$CapIncludedApps = $Cap.Conditions.Applications.IncludeApplications
|
||||
if ($CapIncludedApps.Length -gt 0 -or
|
||||
$null -ne $CapAppFilterMode) {
|
||||
# For "Select what this policy applies to", "Cloud Apps" was selected
|
||||
$Output += "Policy applies to: apps"
|
||||
# Included apps:
|
||||
if ($CapIncludedApps -Contains "All") {
|
||||
$Output += "Apps included: All"
|
||||
}
|
||||
elseif ($CapIncludedApps -Contains "None") {
|
||||
$Output += "Apps included: None"
|
||||
}
|
||||
elseif ($CapIncludedApps.Length -eq 1) {
|
||||
$Output += "Apps included: 1 specific app"
|
||||
}
|
||||
elseif ($CapIncludedApps.Length -gt 1) {
|
||||
$Output += "Apps included: $($CapIncludedApps.Length) specific apps"
|
||||
}
|
||||
if ($CapAppFilterMode -eq "include") {
|
||||
$Output += "Apps included: custom application filter"
|
||||
}
|
||||
|
||||
$CapExcludedApps = $Cap.Conditions.Applications.ExcludeApplications
|
||||
if ($CapExcludedApps.Length -eq 1) {
|
||||
$Output += "Apps excluded: 1 specific app"
|
||||
}
|
||||
elseif ($CapExcludedApps.Length -gt 1) {
|
||||
$Output += "Apps excluded: $($CapExcludedApps.Length) specific apps"
|
||||
}
|
||||
if ($CapAppFilterMode -eq "exclude") {
|
||||
$Output += "Apps excluded: custom application filter"
|
||||
}
|
||||
if ($CapAppFilterMode -ne "exclude" -and
|
||||
$CapExcludedApps.Length -eq 0) {
|
||||
$Output += "Apps excluded: None"
|
||||
}
|
||||
}
|
||||
elseif ($CapIncludedActions.Length -gt 0) {
|
||||
# For "Select what this policy applies to", "User actions" was selected
|
||||
$Output += "Policy applies to: actions"
|
||||
$Output += "User action: $($this.ActionStrings[$CapIncludedActions[0]])"
|
||||
# While "IncludeUserActions" is a list, the GUI doesn't actually let you select more than one
|
||||
# item at a time, hence "IncludeUserActions[0]" above
|
||||
}
|
||||
else {
|
||||
# For "Select what this policy applies to", "Authentication context" was selected
|
||||
$AuthContexts = $Cap.Conditions.Applications.IncludeAuthenticationContextClassReferences
|
||||
if ($AuthContexts.Length -eq 1) {
|
||||
$Output += "Policy applies to: 1 authentication context"
|
||||
}
|
||||
else {
|
||||
$Output += "Policy applies to: $($AuthContexts.Length) authentication contexts"
|
||||
}
|
||||
}
|
||||
return $Output
|
||||
}
|
||||
|
||||
[string[]] GetConditions([System.Object]$Cap) {
|
||||
<#
|
||||
.Description
|
||||
Parses a given conditional access policy (Cap) to generate the list of conditions used in the policy.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
|
||||
# Perform some basic validation of the CAP. If some of these values
|
||||
# are missing it could indicate that the API has been restructured.
|
||||
$Missing = @()
|
||||
$Missing += $this.GetMissingKeys($Cap, @("Conditions"))
|
||||
$Missing += $this.GetMissingKeys($Cap.Conditions, @("UserRiskLevels",
|
||||
"SignInRiskLevels", "Platforms", "Locations", "ClientAppTypes", "Devices"))
|
||||
$Missing += $this.GetMissingKeys($Cap.Conditions.Platforms, @("ExcludePlatforms", "IncludePlatforms"))
|
||||
$Missing += $this.GetMissingKeys($Cap.Conditions.Locations, @("ExcludeLocations", "IncludeLocations"))
|
||||
$Missing += $this.GetMissingKeys($Cap.Conditions.Devices, @("DeviceFilter"))
|
||||
if ($Missing.Length -gt 0) {
|
||||
Write-Warning "Conditional access policy structure not as expected. The following keys are missing: $($Missing -Join ', ')"
|
||||
return @()
|
||||
}
|
||||
|
||||
# Begin processing the CAP
|
||||
$Output = @()
|
||||
|
||||
# User risk
|
||||
$CapUserRiskLevels = $Cap.Conditions.UserRiskLevels
|
||||
if ($CapUserRiskLevels.Length -gt 0) {
|
||||
$Output += "User risk levels: $($CapUserRiskLevels -Join ', ')"
|
||||
}
|
||||
# Sign-in risk
|
||||
$CapSignInRiskLevels = $Cap.Conditions.SignInRiskLevels
|
||||
if ($CapSignInRiskLevels.Length -gt 0) {
|
||||
$Output += "Sign-in risk levels: $($CapSignInRiskLevels -Join ', ')"
|
||||
}
|
||||
# Device platforms
|
||||
$CapIncludedPlatforms = $Cap.Conditions.Platforms.IncludePlatforms
|
||||
if ($null -ne $CapIncludedPlatforms) {
|
||||
$Output += "Device platforms included: $($CapIncludedPlatforms -Join ', ')"
|
||||
$CapExcludedPlatforms = $Cap.Conditions.Platforms.ExcludePlatforms
|
||||
if ($CapExcludedPlatforms.Length -eq 0) {
|
||||
$Output += "Device platforms excluded: none"
|
||||
}
|
||||
else {
|
||||
$Output += "Device platforms excluded: $($CapExcludedPlatforms -Join ', ')"
|
||||
}
|
||||
}
|
||||
# Locations
|
||||
$CapIncludedLocations = $Cap.Conditions.Locations.IncludeLocations
|
||||
if ($null -ne $CapIncludedLocations) {
|
||||
if ($CapIncludedLocations -Contains "All") {
|
||||
$Output += "Locations included: all locations"
|
||||
}
|
||||
elseif ($CapIncludedLocations -Contains "AllTrusted") {
|
||||
$Output += "Locations included: all trusted locations"
|
||||
}
|
||||
elseif ($CapIncludedLocations.Length -eq 1) {
|
||||
$Output += "Locations included: 1 specific location"
|
||||
}
|
||||
else {
|
||||
$Output += "Locations included: $($CapIncludedLocations.Length) specific locations"
|
||||
}
|
||||
|
||||
$CapExcludedLocations = $Cap.Conditions.Locations.ExcludeLocations
|
||||
if ($CapExcludedLocations -Contains "AllTrusted") {
|
||||
$Output += "Locations excluded: all trusted locations"
|
||||
}
|
||||
elseif ($CapExcludedLocations.Length -eq 0) {
|
||||
$Output += "Locations excluded: none"
|
||||
}
|
||||
elseif ($CapExcludedLocations.Length -eq 1) {
|
||||
$Output += "Locations excluded: 1 specific location"
|
||||
}
|
||||
else {
|
||||
$Output += "Locations excluded: $($CapExcludedLocations.Length) specific locations"
|
||||
}
|
||||
}
|
||||
# Client Apps
|
||||
$ClientApps += @($Cap.Conditions.ClientAppTypes | ForEach-Object {$this.ClientAppStrings[$_]})
|
||||
$Output += "Client apps included: $($ClientApps -Join ', ')"
|
||||
# Filter for devices
|
||||
if ($null -ne $Cap.Conditions.Devices.DeviceFilter.Mode) {
|
||||
if ($Cap.Conditions.Devices.DeviceFilter.Mode -eq "include") {
|
||||
$Output += "Custom device filter in include mode active"
|
||||
}
|
||||
else {
|
||||
$Output += "Custom device filter in exclude mode active"
|
||||
}
|
||||
}
|
||||
|
||||
return $Output
|
||||
}
|
||||
|
||||
[string] GetAccessControls([System.Object]$Cap) {
|
||||
<#
|
||||
.Description
|
||||
Parses a given conditional access policy (Cap) to generate the list of access controls used in the policy.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
|
||||
# Perform some basic validation of the CAP. If some of these values
|
||||
# are missing it could indicate that the API has been restructured.
|
||||
$Missing = @()
|
||||
$Missing += $this.GetMissingKeys($Cap, @("GrantControls"))
|
||||
$Missing += $this.GetMissingKeys($Cap.GrantControls, @("AuthenticationStrength",
|
||||
"BuiltInControls", "CustomAuthenticationFactors", "Operator"))
|
||||
$Missing += $this.GetMissingKeys($Cap.GrantControls.AuthenticationStrength, @("DisplayName"))
|
||||
if ($Missing.Length -gt 0) {
|
||||
Write-Warning "Conditional access policy structure not as expected. The following keys are missing: $($Missing -Join ', ')"
|
||||
return @()
|
||||
}
|
||||
|
||||
# Begin processing the CAP
|
||||
$Output = ""
|
||||
if ($null -ne $Cap.GrantControls.BuiltInControls) {
|
||||
if ($Cap.GrantControls.BuiltInControls -Contains "block") {
|
||||
$Output = "Block access"
|
||||
}
|
||||
else {
|
||||
$GrantControls = @($Cap.GrantControls.BuiltInControls | ForEach-Object {$this.GrantControlStrings[$_]})
|
||||
if ($null -ne $Cap.GrantControls.AuthenticationStrength.DisplayName) {
|
||||
$GrantControls += "authentication strength ($($Cap.GrantControls.AuthenticationStrength.DisplayName))"
|
||||
}
|
||||
|
||||
$Output = "Allow access but require $($GrantControls -Join ', ')"
|
||||
if ($GrantControls.Length -gt 1) {
|
||||
# If multiple access controls are in place, insert the AND or the OR
|
||||
# before the final access control
|
||||
$Output = $Output.Insert($Output.LastIndexOf(',')+1, " $($Cap.GrantControls.Operator)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($Output -eq "") {
|
||||
$Output = "None"
|
||||
}
|
||||
return $Output
|
||||
}
|
||||
|
||||
[string[]] GetSessionControls([System.Object]$Cap) {
|
||||
<#
|
||||
.Description
|
||||
Parses a given conditional access policy (Cap) to generate the list of session controls used in the policy.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
|
||||
# Perform some basic validation of the CAP. If some of these values
|
||||
# are missing it could indicate that the API has been restructured.
|
||||
$Missing = @()
|
||||
$Missing += $this.GetMissingKeys($Cap, @("SessionControls"))
|
||||
$Missing += $this.GetMissingKeys($Cap.SessionControls, @("ApplicationEnforcedRestrictions",
|
||||
"CloudAppSecurity", "ContinuousAccessEvaluation", "DisableResilienceDefaults",
|
||||
"PersistentBrowser", "SignInFrequency"))
|
||||
$Missing += $this.GetMissingKeys($Cap.SessionControls.ApplicationEnforcedRestrictions, @("IsEnabled"))
|
||||
$Missing += $this.GetMissingKeys($Cap.SessionControls.CloudAppSecurity, @("CloudAppSecurityType",
|
||||
"IsEnabled"))
|
||||
$Missing += $this.GetMissingKeys($Cap.SessionControls.ContinuousAccessEvaluation, @("Mode"))
|
||||
$Missing += $this.GetMissingKeys($Cap.SessionControls.PersistentBrowser, @("IsEnabled", "Mode"))
|
||||
$Missing += $this.GetMissingKeys($Cap.SessionControls.SignInFrequency, @("IsEnabled",
|
||||
"FrequencyInterval", "Type", "Value"))
|
||||
if ($Missing.Length -gt 0) {
|
||||
Write-Warning "Conditional access policy structure not as expected. The following keys are missing: $($Missing -Join ', ')"
|
||||
return @()
|
||||
}
|
||||
|
||||
# Begin processing the CAP
|
||||
$Output = @()
|
||||
if ($Cap.SessionControls.ApplicationEnforcedRestrictions.IsEnabled) {
|
||||
$Output += "Use app enforced restrictions"
|
||||
}
|
||||
if ($Cap.SessionControls.CloudAppSecurity.IsEnabled) {
|
||||
$Mode = $this.CondAccessAppControlStrings[$Cap.SessionControls.CloudAppSecurity.CloudAppSecurityType]
|
||||
$Output += "Use Conditional Access App Control ($($Mode))"
|
||||
}
|
||||
if ($Cap.SessionControls.SignInFrequency.IsEnabled) {
|
||||
if ($Cap.SessionControls.SignInFrequency.FrequencyInterval -eq "everyTime") {
|
||||
$Output += "Sign-in frequency (every time)"
|
||||
}
|
||||
else {
|
||||
$Value = $Cap.SessionControls.SignInFrequency.Value
|
||||
$Unit = $Cap.SessionControls.SignInFrequency.Type
|
||||
$Output += "Sign-in frequency (every $($Value) $($Unit))"
|
||||
}
|
||||
}
|
||||
if ($Cap.SessionControls.PersistentBrowser.IsEnabled) {
|
||||
$Mode = $Cap.SessionControls.PersistentBrowser.Mode
|
||||
$Output += "Persistent browser session ($($Mode) persistent)"
|
||||
}
|
||||
if ($Cap.SessionControls.ContinuousAccessEvaluation.Mode -eq "disabled") {
|
||||
$Output += "Customize continuous access evaluation"
|
||||
}
|
||||
if ($Cap.SessionControls.DisableResilienceDefaults) {
|
||||
$Output += "Disable resilience defaults"
|
||||
}
|
||||
if ($Output.Length -eq 0) {
|
||||
$Output += "None"
|
||||
}
|
||||
return $Output
|
||||
}
|
||||
|
||||
[string] ExportCapPolicies([System.Object]$Caps) {
|
||||
<#
|
||||
.Description
|
||||
Parses the conditional access policies (Caps) to generate a pre-processed version that can be used to
|
||||
generate the HTML of the condiational access policies in the report.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
$Table = @()
|
||||
|
||||
foreach ($Cap in $Caps) {
|
||||
$State = $this.StateStrings[$Cap.State]
|
||||
$UsersIncluded = $($this.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersExcluded = $($this.GetExcludedUsers($Cap)) -Join ", "
|
||||
$Users = @("Users included: $($UsersIncluded)", "Users excluded: $($UsersExcluded)")
|
||||
$Apps = $this.GetApplications($Cap)
|
||||
$Conditions = $this.GetConditions($Cap)
|
||||
$AccessControls = $this.GetAccessControls($Cap)
|
||||
$SessionControls = $this.GetSessionControls($Cap)
|
||||
$CapDetails = [pscustomobject]@{
|
||||
"Name" = $Cap.DisplayName;
|
||||
"State" = $State;
|
||||
"Users" = $Users
|
||||
"Apps/Actions" = $Apps;
|
||||
"Conditions" = $Conditions;
|
||||
"Block/Grant Access" = $AccessControls;
|
||||
"Session Controls" = $SessionControls;
|
||||
}
|
||||
|
||||
$Table += $CapDetails
|
||||
}
|
||||
|
||||
$CapTableJson = ConvertTo-Json $Table
|
||||
return $CapTableJson
|
||||
}
|
||||
}
|
||||
|
||||
function Get-CapTracker {
|
||||
[CapHelper]::New()
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
Import-Module -Name $PSScriptRoot/../ExportEXOProvider.psm1 -Function Get-ScubaSpfRecords, Get-ScubaDkimRecords, Get-ScubaDmarcRecords
|
||||
Import-Module -Name $PSScriptRoot/../ExportAADProvider.psm1 -Function Get-PrivilegedRole, Get-PrivilegedUser
|
||||
|
||||
class CommandTracker {
|
||||
[string[]]$SuccessfulCommands = @()
|
||||
[string[]]$UnSuccessfulCommands = @()
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command, [hashtable]$CommandArgs) {
|
||||
<#
|
||||
.Description
|
||||
Wraps the given Command inside a try/catch, run with the provided
|
||||
arguments, and tracks successes/failures. Unless otherwise specified,
|
||||
ErrorAction defaults to "Stop"
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
if (-Not $CommandArgs.ContainsKey("ErrorAction")) {
|
||||
$CommandArgs.ErrorAction = "Stop"
|
||||
}
|
||||
|
||||
try {
|
||||
$Result = & $Command @CommandArgs
|
||||
$this.SuccessfulCommands += $Command
|
||||
return $Result
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error running $($Command). $($_)"
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
$Result = @()
|
||||
return $Result
|
||||
}
|
||||
}
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command) {
|
||||
<#
|
||||
.Description
|
||||
Wraps the given Command inside a try/catch and tracks successes/
|
||||
failures. No command arguments are specified beyond ErrorAction=Stop
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
|
||||
return $this.TryCommand($Command, @{})
|
||||
}
|
||||
|
||||
[void] AddSuccessfulCommand([string]$Command) {
|
||||
$this.SuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[void] AddUnSuccessfulCommand([string]$Command) {
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[string[]] GetUnSuccessfulCommands() {
|
||||
return $this.UnSuccessfulCommands
|
||||
}
|
||||
|
||||
[string[]] GetSuccessfulCommands() {
|
||||
return $this.SuccessfulCommands
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function Get-CommandTracker {
|
||||
[CommandTracker]::New()
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
function Get-SPOSiteHelper {
|
||||
<#
|
||||
.Description
|
||||
This function is used for assisting in connecting to different M365 Environments for EXO.
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, ParameterSetName = 'Report')]
|
||||
[ValidateSet("commercial", "gcc", "gcchigh", "dod")]
|
||||
[string]
|
||||
$M365Environment,
|
||||
|
||||
[Parameter(Mandatory = $true, ParameterSetName = 'Report')]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$InitialDomainPrefix
|
||||
)
|
||||
$SPOSiteIdentity = ""
|
||||
switch ($M365Environment) {
|
||||
{"commercial" -or "gcc"} {
|
||||
$SPOSiteIdentity = "https://$($InitialDomainPrefix).sharepoint.com/"
|
||||
}
|
||||
"gcchigh" {
|
||||
$SPOSiteIdentity = "https://$($InitialDomainPrefix).sharepoint.us/"
|
||||
}
|
||||
"dod" {
|
||||
$SPOSiteIdentity = "https://$($InitialDomainPrefix).sharepoint-mil.us/"
|
||||
}
|
||||
default {
|
||||
Write-Error -Message "Unsupported or invalid M365Environment argument"
|
||||
}
|
||||
}
|
||||
$SPOSiteIdentity
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
'Get-SPOSiteHelper'
|
||||
)
|
||||
52
PowerShell/ScubaGear/Modules/RunRego/RunRego.psm1
Normal file
52
PowerShell/ScubaGear/Modules/RunRego/RunRego.psm1
Normal file
@@ -0,0 +1,52 @@
|
||||
function Invoke-Rego {
|
||||
<#
|
||||
.Description
|
||||
This function runs the specifed BaselineName rego file against the
|
||||
ProviderSettings.json using the specified OPA executable
|
||||
Returns a OPA TestResults PSObject Array
|
||||
.Functionality
|
||||
Internal
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateScript({Test-Path -PathType Leaf $_})]
|
||||
[string]
|
||||
$InputFile,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$RegoFile,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$PackageName,
|
||||
|
||||
# The path to the OPA executable. Defaults to the current directory.
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$OPAPath = $PSScriptRoot
|
||||
)
|
||||
try {
|
||||
# PowerShell 5.1 compatible Windows OS check
|
||||
if ("Windows_NT" -eq $Env:OS) {
|
||||
$Cmd = Join-Path -Path $OPAPath -ChildPath "opa_windows_amd64.exe" -ErrorAction 'Stop'
|
||||
}
|
||||
else {
|
||||
# Permissions: chmod 755 ./opa
|
||||
$Cmd = Join-Path -Path $OPAPath -ChildPath "opa" -ErrorAction 'Stop'
|
||||
}
|
||||
$CmdArgs = @("eval", "-i", $InputFile, "-d", $RegoFile, "data.$PackageName.tests", "-f", "values")
|
||||
$TestResults = $(& $Cmd @CmdArgs) | Out-String -ErrorAction 'Stop' | ConvertFrom-Json -ErrorAction 'Stop'
|
||||
$TestResults
|
||||
}
|
||||
catch {
|
||||
throw "Error calling the OPA executable: $($_)"
|
||||
}
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function @(
|
||||
'Invoke-Rego'
|
||||
)
|
||||
90
PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1
Normal file
90
PowerShell/ScubaGear/Modules/ScubaConfig/ScubaConfig.psm1
Normal file
@@ -0,0 +1,90 @@
|
||||
class ScubaConfig {
|
||||
hidden static [ScubaConfig]$_Instance = [ScubaConfig]::new()
|
||||
hidden static [Boolean]$_IsLoaded = $false
|
||||
|
||||
[Boolean]LoadConfig([System.IO.FileInfo]$Path){
|
||||
if (-Not (Test-Path -PathType Leaf $Path)){
|
||||
throw [System.IO.FileNotFoundException]"Failed to load: $Path"
|
||||
}
|
||||
elseif ($false -eq [ScubaConfig]::_IsLoaded){
|
||||
$Content = Get-Content -Raw -Path $Path
|
||||
$this.Configuration = $Content | ConvertFrom-Yaml
|
||||
|
||||
$this.SetParameterDefaults()
|
||||
[ScubaConfig]::_IsLoaded = $true
|
||||
}
|
||||
|
||||
return [ScubaConfig]::_IsLoaded
|
||||
}
|
||||
|
||||
hidden [void]ClearConfiguration(){
|
||||
Get-Member -InputObject ($this.Configuration) -Type properties |
|
||||
ForEach-Object { $this.Configuration.PSObject.Properties.Remove($_.name)}
|
||||
}
|
||||
|
||||
hidden [Guid]$Uuid = [Guid]::NewGuid()
|
||||
hidden [Object]$Configuration
|
||||
|
||||
hidden [void]SetParameterDefaults(){
|
||||
if (-Not $this.Configuration.ProductNames){
|
||||
$this.Configuration.ProductNames = "teams", "exo", "defender", "aad", "sharepoint", "onedrive", "powerplatform" | Sort-Object
|
||||
}
|
||||
else{
|
||||
$this.Configuration.ProductNames = $this.Configuration.ProductNames | Sort-Object
|
||||
}
|
||||
|
||||
if (-Not $this.Configuration.M365Environment){
|
||||
$this.Configuration.M365Environment = 'commercial'
|
||||
}
|
||||
|
||||
if (-Not $this.Configuration.OPAPath){
|
||||
$this.Configuration.OPAPath = (Join-Path -Path $PSScriptRoot -ChildPath "..\..\..")
|
||||
}
|
||||
|
||||
if (-Not $this.Configuration.LogIn){
|
||||
$this.Configuration.LogIn = $true
|
||||
}
|
||||
|
||||
if (-Not $this.Configuration.DisconnectOnExit){
|
||||
$this.Configuration.DisconnectOnExit = $false
|
||||
}
|
||||
|
||||
if (-Not $this.Configuration.OutPath){
|
||||
$this.Configuration.OutPath = '.'
|
||||
}
|
||||
|
||||
if (-Not $this.Configuration.OutFolderName){
|
||||
$this.Configuration.OutFolderName = "M365BaselineConformance"
|
||||
}
|
||||
|
||||
if (-Not $this.Configuration.OutProviderFileName){
|
||||
$this.Configuration.OutProviderFileName = "ProviderSettingsExport"
|
||||
}
|
||||
|
||||
if (-Not $this.Configuration.OutRegoFileName){
|
||||
$this.Configuration.OutFolderName = "TestResults"
|
||||
}
|
||||
|
||||
if (-Not $this.Configuration.OutReportName){
|
||||
$this.Configuration.OutReportName = "BaselineReports"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
hidden ScubaConfig(){
|
||||
}
|
||||
|
||||
static [void]ResetInstance(){
|
||||
if ([ScubaConfig]::_IsLoaded){
|
||||
[ScubaConfig]::_Instance.ClearConfiguration()
|
||||
[ScubaConfig]::_IsLoaded = $false
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
static [ScubaConfig]GetInstance(){
|
||||
return [ScubaConfig]::_Instance
|
||||
}
|
||||
}
|
||||
103
PowerShell/ScubaGear/RequiredVersions.ps1
Normal file
103
PowerShell/ScubaGear/RequiredVersions.ps1
Normal file
@@ -0,0 +1,103 @@
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ModuleList')]
|
||||
$ModuleList = @(
|
||||
@{
|
||||
ModuleName = 'MicrosoftTeams'
|
||||
ModuleVersion = [version] '4.9.3'
|
||||
MaximumVersion = [version] '5.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'ExchangeOnlineManagement' # includes Defender
|
||||
ModuleVersion = [version] '3.2.0'
|
||||
MaximumVersion = [version] '3.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Online.SharePoint.PowerShell' # includes OneDrive
|
||||
ModuleVersion = [version] '16.0.0'
|
||||
MaximumVersion = [version] '16.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'PnP.PowerShell' # alternate for SharePoint PowerShell
|
||||
ModuleVersion = [version] '1.12.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.PowerApps.Administration.PowerShell'
|
||||
ModuleVersion = [version] '2.0.0'
|
||||
MaximumVersion = [version] '2.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.PowerApps.PowerShell'
|
||||
ModuleVersion = [version] '1.0.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.Applications' #TODO: Verify is needed
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.Authentication'
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.DeviceManagement' #TODO: Verify is needed
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.DeviceManagement.Administration' #TODO: Verify is needed
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.DeviceManagement.Enrolment' #TODO: Verify is needed
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.Devices.CorporateManagement' #TODO: Verify is needed
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.Groups'
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.Identity.DirectoryManagement'
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.Identity.Governance' #TODO: Verify is needed
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.Identity.SignIns'
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.Planner' #TODO: Verify is needed
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.Teams' #TODO: Verify is needed
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'Microsoft.Graph.Users'
|
||||
ModuleVersion = [version] '1.14.0'
|
||||
MaximumVersion = [version] '1.99.99999'
|
||||
},
|
||||
@{
|
||||
ModuleName = 'powershell-yaml'
|
||||
ModuleVersion = [version] '0.4.2'
|
||||
MaximumVersion = [version] '0.99.99999'
|
||||
}
|
||||
)
|
||||
133
PowerShell/ScubaGear/ScubaGear.psd1
Normal file
133
PowerShell/ScubaGear/ScubaGear.psd1
Normal file
@@ -0,0 +1,133 @@
|
||||
#
|
||||
# Module manifest for module 'ScubaGear'
|
||||
#
|
||||
# Generated by: CISA
|
||||
#
|
||||
# Generated on: 10/18/2022
|
||||
#
|
||||
|
||||
@{
|
||||
|
||||
# Script module or binary module file associated with this manifest.
|
||||
RootModule = './ScubaGear.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '0.3.0'
|
||||
|
||||
# Supported PSEditions
|
||||
CompatiblePSEditions = 'Desktop'
|
||||
|
||||
# ID used to uniquely identify this module
|
||||
GUID = '83a07295-7ec3-44cd-95d7-d49cdfa05199'
|
||||
|
||||
# Author of this module
|
||||
Author = 'CISA'
|
||||
|
||||
# Company or vendor of this module
|
||||
CompanyName = 'Cybersecurity and Infrastructure Security Agency'
|
||||
|
||||
# Copyright statement for this module
|
||||
Copyright = '(c) 2022 CISA. All rights reserved.'
|
||||
|
||||
# Description of the functionality provided by this module
|
||||
Description = @"
|
||||
The Secure Cloud Business Applications (SCuBA) Gear module automates
|
||||
conformance testing about CISA M365 Secure Configuration Baselines.
|
||||
"@
|
||||
|
||||
# Minimum version of the Windows PowerShell engine required by this module
|
||||
PowerShellVersion = '5.1'
|
||||
|
||||
# Name of the Windows PowerShell host required by this module
|
||||
# PowerShellHostName = ''
|
||||
|
||||
# Minimum version of the Windows PowerShell host required by this module
|
||||
# PowerShellHostVersion = ''
|
||||
|
||||
# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
|
||||
# DotNetFrameworkVersion = ''
|
||||
|
||||
# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only.
|
||||
# CLRVersion = ''
|
||||
|
||||
# Processor architecture (None, X86, Amd64) required by this module
|
||||
# ProcessorArchitecture = ''
|
||||
|
||||
# Modules that must be imported into the global environment prior to importing this module
|
||||
# RequiredModules = @()
|
||||
|
||||
# Assemblies that must be loaded prior to importing this module
|
||||
# RequiredAssemblies = @()
|
||||
|
||||
# Script files (.ps1) that are run in the caller's environment prior to importing this module.
|
||||
ScriptsToProcess = @(
|
||||
'./Dependencies.ps1'
|
||||
)
|
||||
|
||||
# Type files (.ps1xml) to be loaded when importing this module
|
||||
# TypesToProcess = @()
|
||||
|
||||
# Format files (.ps1xml) to be loaded when importing this module
|
||||
# FormatsToProcess = @()
|
||||
|
||||
# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess
|
||||
# NestedModules = @()
|
||||
|
||||
# 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.
|
||||
FunctionsToExport = @(
|
||||
'Invoke-SCuBA',
|
||||
'Invoke-RunCached'
|
||||
'Disconnect-SCuBATenant'
|
||||
)
|
||||
|
||||
# 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.
|
||||
CmdletsToExport = @()
|
||||
|
||||
# Variables to export from this module
|
||||
# VariablesToExport = @()
|
||||
|
||||
# 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.
|
||||
AliasesToExport = @()
|
||||
|
||||
# DSC resources to export from this module
|
||||
# DscResourcesToExport = @()
|
||||
|
||||
# List of all modules packaged with this module
|
||||
# ModuleList = @()
|
||||
|
||||
# List of all files packaged with this module
|
||||
# FileList = @()
|
||||
|
||||
# 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.
|
||||
PrivateData = @{
|
||||
|
||||
PSData = @{
|
||||
|
||||
# Tags applied to this module. These help with module discovery in online galleries.
|
||||
# Tags = @()
|
||||
|
||||
# A URL to the license for this module.
|
||||
# LicenseUri = ''
|
||||
|
||||
# A URL to the main website for this project.
|
||||
# ProjectUri = ''
|
||||
|
||||
# A URL to an icon representing this module.
|
||||
# IconUri = ''
|
||||
|
||||
# ReleaseNotes of this module
|
||||
# ReleaseNotes = ''
|
||||
|
||||
# External dependent modules of this module
|
||||
# ExternalModuleDependencies = ''
|
||||
|
||||
} # End of PSData hashtable
|
||||
|
||||
} # End of PrivateData hashtable
|
||||
|
||||
# HelpInfo URI of this module
|
||||
# HelpInfoURI = ''
|
||||
|
||||
# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix.
|
||||
# DefaultCommandPrefix = ''
|
||||
}
|
||||
2
PowerShell/ScubaGear/ScubaGear.psm1
Normal file
2
PowerShell/ScubaGear/ScubaGear.psm1
Normal file
@@ -0,0 +1,2 @@
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath './Modules/Orchestrator.psm1')
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath './Modules/Connection/Connection.psm1')
|
||||
337
README.md
Normal file
337
README.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# ScubaGear M365 Secure Configuration Baseline Assessment Tool
|
||||
Developed by CISA, this assessment tool verifies that an M365 tenant’s configuration conforms to the policies described in the Secure Cloud Business Applications ([SCuBA](https://cisa.gov/scuba)) Minimum Viable Secure Configuration Baseline [documents](https://github.com/cisagov/ScubaGear/tree/main/baselines).
|
||||
|
||||
> **Warning**
|
||||
> This tool is in an alpha state and in active development. At this time, outputs could be incorrect and should be reviewed carefully.
|
||||
|
||||
## M365 Product License Assumptions
|
||||
This tool was tested against tenants that have an M365 E3 or G3 and E5 or G5 license bundle. It may still function for tenants that do not have one of these bundles.
|
||||
|
||||
Some of the policy checks in the baseline rely on the following licenses which are included by default in M365 E5 and G5.
|
||||
- Azure AD Premium Plan 2
|
||||
- Microsoft Defender for Office 365 Plan 1
|
||||
|
||||
If a tenant does not have the licenses listed above, the report will display a non-compliant output for those policies.
|
||||
|
||||
> **Note**: GCC-High/DOD endpoints are included, but have not been tested. Please open an issue if you encounter bugs. GCC-High testing in progress.
|
||||
|
||||
## Installation
|
||||
### Downloading Repository
|
||||
To download ScubaGear:
|
||||
|
||||
1. Click [here](https://github.com/cisagov/ScubaGear/releases/latest) to see the latest release.
|
||||
2. Click `ScubaGear-v0-2-0.zip` (or latest version) to download the release.
|
||||
3. Extract the folder in the zip file.
|
||||
|
||||
### Installing the required PowerShell Modules
|
||||
> **Note**: Only PowerShell 5.1 is currently supported. PowerShell 7 may work, but has not been tested. PowerShell 7 will be added in a future release.
|
||||
|
||||
To import the module, open a new PowerShell 5.1 terminal and navigate to the repository folder.
|
||||
|
||||
Then run:
|
||||
|
||||
```powershell
|
||||
.\Setup.ps1 #Installs the required modules
|
||||
Import-Module -Name .\PowerShell\ScubaGear #Imports the tool into your session
|
||||
```
|
||||
### Download the required OPA executable
|
||||
> **Note**: OPA executable download script is called by default when running SetUp.ps1. OPA.ps1 can also be run by itself to download the executable.
|
||||
In the event of an unsuccessful download, users can manually download the OPA executable with the following steps:
|
||||
1. Go to OPA download site (https://www.openpolicyagent.org/docs/latest/#running-opa)
|
||||
2. Check the acceptable OPA version (Currently v0.42.1) for Scuba and select the corresponding version on top left of the website
|
||||
3. Navigate to the menu on left side of the screen: Introduction - Running OPA - Download OPA
|
||||
4. Locate the downloaded file, add the file to the root directory of this repository, open PowerShell, and use the following command to check the downloaded OPA version
|
||||
```powershell
|
||||
.\opa_windows_amd64.exe version
|
||||
```
|
||||
|
||||
> **Note**
|
||||
> Starting with release 0.3.0, ScubaGear is signed by a commonly trusted CA. Depending on the [PowerShell execution policy](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-5.1) of the system running ScubaGear, different steps may be required before running ScubaGear. See [PowerShell Execution Policies](#powershell-execution-policies) for more details.
|
||||
|
||||
## Usage
|
||||
ScubaGear can be invoked interactively or non-interactively. The interactive authentication mode will prompt the user for credentials via Microsoft's popup windows. Non-interactive mode is for invoking ScubaGear using an Azure AD application service principal and supports running the tool in automated scenarios such as pipelines or scheduled jobs. Examples 1-3 provide examples for running with interactive mode and example 4 provides an example for running in non-interactive mode.
|
||||
|
||||
### Example 1: Run an assessment against all products (except PowerPlatform)
|
||||
```powershell
|
||||
Invoke-SCuBA
|
||||
```
|
||||
### Example 2: Run an assessment against Azure Active Directory with custom report output location
|
||||
```powershell
|
||||
Invoke-SCuBA -ProductNames aad -OutPath C:\Users\johndoe\reports
|
||||
```
|
||||
### Example 3: Run assessments against multiple products
|
||||
```powershell
|
||||
Invoke-SCuBA -ProductNames aad, spo, teams
|
||||
```
|
||||
### Example 4: Run assessments non-interactively using an application service principal and authenticating via CertificateThumbprint
|
||||
```powershell
|
||||
Invoke-SCuBA -ProductNames * -CertificateThumbprint "<insert-thumbprint>" -AppID "<insert-appid>" -Organization tenant.onmicrosoft.com
|
||||
```
|
||||
|
||||
To view more examples and see detailed help run:
|
||||
```powershell
|
||||
Get-Help -Name Invoke-SCuBA -Full
|
||||
```
|
||||
|
||||
### Parameter Definitions
|
||||
|
||||
- **$LogIn** is a `$true` or `$false` variable that if set to `$true` will prompt the user to provide credentials to establish a connection to the specified M365 products in the **$ProductNames** variable. For most use cases, leave this variable to be `$true`. A connection is established in the current PowerShell terminal session with the first authentication. To run another verification in the same PowerShell session, set this variable to be `$false` to bypass the need to authenticate again in the same session. Note: defender will ask for authentication even if this variable is set to `$false`
|
||||
|
||||
- **$ProductNames** is a list of one ore more M365 shortened product names that the tool will assess when it is executed. Acceptable product name values are listed below. To assess Azure Active Directory you would enter the value **aad**. To assess Exchange Online you would enter **exo** and so forth.
|
||||
- Azure Active Directory: **aad**
|
||||
- Defender for Office 365: **defender**
|
||||
- Exchange Online: **exo**
|
||||
- OneDrive: **onedrive**
|
||||
- Power Platform: **powerplatform**
|
||||
- SharePoint Online: **sharepoint**
|
||||
- Teams: **teams**
|
||||
|
||||
- **$M365Environment** parameter is used to authenticate to the various M365 commercial/ government environments. Valid values include `commercial`, `gcc`, `gcchigh`, or `dod`. Default value is `commercial`.
|
||||
- For M365 tenants that are non-government environments enter the value `commercial`.
|
||||
- For M365 Government Commercial Cloud tenants with G3/G5 licenses enter the value `gcc`.
|
||||
- For M365 Government Commercial Cloud High tenants enter the value `gcchigh`.
|
||||
- For M365 Department of Defense tenants enter the value `dod`.
|
||||
|
||||
|
||||
- **$OPAPath** refers to the folder location of the Open Policy Agent (OPA) policy engine executable file. By default the OPA policy engine executable embedded with this project is located in the project's root folder `"./"` and for most cases this value will not need to be modified. To execute the tool using a version of the OPA policy engine located in another folder, customize the variable value with the full path to the folder containing the OPA policy engine executable file.
|
||||
|
||||
- **$OutPath** refers to the folder path where the output JSON and the HTML report will be created. Defaults to the same directory where the script is executed. This parameter is only necessary if an alternate report folder path is desired. The folder will be created if it does not exist.
|
||||
|
||||
### Viewing the Report
|
||||
The HTML report should open in your browser once the script completes. If it does not, navigate to the output folder and open the BaselineReports.html file using your browser. The result files generated from the tool are also saved to the output folder.
|
||||
|
||||
## Required Permissions
|
||||
When executing the tool interactively, there are two types of permissions that are required:
|
||||
- User Permissions (which are associated with Azure AD roles assigned to a user)
|
||||
- Application Permissions (which are assigned to the MS Graph PowerShell application in Azure AD).
|
||||
|
||||
|
||||
### User Permissions
|
||||
The minimum user roles needed for each product are described in the table below.
|
||||
|
||||
[This article](https://learn.microsoft.com/en-us/microsoft-365/admin/add-users/assign-admin-roles?view=o365-worldwide) also explains how to assign admin roles in M365.
|
||||
|
||||
| Product | Role |
|
||||
|-------------------------|:-----------------------------------------------------------------------------------:|
|
||||
| Azure Active Directory | Global Reader |
|
||||
| Teams | Global Reader (or Teams Administrator) |
|
||||
| Exchange Online | Global Reader (or Exchange Administrator) |
|
||||
| Defender for Office 365 | Global Reader (or Exchange Administrator) |
|
||||
| Power Platform | Power Platform Administrator and a "Power Apps for Office 365" license |
|
||||
| Sharepoint Online | SharePoint Administrator |
|
||||
| OneDrive | SharePoint Administrator |
|
||||
|
||||
- **Note**: Users with the Global Administrator role always have the necessary user permissions to run the tool.
|
||||
|
||||
|
||||
### Microsoft Graph Powershell SDK permissions
|
||||
The Azure AD baseline requires the use of Microsoft Graph. The script will attempt to configure the required API permissions needed by the Microsoft Graph PowerShell module, if they have not already been configured in the target tenant.
|
||||
|
||||
The process to configure the application permissions is sometimes referred to as the "application consent process" because an Administrator must "consent" for the Microsoft Graph PowerShell application to access the tenant and the necessary Graph APIs to extract the configuration data. Depending on the Azure AD roles assigned to the user running the tool and how the application consent settings are configured in the target tenant, the process may vary slightly. To understand the application consent process, read [this article](https://learn.microsoft.com/en-us/azure/active-directory/develop/application-consent-experience) from Microsoft.
|
||||
|
||||
Microsoft Graph is used, because Azure AD PowerShell is being deprecated.
|
||||
|
||||
> **Note**
|
||||
> Microsoft Graph PowerShell SDK appears as "unverified" on the AAD application consent screen. This is a [known issue](https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/482).
|
||||
|
||||
The following API permissions are required for Microsoft Graph Powershell:
|
||||
|
||||
- Directory.Read.All
|
||||
- GroupMember.Read.All
|
||||
- Organization.Read.All
|
||||
- Policy.Read.All
|
||||
- RoleManagement.Read.Directory
|
||||
- User.Read.All
|
||||
- UserAuthenticationMethod.Read.All
|
||||
|
||||
|
||||
### Application Service Principal Permissions & Setup
|
||||
Below are the permissions for running the tool non-interactively. The minimum API permissions for all products are listed in the image below. The minimum user role permissions that need to be granted to the application are listed in the *Assign the following Azure AD roles to the service principal* subsection.
|
||||
|
||||
This [video](https://www.youtube.com/watch?v=GyF8HV_35GA) provides a good tutorial for creating an application manually in the Azure Portal. Augment the API permissions and replace the role assignment instructions in the video with the permissions listed below.
|
||||
|
||||
|
||||
**API Permissions**
|
||||

|
||||
|
||||
|
||||
**Power Platform**
|
||||
|
||||
For Power Platform, the application must be [manually registered to Power Platform via interactive authentication](https://learn.microsoft.com/en-us/power-platform/admin/powershell-create-service-principal#registering-an-admin-management-application).
|
||||
```powershell
|
||||
Add-PowerAppsAccount -Endpoint prod -TenantID $tenantId # use -Endpoint usgov for gcc tenants
|
||||
New-PowerAppManagementApp -ApplicationId $appId # Must be run from a Power Platform Adminstrator or Global Adminstrator account
|
||||
```
|
||||
|
||||
|
||||
**Assign the following Azure AD roles to the service principal**
|
||||
- SharePoint Administrator
|
||||
- Global Reader
|
||||
|
||||
|
||||
**Certificate store notes**
|
||||
- Power Platform has a [hardcoded expectation](https://github.com/microsoft/Microsoft365DSC/issues/2781) that the certificate is located in "Cert:\CurrentUser\My".
|
||||
- MS Graph seems to also have an expectation that the certificate at least be located in one of the local client's certificate store(s).
|
||||
|
||||
> **Notes**: Only authentication via `CertificateThumbprint` is currently supported. We will also be supporting automated app registration in a later release.
|
||||
|
||||
|
||||
## Architecture
|
||||

|
||||
The tool employs a three-step process:
|
||||
1. **Extract & Export**. In this step, we utilize the various PowerShell modules authored by Microsoft to export and serialize all the relevant settings into JSON.
|
||||
2. **Test & Record**. Compare the exported settings from the previous step with the configuration prescribed in the baselines. This is done using [OPA Rego](https://www.openpolicyagent.org/docs/latest/policy-language/#what-is-rego), a declarative query language for defining policy. OPA provides a ready-to-use policy engine executable and version v0.41.0 is already included in this repository. The code for the ScubaGear tool was tested against the included version of OPA. To use a later version of the OPA policy engine, follow the instructions listed [here](https://www.openpolicyagent.org/docs/latest/#running-opa) and customize the `$OPAPath` variable described in the Usage section above.
|
||||
3. **Format & Report**. Package the data output by the OPA policy engine into a human-friendly HTML report.
|
||||
|
||||
## Repository Organization
|
||||
- `PowerShell` contains the code used to export the configuration settings from the M365 tenant and orchestrate the entire process from export through evaluation to report. The main PowerShell module manifest `SCuBA.psd1` is located in the PowerShell folder.
|
||||
- `Rego` holds the `.rego` files. Each Rego file audits against the desired state for each product, per the SCuBA M365 secure configuration baseline documents.
|
||||
- `Testing` contains code that is used during the development process to unit test Rego policies.
|
||||
|
||||
## Project License
|
||||
|
||||
Unless otherwise noted, this project is distributed under the Creative Commons Zero license. With developer approval, contributions may be submitted with an alternate compatible license. If accepted, those contributions will be listed herein with the appropriate license.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Executing against multiple tenants
|
||||
ScubaGear creates connections to several M365 services. If running against multiple tenants, it is necessary to disconnect those sessions.
|
||||
|
||||
`Invoke-SCuBA` includes the `-DisconnectOnExit` parameter to disconnect each of connection upon exit. To disconnect sessions after a run, use `Disconnect-SCuBATenant`. The cmdlet disconnects from Azure Active Directory (via MS Graph API), Defender, Exchange Online, OneDrive, Power Platform, SharePoint Online, and Microsoft Teams.
|
||||
|
||||
```PowerShell
|
||||
Disconnect-SCuBATenant
|
||||
```
|
||||
> The cmdlet will attempt to disconnect from all services regardless of current session state. Only connections established within the current PowerShell session will be disconnected and removed. Services that are already disconnected will not generate an error.
|
||||
|
||||
### Errors connecting to Defender
|
||||
If when running the tool against Defender (via ExchangeOnlineManagement PowerShell Module), you may see the connection error "Create Powershell Session is failed using OAuth" in the Powershell window, follow the instructions in this section. An example of the full error message is provided below.
|
||||
|
||||
```
|
||||
WARNING: Please note that you can only use above 9 new EXO cmdlets (the one with *-EXO* naming pattern). You can't use other cmdlets
|
||||
as we couldn't establish a Remote PowerShell session as basic auth is disabled in your client machine. To enable Basic Auth, please
|
||||
check instruction here
|
||||
https://docs.microsoft.com/en-us/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps#prerequisites-for-the-exo-v2-module
|
||||
Create Powershell Session is failed using OAuth
|
||||
```
|
||||
|
||||
If you see this error message it means that basic authentication needs to be enabled on the client computer running the automation scripts. The automation relies on the Microsoft Security & Compliance PowerShell environment for Defender information. Security & Compliance PowerShell connections, unlike other services used by the ExchangeOnlineManagement module, currently [require](https://learn.microsoft.com/en-us/powershell/exchange/exchange-online-powershell-v2?view=exchange-ps#updates-for-version-300-the-exo-v3-module) basic authentication to be enabled on the local machine. Basic authentication is required because the ExchangeOnlineManagement module connects to Security & Compliance PowerShell using Remote PowerShell, which only supports basic authentication. Even in this case, your password is NOT sent to the remote server. When running the tool against M365 products other than Defender, basic authentication need not be enabled on the client computer. Note that these instructions are only about the behavior of the client computer running the tool. In particular, basic authentication should still be disabled using conditional access per the Azure Active Directory baseline instructions.
|
||||
|
||||
Enabling basic authentication instructions are [here](https://docs.microsoft.com/en-us/powershell/exchange/basic-auth-connect-to-exo-powershell?view=exchange-ps).
|
||||
We provide a convenience script named `.\AllowBasicAuthentication.ps1`, in the root project folder, to enable basic authentication. The script must be run from a PowerShell "Run as administrator" window and it updates a registry key. Depending on how your client computer is configured you may have to re-enable basic authentication each time you restart your computer or after it completes a group policy update.
|
||||
|
||||
### Exchange Online maximum connections error
|
||||
If when running the tool against Exchange Online, you see the error below in the Powershell window, follow the instructions in this section.
|
||||
|
||||
```PowerShell
|
||||
New-ExoPSSession : Processing data from remote server outlook.office365.com failed with the
|
||||
following error message: [AuthZRequestId=8feccdea-493c-4c12-85dd-d185232cc0be][FailureCategory=A
|
||||
uthZ-AuthorizationException] Fail to create a runspace because you have exceeded the maximum
|
||||
number of connections allowed : 3
|
||||
```
|
||||
|
||||
If you see the error above run the command below in Powershell:
|
||||
```PowerShell
|
||||
Disconnect-ExchangeOnline
|
||||
```
|
||||
|
||||
or alternatively run `Disconnect-SCuBATenant` exported by the ScubaGear module.
|
||||
```PowerShell
|
||||
Disconnect-SCuBATenant
|
||||
```
|
||||
|
||||
### Power Platform empty policy in report
|
||||
In order for the tool to properly assess the Power Platform product, one of the following conditions must be met:
|
||||
* The tenant includes the `Power Apps for Office 365` license AND the user running the tool has the `Power Platform Administrator` role assigned
|
||||
* The user running the tool has the `Global Administrator` role
|
||||
|
||||
If these conditions are not met, the tool will generate an incorrect report output. The development team is working on a fix to address this bug that will be included in the next release. The screenshot below shows an example of this error for Power Platform policy 2.3. When a user with the required license and role runs the tool, it will produce a correct report.
|
||||
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
### Microsoft Graph Errors
|
||||
|
||||
#### Infinite AAD Signin Loop
|
||||
While running the tool, AAD signin prompts sometimes get stuck in a loop. This is likely an issue with the connection to Microsoft Graph.
|
||||
|
||||
To fix the loop, run:
|
||||
```PowerShell
|
||||
Disconnect-MgGraph
|
||||
```
|
||||
Then run the tool again.
|
||||
|
||||
#### Error `Connect-MgGraph : Key not valid for use in specified state.`
|
||||
|
||||
This is due to a [bug](https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/554) in the Microsoft Authentication Library. The workaround is to delete broken configuration information by running this command (replace `{username}` with your username):
|
||||
|
||||
```
|
||||
rm -r C:\Users\{username}\.graph
|
||||
```
|
||||
After deleting the `.graph` folder in your home directory, re-run the tool and the error should disappear.
|
||||
|
||||
#### Error `Could not load file or assembly 'Microsoft.Graph.Authentication'`
|
||||
|
||||
This indicates that the authentication module is at a version level that conflicts with the MS Graph modules used by the tool. Follow the instructions in the Installation section and execute the Setup script again. This will ensure that the module versions get synchronized with dependencies and then execute the tool again.
|
||||
|
||||
|
||||
### Running the Script Behind Some Proxies
|
||||
If you receive connection or network proxy errors, try running:
|
||||
```powershell
|
||||
$Wcl=New-Object System.Net.WebClient
|
||||
$Wcl.Proxy.Credentials=[System.Net.CredentialCache]::DefaultNetworkCredentials
|
||||
```
|
||||
|
||||
### Utility Scripts
|
||||
The ScubaGear repository includes several utility scripts to help with troubleshooting and recovery from error conditions in the `utils` folder. These helper scripts are designed to assist developers and users when running into errors with the ScubaGear tool or local system environment. See the sections below for details on each script.
|
||||
|
||||
#### ScubaGear Support
|
||||
If a user receives errors and needs additional support diagnosing issues, the `ScubaGearSupport.ps1` script can be run to gather information about their system environment and previous tool output.
|
||||
The script gathers this information into a single ZIP formatted archive to allow for easy sharing with developers or other support staff to assist in troubleshooting. Since the script does gather report output, do keep in mind that the resulting archive may contain details about the associated M365 environment and its settings.
|
||||
|
||||
The script can be run with no arguments and will only collect environment information for troubleshooting. If the `IncludeReports` parameter is provided, it will contain the most recent report from the default `Reports` folder.
|
||||
|
||||
```PowerShell
|
||||
.\ScubaGearSupport.ps1
|
||||
```
|
||||
|
||||
An alternate report path can be specified via the `ReportPath` parameter.
|
||||
|
||||
```PowerShell
|
||||
.\ScubaGearSupport.ps1 -ReportPath C:\ScubaGear\Reports
|
||||
```
|
||||
|
||||
Finally, the script can optionally include all previous reports rather than the most recent one by adding the `AllReports` option.
|
||||
|
||||
```PowerShell
|
||||
.\ScubaGearSupport.ps1 -AllReports
|
||||
```
|
||||
|
||||
Data gathered by the script includes:
|
||||
* Listings of locally installed PowerShell modules and their installation paths
|
||||
* PowerShell versions and environment details
|
||||
* WinRM client service Basic Authentication registry setting
|
||||
* (optional) ScubaGear output from one or more previous invocations which contains
|
||||
* HTML product and summary reports
|
||||
* JSON-formatted M365 product configuration extracts
|
||||
* JSON and CSV-formatted M365 baseline test results
|
||||
|
||||
#### Removing installed modules
|
||||
ScubaGear requires a number of PowerShell modules to function. A user or developer, however, may wish to remove these PowerShell modules for testing or for cleanup after ScubaGear has been run. The `UninstallModules.ps1` script will remove the latest version of the modules required by ScubaGear and installed by the associated `Setup.ps1` script. The script does not take any options and can be as follows:
|
||||
|
||||
```PowerShell
|
||||
.\UninstallModules.ps1
|
||||
```
|
||||
|
||||
>PowerShellGet 2.x has a known issue uninstalling modules installed on a OneDrive path that may result in an "Access to the cloud file is denied" error. Installing PSGet 3.0, currently in beta, will allow the script to successfully uninstall such modules or you can remove the modules files from OneDrive manually.
|
||||
|
||||
### PowerShell Execution Policies
|
||||
|
||||
On Windows Servers, the default [execution policy](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/set-executionpolicy?view=powershell-5.1) is `RemoteSigned`, which will allow ScubaGear to run after the publisher (CISA) is agreed to once.
|
||||
|
||||
On Windows Clients, the default execution policy is `Restricted`. In this case, `Set-ExecutionPolicy RemoteSigned` should be invoked to permit ScubaGear to run.
|
||||
|
||||
In ScubaGear version 0.2.1 and earlier, running `Unblock-File` on the ScubaGear folder may be required. See [here](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/unblock-file?view=powershell-5.1) for more information.
|
||||
1108
Rego/AADConfig.rego
Normal file
1108
Rego/AADConfig.rego
Normal file
File diff suppressed because it is too large
Load Diff
1993
Rego/DefenderConfig.rego
Normal file
1993
Rego/DefenderConfig.rego
Normal file
File diff suppressed because it is too large
Load Diff
832
Rego/EXOConfig.rego
Normal file
832
Rego/EXOConfig.rego
Normal file
@@ -0,0 +1,832 @@
|
||||
package exo
|
||||
import future.keywords
|
||||
|
||||
Format(Array) = format_int(count(Array), 10)
|
||||
|
||||
Description(String1, String2, String3) = trim(concat(" ", [String1, concat(" ", [String2, String3])]), " ")
|
||||
|
||||
ReportDetailsBoolean(Status) = "Requirement met" if {Status == true}
|
||||
|
||||
ReportDetailsBoolean(Status) = "Requirement not met" if {Status == false}
|
||||
|
||||
ReportDetailsArray(Status, Array1, Array2) = Detail if {
|
||||
Status == true
|
||||
Detail := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetailsArray(Status, Array1, Array2) = Detail if {
|
||||
Status == false
|
||||
Fraction := concat(" of ", [Format(Array1), Format(Array2)])
|
||||
String := concat(", ", Array1)
|
||||
Detail := Description(Fraction, "agency domain(s) found in violation:", String)
|
||||
}
|
||||
|
||||
ReportDetailsString(Status, String) = Detail if {
|
||||
Status == true
|
||||
Detail := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetailsString(Status, String) = Detail if {
|
||||
Status == false
|
||||
Detail := String
|
||||
}
|
||||
|
||||
AllDomains := {Domain.domain | Domain = input.spf_records[_]}
|
||||
|
||||
CustomDomains[Domain.domain] {
|
||||
Domain = input.spf_records[_]
|
||||
not endswith( Domain.domain, "onmicrosoft.com")
|
||||
}
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.1 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.1: Policy 1
|
||||
#--
|
||||
RemoteDomainsAllowingForwarding[Domain.DomainName] {
|
||||
Domain := input.remote_domains[_]
|
||||
Domain.AutoForwardEnabled == true
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Automatic forwarding to external domains SHALL be disabled",
|
||||
"Control" : "EXO 2.1",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-RemoteDomain"],
|
||||
"ActualValue" : Domains,
|
||||
"ReportDetails" : ReportDetailsString(Status, ErrorMessage),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Domains := RemoteDomainsAllowingForwarding
|
||||
ErrorMessage := Description(Format(Domains), "remote domain(s) that allows automatic forwarding:", concat(", ", Domains))
|
||||
Status := count(Domains) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.2 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test for X because of Y
|
||||
tests[{
|
||||
"Requirement" : "A list of approved IP addresses for sending mail SHALL be maintained",
|
||||
"Control" : "EXO 2.2",
|
||||
"Criticality" : "Shall/Not-Implemented",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Currently cannot be checked automatically. See Exchange Online Secure Configuration Baseline policy 2.# for instructions on manual check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 2
|
||||
#--
|
||||
DomainsWithoutSpf[DNSResponse.domain] {
|
||||
DNSResponse := input.spf_records[_]
|
||||
SpfRecords := {Record | Record = DNSResponse.rdata[_]; startswith(Record, "v=spf1 ")}
|
||||
count(SpfRecords) == 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "An SPF policy(s) that designates only these addresses as approved senders SHALL be published",
|
||||
"Control" : "EXO 2.2",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-ScubaSpfRecords", "Get-AcceptedDomain"],
|
||||
"ActualValue" : Domains,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Domains, AllDomains),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Domains := DomainsWithoutSpf
|
||||
Status := count(Domains) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.3 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.3: Policy 1
|
||||
#--
|
||||
DomainsWithDkim[DkimConfig.Domain] {
|
||||
DkimConfig := input.dkim_config[_]
|
||||
DkimConfig.Enabled == true
|
||||
DkimRecord := input.dkim_records[_]
|
||||
DkimRecord.domain == DkimConfig.Domain
|
||||
ValidAnswers := [Answer | Answer := DkimRecord.rdata[_]; startswith(Answer, "v=DKIM1;")]
|
||||
count(ValidAnswers) > 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "DKIM SHOULD be enabled for any custom domain",
|
||||
"Control" : "EXO 2.3",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-DkimSigningConfig", "Get-ScubaDkimRecords", "Get-AcceptedDomain"],
|
||||
"ActualValue" : [input.dkim_records, input.dkim_config],
|
||||
"ReportDetails" : ReportDetailsArray(Status, DomainsWithoutDkim, CustomDomains),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
DomainsWithoutDkim := CustomDomains - DomainsWithDkim
|
||||
Status := count(DomainsWithoutDkim) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.4 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.4: Policy 1
|
||||
#--
|
||||
DomainsWithoutDmarc[DmarcRecord.domain] {
|
||||
DmarcRecord := input.dmarc_records[_]
|
||||
ValidAnswers := [Answer | Answer := DmarcRecord.rdata[_]; startswith(Answer, "v=DMARC1;")]
|
||||
count(ValidAnswers) == 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "A DMARC policy SHALL be published for every second-level domain",
|
||||
"Control" : "EXO 2.4",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-ScubaDmarcRecords", "Get-AcceptedDomain"],
|
||||
"ActualValue" : input.dmarc_records,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Domains, AllDomains),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Domains := DomainsWithoutDmarc
|
||||
Status := count(Domains) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.4: Policy 2
|
||||
#--
|
||||
DomainsWithoutPreject[DmarcRecord.domain] {
|
||||
DmarcRecord := input.dmarc_records[_]
|
||||
ValidAnswers := [Answer | Answer := DmarcRecord.rdata[_]; contains(Answer, "p=reject;")]
|
||||
count(ValidAnswers) == 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "The DMARC message rejection option SHALL be \"p=reject\"",
|
||||
"Control" : "EXO 2.4",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-ScubaDmarcRecords", "Get-AcceptedDomain"],
|
||||
"ActualValue" : input.dmarc_records,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Domains, AllDomains),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Domains := DomainsWithoutPreject
|
||||
Status := count(Domains) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.4: Policy 3
|
||||
#--
|
||||
DomainsWithoutDHSContact[DmarcRecord.domain] {
|
||||
DmarcRecord := input.dmarc_records[_]
|
||||
ValidAnswers := [Answer | Answer := DmarcRecord.rdata[_]; contains(Answer, "mailto:reports@dmarc.cyber.dhs.gov")]
|
||||
count(ValidAnswers) == 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "The DMARC point of contact for aggregate reports SHALL include reports@dmarc.cyber.dhs.gov",
|
||||
"Control" : "EXO 2.4",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-ScubaDmarcRecords", "Get-AcceptedDomain"],
|
||||
"ActualValue" : input.dmarc_records,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Domains, AllDomains),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Domains := DomainsWithoutDHSContact
|
||||
Status := count(Domains) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.4: Policy 4
|
||||
#--
|
||||
DomainsWithoutAgencyContact[DmarcRecord.domain] {
|
||||
DmarcRecord := input.dmarc_records[_]
|
||||
EnoughContacts := [Answer | Answer := DmarcRecord.rdata[_]; count(split(Answer, "@")) >= 3]
|
||||
count(EnoughContacts) == 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "An agency point of contact SHOULD be included for aggregate and/or failure reports",
|
||||
"Control" : "EXO 2.4",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-ScubaDmarcRecords", "Get-AcceptedDomain"],
|
||||
"ActualValue" : input.dmarc_records,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Domains, AllDomains),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Domains := DomainsWithoutAgencyContact
|
||||
Status := count(Domains) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.5 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.5: Policy 1
|
||||
#--
|
||||
|
||||
SmtpClientAuthEnabled[TransportConfig.Name] {
|
||||
TransportConfig := input.transport_config[_]
|
||||
TransportConfig.SmtpClientAuthenticationDisabled == false
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "SMTP AUTH SHALL be disabled in Exchange Online",
|
||||
"Control" : "EXO 2.5",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-TransportConfig"],
|
||||
"ActualValue" : input.transport_config,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Status := count(SmtpClientAuthEnabled) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.6 #
|
||||
################
|
||||
|
||||
# Are both the tests supposed to be the same?
|
||||
|
||||
#
|
||||
# Baseline 2.6: Policy 1
|
||||
#--
|
||||
|
||||
SharingPolicyAllowedSharing[SharingPolicy.Name] {
|
||||
SharingPolicy := input.sharing_policy[_]
|
||||
InList := "*" in SharingPolicy.Domains
|
||||
InList == true
|
||||
}
|
||||
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Contact folders SHALL NOT be shared with all domains, although they MAY be shared with specific domains",
|
||||
"Control" : "EXO 2.6",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-SharingPolicy"],
|
||||
"ActualValue" : input.sharing_policy,
|
||||
"ReportDetails" : ReportDetailsString(Status, ErrorMessage),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
ErrorMessage := "Wildcard domain (\"*\") in shared domains list, enabling sharing with all domains by default"
|
||||
|
||||
Status := count(SharingPolicyAllowedSharing) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.6: Policy 2
|
||||
#--
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Calendar details SHALL NOT be shared with all domains, although they MAY be shared with specific domains",
|
||||
"Control" : "EXO 2.6",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-SharingPolicy"],
|
||||
"ActualValue" : input.sharing_policy,
|
||||
"ReportDetails" : ReportDetailsString(Status, ErrorMessage),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
ErrorMessage := "Wildcard domain (\"*\") in shared domains list, enabling sharing with all domains by default"
|
||||
Status := count(SharingPolicyAllowedSharing) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.7 #
|
||||
################
|
||||
#
|
||||
# Baseline 2.7: Policy 1
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "External sender warnings SHALL be implemented",
|
||||
"Control" : "EXO 2.7",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-TransportRule"],
|
||||
"ActualValue" : [Rule.FromScope | Rule = Rules[_]],
|
||||
"ReportDetails" : ReportDetailsString(Status, ErrorMessage),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Rules := input.transport_rule
|
||||
ErrorMessage := "No transport rule found that applies warnings to emails received from outside the organization"
|
||||
EnabledRules := [rule | rule = Rules[_]; rule.State == "Enabled"; rule.Mode == "Enforce"]
|
||||
Conditions := [IsCorrectScope | IsCorrectScope = EnabledRules[_].FromScope == "NotInOrganization"]
|
||||
Status := count([Condition | Condition = Conditions[_]; Condition == true]) > 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.8 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.8: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "A DLP solution SHALL be used. The selected DLP solution SHOULD offer services comparable to the native DLP solution offered by Microsoft",
|
||||
"Control" : "EXO 2.8",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.8: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "The DLP solution SHALL protect PII and sensitive information, as defined by the agency. At a minimum, the sharing of credit card numbers, Taxpayer Identification Numbers (TIN), and Social Security Numbers (SSN) via email SHALL be restricted",
|
||||
"Control" : "EXO 2.8",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.9 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.9: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Emails SHALL be filtered by the file types of included attachments. The selected filtering solution SHOULD offer services comparable to Microsoft Defender's Common Attachment Filter",
|
||||
"Control" : "EXO 2.9",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.9: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "The attachment filter SHOULD attempt to determine the true file type and assess the file extension",
|
||||
"Control" : "EXO 2.9",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.9: Policy 3
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Disallowed file types SHALL be determined and set. At a minimum, click-to-run files SHOULD be blocked (e.g., .exe, .cmd, and .vbe)",
|
||||
"Control" : "EXO 2.9",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.10 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.10: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Emails SHALL be scanned for malware",
|
||||
"Control" : "EXO 2.10",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.10: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Emails identified as containing malware SHALL be quarantined or dropped",
|
||||
"Control" : "EXO 2.10",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.10: Policy 3
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Email scanning SHOULD be capable of reviewing emails after delivery",
|
||||
"Control" : "EXO 2.10",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.11 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.11: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Impersonation protection checks SHOULD be used",
|
||||
"Control" : "EXO 2.11",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.11: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "User warnings, comparable to the user safety tips included with EOP, SHOULD be displayed",
|
||||
"Control" : "EXO 2.11",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.11: Policy 3
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "The phishing protection solution SHOULD include an AI-based phishing detection tool comparable to EOP Mailbox Intelligence",
|
||||
"Control" : "EXO 2.11",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.12 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.12: Policy 1
|
||||
#--
|
||||
|
||||
ConnFiltersWithIPAllowList[ConnFilter.Name] {
|
||||
ConnFilter := input.conn_filter[_]
|
||||
count(ConnFilter.IPAllowList) > 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "IP allow lists SHOULD NOT be created",
|
||||
"Control" : "EXO 2.12",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-HostedConnectionFilterPolicy"],
|
||||
"ActualValue" : input.conn_filter,
|
||||
"ReportDetails" : ReportDetailsString(Status, ErrorMessage),
|
||||
"RequirementMet" : Status
|
||||
}]{
|
||||
ErrorMessage := "Allow-list is in use"
|
||||
Status := count(ConnFiltersWithIPAllowList) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.12: Policy 2
|
||||
#--
|
||||
|
||||
ConnFiltersWithSafeList[ConnFilter.Name] {
|
||||
ConnFilter := input.conn_filter[_]
|
||||
ConnFilter.EnableSafeList == true
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Safe lists SHOULD NOT be enabled",
|
||||
"Control" : "EXO 2.12",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-HostedConnectionFilterPolicy"],
|
||||
"ActualValue" : input.conn_filter,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}]{
|
||||
Status := count(ConnFiltersWithSafeList) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.13 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.13: Policy 1
|
||||
#--
|
||||
AuditEnabled[OrgConfig.Name] {
|
||||
OrgConfig := input.org_config[_]
|
||||
OrgConfig.AuditDisabled == true
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Mailbox auditing SHALL be enabled",
|
||||
"Control" : "EXO 2.13",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-OrganizationConfig"],
|
||||
"ActualValue" : input.org_config,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Status := count(AuditEnabled) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.14 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.14: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "A spam filter SHALL be enabled. The filtering solution selected SHOULD offer services comparable to the native spam filtering offered by Microsoft",
|
||||
"Control" : "EXO 2.14",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.14: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Spam and high confidence spam SHALL be moved to either the junk email folder or the quarantine folder",
|
||||
"Control" : "EXO 2.14",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.14: Policy 3
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Allowed senders MAY be added, but allowed domains SHALL NOT be added",
|
||||
"Control" : "EXO 2.14",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.15 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.15: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "URL comparison with a block-list SHOULD be enabled",
|
||||
"Control" : "EXO 2.15",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.15: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Direct download links SHOULD be scanned for malware",
|
||||
"Control" : "EXO 2.15",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.15: Policy 3
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "User click tracking SHOULD be enabled",
|
||||
"Control" : "EXO 2.15",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.16 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.16: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "At a minimum, the following alerts SHALL be enabled...[see Exchange Online secure baseline for list]",
|
||||
"Control" : "EXO 2.16",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.16: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "The alerts SHOULD be sent to a monitored address or incorporated into a SIEM",
|
||||
"Control" : "EXO 2.16",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.17 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.17: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Unified audit logging SHALL be enabled",
|
||||
"Control" : "EXO 2.17",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.17: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Advanced audit SHALL be enabled",
|
||||
"Control" : "EXO 2.17",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.17: Policy 3
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31",
|
||||
"Control" : "EXO 2.17",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
273
Rego/OneDriveConfig.rego
Normal file
273
Rego/OneDriveConfig.rego
Normal file
@@ -0,0 +1,273 @@
|
||||
package onedrive
|
||||
import future.keywords
|
||||
|
||||
ReportDetailsBoolean(Status) = "Requirement met" if {Status == true}
|
||||
|
||||
ReportDetailsBoolean(Status) = "Requirement not met" if {Status == false}
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.1 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.1: Policy 1
|
||||
#--
|
||||
AnyoneLinksPolicy[Policy]{
|
||||
Policy := input.SPO_tenant_info[_]
|
||||
Policy.OneDriveSharingCapability != 2
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Anyone links SHOULD be disabled",
|
||||
"Control" : "OneDrive 2.1",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-SPOTenant", "Get-PnPTenant"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
input.OneDrive_PnP_Flag == false
|
||||
Policies := AnyoneLinksPolicy
|
||||
Status := count(Policies) == 1
|
||||
}
|
||||
#--
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Anyone links SHOULD be disabled",
|
||||
"Control" : "OneDrive 2.1",
|
||||
"Criticality" : "Should/Not-Implemented",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Currently cannot be checked automatically while using Service Principals. See Onedrive Secure Configuration Baseline policy 2.1 for instructions on manual check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
input.OneDrive_PnP_Flag
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.2 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 1
|
||||
#--
|
||||
ReportDetails2_2(Policy) = Description if {
|
||||
Policy.OneDriveSharingCapability != 2
|
||||
Description := "Requirement met: Anyone links are disabled"
|
||||
}
|
||||
|
||||
ReportDetails2_2(Policy) = Description if {
|
||||
Policy.OneDriveSharingCapability == 2
|
||||
Policy.RequireAnonymousLinksExpireInDays == 30
|
||||
Description := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetails2_2(Policy) = Description if {
|
||||
Policy.OneDriveSharingCapability == 2
|
||||
Policy.RequireAnonymousLinksExpireInDays != 30
|
||||
Description := "Requirement not met: Expiration date is not 30"
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "An expiration date SHOULD be set for Anyone links",
|
||||
"Control" : "OneDrive 2.2",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-SPOTenant", "Get-PnPTenant"],
|
||||
"ActualValue" : [Policy.OneDriveSharingCapability, Policy.RequireAnonymousLinksExpireInDays],
|
||||
"ReportDetails" : ReportDetails2_2(Policy),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policy := input.SPO_tenant_info[_]
|
||||
Conditions1 := [Policy.OneDriveSharingCapability !=2]
|
||||
Case1 := count([Condition | Condition = Conditions1[_]; Condition == false]) == 0
|
||||
Conditions2 := [Policy.OneDriveSharingCapability == 2, Policy.RequireAnonymousLinksExpireInDays == 30]
|
||||
Case2 := count([Condition | Condition = Conditions2[_]; Condition == false]) == 0
|
||||
Conditions := [Case1, Case2]
|
||||
Status := count([Condition | Condition = Conditions[_]; Condition == true]) > 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "An expiration date SHOULD be set for Anyone links",
|
||||
"Control" : "OneDrive 2.2",
|
||||
"Criticality" : "Should/Not-Implemented",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Currently cannot be checked automatically while using Service Principals. See Onedrive Secure Configuration Baseline policy 2.2 for instructions on manual check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
input.OneDrive_PnP_Flag
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.3 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.3: Policy 1
|
||||
#--
|
||||
ReportDetails2_3(Policy) = Description if {
|
||||
Policy.OneDriveSharingCapability != 2
|
||||
Description := "Requirement met: Anyone links are disabled"
|
||||
}
|
||||
|
||||
ReportDetails2_3(Policy) = Description if {
|
||||
Policy.OneDriveSharingCapability == 2
|
||||
Policy.FileAnonymousLinkType == 1
|
||||
Policy.FolderAnonymousLinkType == 1
|
||||
Description := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetails2_3(Policy) = Description if {
|
||||
Policy.OneDriveSharingCapability == 2
|
||||
Policy.FileAnonymousLinkType == 2
|
||||
Policy.FolderAnonymousLinkType == 2
|
||||
Description := "Requirement not met: both files and folders are not limited to view for Anyone"
|
||||
}
|
||||
|
||||
ReportDetails2_3(Policy) = Description if {
|
||||
Policy.OneDriveSharingCapability == 2
|
||||
Policy.FileAnonymousLinkType == 1
|
||||
Policy.FolderAnonymousLinkType == 2
|
||||
Description := "Requirement not met: folders are not limited to view for Anyone"
|
||||
}
|
||||
|
||||
ReportDetails2_3(Policy) = Description if {
|
||||
Policy.OneDriveSharingCapability == 2
|
||||
Policy.FileAnonymousLinkType == 2
|
||||
Policy.FolderAnonymousLinkType == 1
|
||||
Description := "Requirement not met: files are not limited to view for Anyone"
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Anyone link permissions SHOULD be limited to View",
|
||||
"Control" : "OneDrive 2.3",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-SPOTenant", "Get-PnPTenant"],
|
||||
"ActualValue" : [Policy.OneDriveSharingCapability, Policy.FileAnonymousLinkType, Policy.FolderAnonymousLinkType],
|
||||
"ReportDetails" : ReportDetails2_3(Policy),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policy := input.SPO_tenant_info[_]
|
||||
Conditions1 := [Policy.OneDriveSharingCapability !=2]
|
||||
Case1 := count([Condition | Condition = Conditions1[_]; Condition == false]) == 0
|
||||
Conditions2 := [Policy.OneDriveSharingCapability == 2, Policy.FileAnonymousLinkType == 1, Policy.FolderAnonymousLinkType == 1]
|
||||
Case2 := count([Condition | Condition = Conditions2[_]; Condition == false]) == 0
|
||||
Conditions := [Case1, Case2]
|
||||
Status := count([Condition | Condition = Conditions[_]; Condition == true]) > 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Anyone link permissions SHOULD be limited to View",
|
||||
"Control" : "OneDrive 2.3",
|
||||
"Criticality" : "Should/Not-Implemented",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Currently cannot be checked automatically while using Service Principals. See Onedrive Secure Configuration Baseline policy 2.3 for instructions on manual check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
input.OneDrive_PnP_Flag
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.4 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.4: Policy 1
|
||||
#--
|
||||
DefinedDomainsPolicy[Policy]{
|
||||
Policy := input.Tenant_sync_info[_]
|
||||
count(Policy.AllowedDomainList) > 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "OneDrive Client for Windows SHALL be restricted to agency-Defined Domain(s)",
|
||||
"Control" : "OneDrive 2.4",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-SPOTenant", "Get-PnPTenant"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := DefinedDomainsPolicy
|
||||
Status := count(Policies) == 1
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.5 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.5: Policy 1
|
||||
#--
|
||||
ClientSyncPolicy[Policy]{
|
||||
Policy := input.Tenant_sync_info[_]
|
||||
Policy.BlockMacSync == false
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "OneDrive Client Sync SHALL only be allowed only within the local domain",
|
||||
"Control" : "OneDrive 2.5",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-SPOTenantSyncClientRestriction", "Get-PnPTenantSyncClientRestriction"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := ClientSyncPolicy
|
||||
Status := count(Policies) == 1
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.6 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.6: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test for X because of Y
|
||||
tests[{
|
||||
"Requirement" : "OneDrive Client Sync SHALL be restricted to the local domain",
|
||||
"Control" : "OneDrive 2.6",
|
||||
"Criticality" : "Shall/Not-Implemented",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Currently cannot be checked automatically. See Onedrive Secure Configuration Baseline policy 2.6 for instructions on manual check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.7 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.7: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test for X because of Y
|
||||
tests[{
|
||||
"Requirement" : "Legacy Authentication SHALL be blocked",
|
||||
"Control" : "OneDrive 2.7",
|
||||
"Criticality" : "Shall/Not-Implemented",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Currently cannot be checked automatically. See Onedrive Secure Configuration Baseline policy 2.7 for instructions on manual check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
380
Rego/PowerPlatformConfig.rego
Normal file
380
Rego/PowerPlatformConfig.rego
Normal file
@@ -0,0 +1,380 @@
|
||||
package powerplatform
|
||||
import future.keywords
|
||||
|
||||
|
||||
Format(Array) = format_int(count(Array), 10)
|
||||
|
||||
Description(String1, String2, String3) = trim(concat(" ", [String1, concat(" ", [String2, String3])]), " ")
|
||||
|
||||
ReportDetailsBoolean(Status) = "Requirement met" if {Status == true}
|
||||
|
||||
ReportDetailsBoolean(Status) = "Requirement not met" if {Status == false}
|
||||
|
||||
ReportDetailsArray(Status, Array, String1) = Detail if {
|
||||
Status == true
|
||||
Detail := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetailsArray(Status, Array, String1) = Detail if {
|
||||
Status == false
|
||||
String2 := concat(", ", Array)
|
||||
Detail := Description(Format(Array), String1, String2)
|
||||
}
|
||||
ReportDetailsString(Status, String) = Detail if {
|
||||
Status == true
|
||||
Detail := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetailsString(Status, String) = Detail if {
|
||||
Status == false
|
||||
Detail := String
|
||||
}
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.1 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.1: Policy 1
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "The ability to create production and sandbox environments SHALL be restricted to admins",
|
||||
"Control" : "Power Platform 2.1",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-TenantSettings"],
|
||||
"ActualValue" : EnvironmentCreation.disableEnvironmentCreationByNonAdminUsers,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
EnvironmentCreation := input.environment_creation[_]
|
||||
Status := EnvironmentCreation.disableEnvironmentCreationByNonAdminUsers == true
|
||||
}
|
||||
#--
|
||||
|
||||
# Baseline 2.1: Policy 1 PoSh Error
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "The ability to create production and sandbox environments SHALL be restricted to admins",
|
||||
"Control" : "Power Platform 2.1",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-TenantSettings"],
|
||||
"ActualValue" : "PowerShell Error",
|
||||
"ReportDetails" : "PowerShell Error",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
count(input.environment_creation) <= 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.1: Policy 2
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "The ability to create trial environments SHALL be restricted to admins",
|
||||
"Control" : "Power Platform 2.1",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-TenantSettings"],
|
||||
"ActualValue" : EnvironmentCreation.disableTrialEnvironmentCreationByNonAdminUsers,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
EnvironmentCreation := input.environment_creation[_]
|
||||
Status := EnvironmentCreation.disableTrialEnvironmentCreationByNonAdminUsers == true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.1: Policy 2 PoSh Error
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "The ability to create trial environments SHALL be restricted to admins",
|
||||
"Control" : "Power Platform 2.1",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-TenantSettings"],
|
||||
"ActualValue" : "PowerShell Error",
|
||||
"ReportDetails" : "PowerShell Error",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
count(input.environment_creation) <= 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.2 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 1
|
||||
#--
|
||||
DefaultEnvPolicies[{"PolicyName" : Policy.displayName}]{
|
||||
TenantId := input.tenant_id
|
||||
DlpPolicies := input.dlp_policies[_]
|
||||
Policy := DlpPolicies.value[_]
|
||||
Env := Policy.environments[_]
|
||||
Env.name == concat("-", ["Default", TenantId])
|
||||
}
|
||||
|
||||
# Note: there is only one default environment per tenant and it cannot be deleted or backed up
|
||||
tests[{
|
||||
"Requirement" : "A DLP policy SHALL be created to restrict connector access in the default Power Platform environment",
|
||||
"Control" : "Power Platform 2.2",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-DlpPolicy"],
|
||||
"ActualValue" : DefaultEnvPolicies,
|
||||
"ReportDetails" : ReportDetailsString(Status, ErrorMessage),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
ErrorMessage := "No policy found that applies to default environment"
|
||||
Status := count(DefaultEnvPolicies) > 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 2
|
||||
#--
|
||||
# gets the list of all tenant environments
|
||||
AllEnvironments [{ "EnvName" : EnvName }] {
|
||||
EnvironmentList := input.environment_list[_]
|
||||
EnvName := EnvironmentList.EnvironmentName
|
||||
}
|
||||
|
||||
# gets the list of all environments with policies applied to them
|
||||
EnvWithPolicies [{"EnvName" : PolicyEnvName }] {
|
||||
DlpPolicies := input.dlp_policies[_]
|
||||
Policy := DlpPolicies.value[_]
|
||||
Env := Policy.environments[_]
|
||||
PolicyEnvName := Env.name
|
||||
}
|
||||
|
||||
# finds the environments with no policies applied to them
|
||||
EnvWithoutPolicies [Env] {
|
||||
AllEnvSet := {Env.EnvName | Env = AllEnvironments[_]}
|
||||
PolicyEnvSet := {Env.EnvName | Env = EnvWithPolicies[_]}
|
||||
Difference := AllEnvSet - PolicyEnvSet
|
||||
Env := Difference[_]
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Non-default environments SHOULD have at least one DLP policy that affects them",
|
||||
"Control" : "Power Platform 2.2",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-DlpPolicy"],
|
||||
"ActualValue" : EnvWithoutPolicies,
|
||||
"ReportDetails" : ReportDetailsArray(Status, EnvWithoutPolicies, ErrorMessage),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
DLPPolicies = input.dlp_policies[_]
|
||||
count(DLPPolicies.value) > 0
|
||||
ErrorMessage := "Subsequent environments without DLP policies:"
|
||||
Status := count(EnvWithoutPolicies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 2 No DLP Policies found
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "Non-default environments SHOULD have at least one DLP policy that affects them",
|
||||
"Control" : "Power Platform 2.2",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-DlpPolicy"],
|
||||
"ActualValue" : "No DLP Policies found",
|
||||
"ReportDetails" : "No DLP Policies found",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
DLPPolicies = input.dlp_policies[_]
|
||||
count(DLPPolicies.value) <= 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 2 PoSh Error
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "Non-default environments SHOULD have at least one DLP policy that affects them",
|
||||
"Control" : "Power Platform 2.2",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-DlpPolicy"],
|
||||
"ActualValue" : "PowerShell Error",
|
||||
"ReportDetails" : "PowerShell Error",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
count(input.dlp_policies) <= 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 3
|
||||
#--
|
||||
# gets the set of connectors that are allowed in the default environment
|
||||
# general and confidential groups refer to business and non-business
|
||||
ConnectorSet[Connector.id] {
|
||||
TenantId := input.tenant_id
|
||||
DlpPolicies := input.dlp_policies[_]
|
||||
Policy := DlpPolicies.value[_]
|
||||
Env := Policy.environments[_]
|
||||
Group := Policy.connectorGroups[_]
|
||||
Connector := Group.connectors[_]
|
||||
Conditions := [Group.classification == "General", Group.classification == "Confidential"]
|
||||
# Filter: only include policies that meet all the requirements
|
||||
Env.name == concat("-", ["Default", TenantId])
|
||||
count([Condition | Condition = Conditions[_]; Condition == true]) > 0
|
||||
}
|
||||
|
||||
# set of all connectors that cannot be blocked
|
||||
AllowedInBaseline := {
|
||||
"/providers/Microsoft.PowerApps/apis/shared_powervirtualagents",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_sharepointonline",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_onedriveforbusiness",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_approvals",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_cloudappsecurity",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_commondataservice",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_commondataserviceforapps",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_excelonlinebusiness",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_flowpush",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_kaizala",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_microsoftformspro",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_office365",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_office365groups",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_office365groupsmail",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_office365users",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_onenote",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_planner",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_powerappsnotification",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_powerappsnotificationv2",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_powerbi",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_shifts",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_skypeforbiz",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_teams",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_todo",
|
||||
"/providers/Microsoft.PowerApps/apis/shared_yammer"
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "All connectors except those listed...[see Power Platform secure configuration baseline for list]...SHOULD be added to the Blocked category in the default environment policy",
|
||||
"Control" : "Power Platform 2.2",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-DlpPolicy"],
|
||||
"ActualValue" : RogueConnectors,
|
||||
"ReportDetails" : ReportDetailsArray(Status, RogueConnectors, ErrorMessage),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
DLPPolicies = input.dlp_policies[_]
|
||||
count(DLPPolicies.value) > 0
|
||||
ErrorMessage := "Connectors are allowed that should be blocked:"
|
||||
RogueConnectors := (ConnectorSet - AllowedInBaseline)
|
||||
Status := count(RogueConnectors) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 3 Error No DLP policies Found
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "All connectors except those listed...[see Power Platform secure configuration baseline for list]...SHOULD be added to the Blocked category in the default environment policy",
|
||||
"Control" : "Power Platform 2.2",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-DlpPolicy"],
|
||||
"ActualValue" : "No DLP Policies found",
|
||||
"ReportDetails" : "No DLP Policies found",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
DLPPolicies = input.dlp_policies[_]
|
||||
count(DLPPolicies.value) <= 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 3 PoSh Error
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "All connectors except those listed...[see Power Platform secure configuration baseline for list]...SHOULD be added to the Blocked category in the default environment policy",
|
||||
"Control" : "Power Platform 2.2",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-DlpPolicy"],
|
||||
"ActualValue" : "PowerShell error",
|
||||
"ReportDetails" : "PowerShell error",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
count(input.dlp_policies) <= 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.3 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.3: Policy 1
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "Power Platform tenant isolation SHALL be enabled",
|
||||
"Control" : "Power Platform 2.3",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-PowerAppTenantIsolationPolicy"],
|
||||
"ActualValue" : TenantIsolation.properties.isDisabled,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
TenantIsolation := input.tenant_isolation[_]
|
||||
Status := TenantIsolation.properties.isDisabled == false
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.3: Policy 1 PoSh Error
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "Power Platform tenant isolation SHALL be enabled",
|
||||
"Control" : "Power Platform 2.3",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-PowerAppTenantIsolationPolicy"],
|
||||
"ActualValue" : "PowerShell Error",
|
||||
"ReportDetails" : "PowerShell Error",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
count(input.tenant_isolation) <= 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.3: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test for X because of Y
|
||||
tests[{
|
||||
"Requirement" : "An inbound/outbound connection allowlist SHOULD be configured",
|
||||
"Control" : "Power Platform 2.3",
|
||||
"Criticality" : "Should/Not-Implemented",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Currently cannot be checked automatically. See Power Platform Secure Configuration Baseline policy 2.3 for instructions on manual check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.4 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.4: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test for X because of Y
|
||||
tests[{
|
||||
"Requirement" : "Content security policies for model-driven Power Apps SHALL be enabled",
|
||||
"Control" : "Power Platform 2.4",
|
||||
"Criticality" : "Shall/Not-Implemented",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Currently cannot be checked automatically. See Power Platform Secure Configuration Baseline policy 2.4 for instructions on manual check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
265
Rego/SharepointConfig.rego
Normal file
265
Rego/SharepointConfig.rego
Normal file
@@ -0,0 +1,265 @@
|
||||
package sharepoint
|
||||
import future.keywords
|
||||
|
||||
ReportDetailsBoolean(Status) = "Requirement met" if {Status == true}
|
||||
|
||||
ReportDetailsBoolean(Status) = "Requirement not met" if {Status == false}
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.1 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.1: Policy 1
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "File and folder links default sharing setting SHALL be set to \"Specific People (Only the People the User Specifies)\"",
|
||||
"Control" : "Sharepoint 2.1",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-SPOTenant", "Get-PnPTenant"],
|
||||
"ActualValue" : Policy.DefaultSharingLinkType,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policy := input.SPO_tenant[_]
|
||||
Status := Policy.DefaultSharingLinkType == 1
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.2 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 1
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "External sharing SHOULD be limited to approved domains and security groups per interagency collaboration needs",
|
||||
"Control" : "Sharepoint 2.2",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-SPOTenant", "Get-PnPTenant"],
|
||||
"ActualValue" : Policy.SharingCapability,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policy := input.SPO_tenant[_]
|
||||
Status := Policy.SharingCapability != 2
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 2
|
||||
#--
|
||||
#tests[{
|
||||
# "Requirement" : "External sharing SHOULD be limited to approved domains and security groups per interagency collaboration needs",
|
||||
# "Control" : "Sharepoint 2.2",
|
||||
# "Criticality" : "Should",
|
||||
# "Commandlet" : ["Get-SPOTenant", "Get-PnPTenant"],
|
||||
# "ActualValue" : Policy.SharingDomainRestrictionMode,
|
||||
# "ReportDetails" : ReportDetailsBoolean(Status),
|
||||
# "RequirementMet" : Status
|
||||
#}] {
|
||||
# Policy := input.SPO_tenant[_]
|
||||
# Status := Policy.SharingDomainRestrictionMode == 1
|
||||
#}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 3
|
||||
#--
|
||||
#tests[{
|
||||
# "Requirement" : "External sharing SHOULD be limited to approved domains and security groups per interagency collaboration needs",
|
||||
# "Control" : "Sharepoint 2.2",
|
||||
# "Criticality" : "Should",
|
||||
# "Commandlet" : ["Get-SPOTenant", "Get-PnPTenant"],
|
||||
# "ActualValue" : [Policy.SharingCapability, Policy.SharingDomainRestrictionMode],
|
||||
# "ReportDetails" : ReportDetails2_2(Policy),
|
||||
# "RequirementMet" : Status
|
||||
#}] {
|
||||
# Policy := input.SPO_tenant[_]
|
||||
# TODO: Missing Allow only users in specific security groups to share externally
|
||||
#}
|
||||
#--
|
||||
|
||||
################
|
||||
# Baseline 2.3 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.3: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test for X because of Y
|
||||
tests[{
|
||||
"Requirement" : "Sharing settings for specific SharePoint sites SHOULD align to their sensitivity level",
|
||||
"Control" : "Sharepoint 2.3",
|
||||
"Criticality" : "Should/Not-Implemented",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Currently cannot be checked automatically. See Sharepoint Secure Configuration Baseline policy 2.3 for instructions on manual check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.4 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.4: Policy 1
|
||||
#--
|
||||
ReportDetails2_4_1(Policy) = Description if {
|
||||
Policy.SharingCapability == 0
|
||||
Description := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetails2_4_1(Policy) = Description if {
|
||||
Policy.SharingCapability != 0
|
||||
Policy.ExternalUserExpirationRequired == true
|
||||
Policy.ExternalUserExpireInDays == 30
|
||||
Description := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetails2_4_1(Policy) = Description if {
|
||||
Policy.SharingCapability != 0
|
||||
Policy.ExternalUserExpirationRequired == false
|
||||
Policy.ExternalUserExpireInDays == 30
|
||||
Description := "Requirement not met: Expiration timer for 'Guest access to a site or OneDrive' NOT enabled"
|
||||
}
|
||||
|
||||
ReportDetails2_4_1(Policy) = Description if {
|
||||
Policy.SharingCapability != 0
|
||||
Policy.ExternalUserExpirationRequired == true
|
||||
Policy.ExternalUserExpireInDays != 30
|
||||
Description := "Requirement not met: Expiration timer for 'Guest access to a site or OneDrive' NOT set to 30 days"
|
||||
}
|
||||
|
||||
ReportDetails2_4_1(Policy) = Description if {
|
||||
Policy.SharingCapability != 0
|
||||
Policy.ExternalUserExpirationRequired == false
|
||||
Policy.ExternalUserExpireInDays != 30
|
||||
Description := "Requirement not met"
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Expiration timer for 'Guest access to a site or OneDrive' should be set to 30 days",
|
||||
"Control" : "Sharepoint 2.4",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-SPOTenant", "Get-PnPTenant"],
|
||||
"ActualValue" : [Policy.SharingCapability, Policy.ExternalUserExpirationRequired, Policy.ExternalUserExpireInDays],
|
||||
"ReportDetails" : ReportDetails2_4_1(Policy),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policy := input.SPO_tenant[_]
|
||||
|
||||
# Role policy requires assignment expiration, but maximum duration is 30 days
|
||||
Conditions1 := [Policy.ExternalUserExpirationRequired == true, Policy.ExternalUserExpireInDays == 30]
|
||||
Case := count([Condition | Condition = Conditions1[_]; Condition == false]) == 0
|
||||
|
||||
# Filter: only include rules that meet one of the two cases
|
||||
Conditions2 := [Policy.SharingCapability == 0, Case]
|
||||
Status := count([Condition | Condition = Conditions2[_]; Condition == true]) > 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.4: Policy 2
|
||||
#--
|
||||
ReportDetails2_4_2(Policy) = Description if {
|
||||
Policy.SharingCapability == 0
|
||||
Description := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetails2_4_2(Policy) = Description if {
|
||||
Policy.SharingCapability != 0
|
||||
Policy.EmailAttestationRequired == true
|
||||
Policy.EmailAttestationReAuthDays == 30
|
||||
Description := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetails2_4_2(Policy) = Description if {
|
||||
Policy.SharingCapability != 0
|
||||
Policy.EmailAttestationRequired == false
|
||||
Policy.EmailAttestationReAuthDays == 30
|
||||
Description := "Requirement not met: Expiration timer for 'People who use a verification code' NOT enabled"
|
||||
}
|
||||
|
||||
ReportDetails2_4_2(Policy) = Description if {
|
||||
Policy.SharingCapability != 0
|
||||
Policy.EmailAttestationRequired == true
|
||||
Policy.EmailAttestationReAuthDays != 30
|
||||
Description := "Requirement not met: Expiration timer for 'People who use a verification code' NOT set to 30 days"
|
||||
}
|
||||
|
||||
ReportDetails2_4_2(Policy) = Description if {
|
||||
Policy.SharingCapability != 0
|
||||
Policy.EmailAttestationRequired == false
|
||||
Policy.EmailAttestationReAuthDays != 30
|
||||
Description := "Requirement not met"
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Expiration timer for 'People who use a verification code' should be set to 30 days",
|
||||
"Control" : "Sharepoint 2.4",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-SPOTenant", "Get-PnPTenant"],
|
||||
"ActualValue" : [Policy.SharingCapability, Policy.EmailAttestationRequired, Policy.EmailAttestationReAuthDays],
|
||||
"ReportDetails" : ReportDetails2_4_2(Policy),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policy := input.SPO_tenant[_]
|
||||
|
||||
# Role policy requires assignment expiration, but maximum duration is 30 days
|
||||
Conditions1 := [Policy.EmailAttestationRequired == true, Policy.EmailAttestationReAuthDays == 30]
|
||||
Case := count([Condition | Condition = Conditions1[_]; Condition == false]) == 0
|
||||
|
||||
# Filter: only include rules that meet one of the two cases
|
||||
Conditions2 := [Policy.SharingCapability == 0, Case]
|
||||
Status := count([Condition | Condition = Conditions2[_]; Condition == true]) > 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.5 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.5: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test for X because of Y
|
||||
tests[{
|
||||
"Requirement" : "Users SHALL be prevented from running custom scripts on personal sites (OneDrive)",
|
||||
"Control" : "Sharepoint 2.5",
|
||||
"Criticality" : "Shall/Not-Implemented",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Currently cannot be checked automatically. See Sharepoint Secure Configuration Baseline policy 2.5 for instructions on manual check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.5: Policy 2
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "Users SHALL be prevented from running custom scripts on self-service created sites",
|
||||
"Control" : "Sharepoint 2.5",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-SPOSite", "Get-PnPTenantSite"],
|
||||
"ActualValue" : Policy.DenyAddAndCustomizePages,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policy := input.SPO_site[_]
|
||||
# 1 == Allow users to run custom script on self-service created sites
|
||||
# 2 == Prevent users from running custom script on self-service created sites
|
||||
Status := Policy.DenyAddAndCustomizePages == 2
|
||||
}
|
||||
#--
|
||||
721
Rego/TeamsConfig.rego
Normal file
721
Rego/TeamsConfig.rego
Normal file
@@ -0,0 +1,721 @@
|
||||
package teams
|
||||
import future.keywords
|
||||
|
||||
Format(Array) = format_int(count(Array), 10)
|
||||
|
||||
Description(String1, String2, String3) = trim(concat(" ", [String1, concat(" ", [String2, String3])]), " ")
|
||||
|
||||
ReportDetailsBoolean(Status) = "Requirement met" if {Status == true}
|
||||
|
||||
ReportDetailsBoolean(Status) = "Requirement not met" if {Status == false}
|
||||
|
||||
ReportDetailsArray(Status, Array, String1) = Detail if {
|
||||
Status == true
|
||||
Detail := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetailsArray(Status, Array, String1) = Detail if {
|
||||
Status == false
|
||||
String2 := concat(", ", Array)
|
||||
Detail := Description(Format(Array), String1, String2)
|
||||
}
|
||||
|
||||
ReportDetailsString(Status, String) = Detail if {
|
||||
Status == true
|
||||
Detail := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetailsString(Status, String) = Detail if {
|
||||
Status == false
|
||||
Detail := String
|
||||
}
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.1 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.1: Policy 1
|
||||
#--
|
||||
# The english translation of the following is:
|
||||
# Iterate through all meeting policies. For each, check if AllowExternalParticipantGiveRequestControl
|
||||
# is true. If so, save the policy Identity to the "meetings_allowing_control" list.
|
||||
MeetingsAllowingExternalControl[Policy.Identity] {
|
||||
Policy := input.meeting_policies[_]
|
||||
Policy.AllowExternalParticipantGiveRequestControl == true
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "External participants SHOULD NOT be enabled to request control of shared desktops or windows in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist",
|
||||
"Control" : "Teams 2.1",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingPolicy"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Policies, String),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := MeetingsAllowingExternalControl
|
||||
String := "meeting policy(ies) found that allows external control:"
|
||||
Status := count(Policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.2 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.2: Policy 1
|
||||
#--
|
||||
MeetingsAllowingAnonStart[Policy.Identity] {
|
||||
Policy := input.meeting_policies[_]
|
||||
Policy.AllowAnonymousUsersToStartMeeting == true
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Anonymous users SHALL NOT be enabled to start meetings in the Global (Org-wide default) meeting policy or in custom meeting policies if any exist",
|
||||
"Control" : "Teams 2.2",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingPolicy"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Policies, String),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := MeetingsAllowingAnonStart
|
||||
String := "meeting policy(ies) found that allows anonymous users to start meetings:"
|
||||
Status := count(Policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.3 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.3: Policy 1
|
||||
#--
|
||||
ReportDetails2_3(Policy) = Description if {
|
||||
Policy.AutoAdmittedUsers != "Everyone"
|
||||
Policy.AllowPSTNUsersToBypassLobby == false
|
||||
Description := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetails2_3(Policy) = Description if {
|
||||
Policy.AutoAdmittedUsers != "Everyone"
|
||||
Policy.AllowPSTNUsersToBypassLobby == true
|
||||
Description := "Requirement not met: Dial-in users are enabled to bypass the lobby"
|
||||
}
|
||||
|
||||
ReportDetails2_3(Policy) = Description if {
|
||||
Policy.AutoAdmittedUsers == "Everyone"
|
||||
Description := "Requirement not met: All users are admitted automatically"
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically",
|
||||
"Control" : "Teams 2.3",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingPolicy"],
|
||||
"ActualValue" : [Policy.AutoAdmittedUsers, Policy.AllowPSTNUsersToBypassLobby],
|
||||
"ReportDetails" : ReportDetails2_3(Policy),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policy := input.meeting_policies[_]
|
||||
# This control specifically states that non-global policies MAY be different, so filter for the global policy
|
||||
Policy.Identity = "Global"
|
||||
Conditions := [Policy.AutoAdmittedUsers != "Everyone", Policy.AllowPSTNUsersToBypassLobby == false]
|
||||
Status := count([Condition | Condition = Conditions[_]; Condition == false]) == 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Anonymous users, including dial-in users, SHOULD NOT be admitted automatically",
|
||||
"Control" : "Teams 2.3",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingPolicy"],
|
||||
"ActualValue" : "PowerShell Error",
|
||||
"ReportDetails" : "PowerShell Error",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
count(input.meeting_policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.3: Policy 2
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "Internal users SHOULD be admitted automatically",
|
||||
"Control" : "Teams 2.3",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingPolicy"],
|
||||
"ActualValue" : Policy.AutoAdmittedUsers,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policy := input.meeting_policies[_]
|
||||
# This control specifically states that non-global policies MAY be different, so filter for the global policy
|
||||
Policy.Identity = "Global"
|
||||
Status := Policy.AutoAdmittedUsers in ["EveryoneInCompany", "EveryoneInSameAndFederatedCompany", "EveryoneInCompanyExcludingGuests"]
|
||||
}
|
||||
|
||||
#
|
||||
# Baseline 2.3: Policy 2
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "Internal users SHOULD be admitted automatically",
|
||||
"Control" : "Teams 2.3",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingPolicy"],
|
||||
"ActualValue" : "PowerShell Error",
|
||||
"ReportDetails" : "PowerShell Error",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
count(input.meeting_policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.4 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.4: Policy 1
|
||||
#--
|
||||
ExternalAccessConfig[Policy.Identity] {
|
||||
Policy := input.federation_configuration[_]
|
||||
# Filter: only include policies that meet all the requirements
|
||||
Policy.AllowFederatedUsers == true
|
||||
count(Policy.AllowedDomains) == 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "External access SHALL only be enabled on a per-domain basis",
|
||||
"Control" : "Teams 2.4",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-CsTenantFederationConfiguration"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Policies, String),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := ExternalAccessConfig
|
||||
String := "meeting policy(ies) that allow external access across all domains:"
|
||||
Status := count(Policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.4: Policy 2
|
||||
#--
|
||||
MeetingsNotAllowingAnonJoin[Policy.Identity] {
|
||||
Policy := input.meeting_policies[_]
|
||||
Policy.AllowAnonymousUsersToJoinMeeting == false
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Anonymous users SHOULD be enabled to join meetings",
|
||||
"Control" : "Teams 2.4",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingPolicy"],
|
||||
"ActualValue" : MeetingsNotAllowingAnonJoin,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Policies, String),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := MeetingsNotAllowingAnonJoin
|
||||
String := "meeting policy(ies) found that don't allow anonymous users to join meetings:"
|
||||
Status := count(Policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.5 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.5: Policy 1
|
||||
#--
|
||||
# There are two relevant settings:
|
||||
# - AllowTeamsConsumer: Is contact to or from unmanaged users allowed at all?
|
||||
# - AllowTeamsConsumerInbound: Are unamanged users able to initiate contact?
|
||||
# If AllowTeamsConsumer is false, unmanaged users will be unable to initiate
|
||||
# contact regardless of what AllowTeamsConsumerInbound is set to as contact
|
||||
# is completely disabled. However, unfortunately setting AllowTeamsConsumer
|
||||
# to false doesn't automatically set AllowTeamsConsumerInbound to false as
|
||||
# well, and in the GUI the checkbox for AllowTeamsConsumerInbound completely
|
||||
# disappears when AllowTeamsConsumer is set to false, basically preserving
|
||||
# on the backend whatever value was there to begin with.
|
||||
#
|
||||
# TLDR: This requirement can be met if:
|
||||
# - AllowTeamsConsumer is false regardless of the value for AllowTeamsConsumerInbound OR
|
||||
# - AllowTeamsConsumerInbound is false
|
||||
# Basically, both cannot be true.
|
||||
|
||||
FederationConfiguration[Policy.Identity] {
|
||||
Policy := input.federation_configuration[_]
|
||||
# Filter: only include policies that meet all the requirements
|
||||
Policy.AllowTeamsConsumerInbound == true
|
||||
Policy.AllowTeamsConsumer == true
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Unmanaged users SHALL NOT be enabled to initiate contact with internal users",
|
||||
"Control" : "Teams 2.5",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-CsTenantFederationConfiguration"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Policies, String),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := FederationConfiguration
|
||||
String := "Configuration allowed unmanaged users to initiate contact with internal user across domains:"
|
||||
Status := count(Policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.5: Policy 2
|
||||
#--
|
||||
InternalCannotenable[Policy.Identity] {
|
||||
Policy := input.federation_configuration[_]
|
||||
Policy.AllowTeamsConsumer == true
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Internal users SHOULD NOT be enabled to initiate contact with unmanaged users",
|
||||
"Control" : "Teams 2.5",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTenantFederationConfiguration"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Policies, String),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := InternalCannotenable
|
||||
String := "Internal users are enabled to initiate contact with unmanaged users across domains:"
|
||||
Status := count(Policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.6 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.6: Policy 1
|
||||
#--
|
||||
SkpyeBlocConfig[Policy.Identity] {
|
||||
Policy := input.federation_configuration[_]
|
||||
Policy.AllowPublicUsers == true
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Contact with Skype users SHALL be blocked",
|
||||
"Control" : "Teams 2.6",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-CsTenantFederationConfiguration"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Policies, String),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := SkpyeBlocConfig
|
||||
String := "domains that allows contact with Skype users:"
|
||||
Status := count(Policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.7 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.7: Policy 1
|
||||
#--
|
||||
ConfigsAllowingEmail[Policy.Identity] {
|
||||
Policy := input.client_configuration[_]
|
||||
Policy.AllowEmailIntoChannel == true
|
||||
}
|
||||
|
||||
ReportDetails2_7(IsGCC, IsEnabled) = Description if {
|
||||
IsGCC == true
|
||||
Description := "N/A: Feature is unavailable in GCC environments"
|
||||
}
|
||||
|
||||
ReportDetails2_7(IsGCC, IsEnabled) = Description if {
|
||||
IsGCC == false
|
||||
IsEnabled == true
|
||||
Description := "Requirement met"
|
||||
}
|
||||
|
||||
ReportDetails2_7(IsGCC, IsEnabled) = Description if {
|
||||
IsGCC == false
|
||||
IsEnabled == false
|
||||
Description := "Requirement not met"
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Teams email integration SHALL be disabled",
|
||||
"Control" : "Teams 2.7",
|
||||
"Criticality" : "Shall",
|
||||
"Commandlet" : ["Get-CsTeamsClientConfiguration", "Get-CsTenant"],
|
||||
"ActualValue" : {"ClientConfig": input.client_configuration, "AssignedPlans": AssignedPlans},
|
||||
"ReportDetails" : ReportDetails2_7(IsGCC, IsEnabled),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
# According to Get-CsTeamsClientConfiguration, is team email integration enabled?
|
||||
IsEnabled := count(ConfigsAllowingEmail) == 0
|
||||
# What is the tenant type according to Get-CsTenant?
|
||||
TenantConfig := input.teams_tenant_info[_]
|
||||
AssignedPlans := concat(", ", TenantConfig.AssignedPlan)
|
||||
GCCConditions := [contains(AssignedPlans, "GCC"), contains(AssignedPlans, "DOD")]
|
||||
IsGCC := count([Condition | Condition = GCCConditions[_]; Condition == true]) > 0
|
||||
# As long as either:
|
||||
# 1) Get-CsTeamsClientConfiguration reports email integration is disabled or
|
||||
# 2) Get-CsTenant reports this as a gov tenant
|
||||
# this test should pass.
|
||||
Conditions := [IsEnabled, IsGCC]
|
||||
Status := count([Condition | Condition = Conditions[_]; Condition == true]) > 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.8 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.8: Policy 1
|
||||
#--
|
||||
PoliciesBlockingDefaultApps[Policy.Identity] {
|
||||
Policy := input.app_policies[_]
|
||||
Policy.DefaultCatalogAppsType != "BlockedAppList"
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Agencies SHOULD allow all apps published by Microsoft, but MAY block specific Microsoft apps as needed",
|
||||
"Control" : "Teams 2.8",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsAppPermissionPolicy"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Policies, String),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := PoliciesBlockingDefaultApps
|
||||
String := "meeting policy(ies) found that block Microsoft Apps by default:"
|
||||
Status = count(Policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.8: Policy 2
|
||||
#--
|
||||
PoliciesAllowingGlobalApps[Policy.Identity] {
|
||||
Policy := input.app_policies[_]
|
||||
Policy.GlobalCatalogAppsType != "AllowedAppList"
|
||||
}
|
||||
|
||||
PoliciesAllowingCustomApps[Policy.Identity] {
|
||||
Policy := input.app_policies[_]
|
||||
Policy.PrivateCatalogAppsType != "AllowedAppList"
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Agencies SHOULD NOT allow installation of all third-party apps, but MAY allow specific apps as needed",
|
||||
"Control" : "Teams 2.8",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsAppPermissionPolicy"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Policies, String),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := PoliciesAllowingGlobalApps
|
||||
String := "meeting policy(ies) found that allow third-party apps by default:"
|
||||
Status = count(Policies) == 0
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "Agencies SHOULD NOT allow installation of all custom apps, but MAY allow specific apps as needed",
|
||||
"Control" : "Teams 2.8",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsAppPermissionPolicy"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Policies, String),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := PoliciesAllowingCustomApps
|
||||
String := "meeting policy(ies) found that allow custom apps by default:"
|
||||
Status = count(Policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.8: Policy 3
|
||||
#--
|
||||
# At this time we are unable to test for X because of Y
|
||||
tests[{
|
||||
"Requirement" : "Agencies SHALL establish policy dictating the app review and approval process to be used by the agency",
|
||||
"Control" : "Teams 2.8",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Cannot be checked automatically. See Microsoft Teams Secure Configuration Baseline policy 2.8 for instructions on manual check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
################
|
||||
# Baseline 2.9 #
|
||||
################
|
||||
|
||||
#
|
||||
# Baseline 2.9: Policy 1
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "Cloud video recording SHOULD be disabled in the global (org-wide default) meeting policy",
|
||||
"Control" : "Teams 2.9",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingPolicy"],
|
||||
"ActualValue" : Policy.AllowCloudRecording,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policy := input.meeting_policies[_]
|
||||
Policy.Identity == "Global" # Filter: this control only applies to the Global policy
|
||||
Status := Policy.AllowCloudRecording == false
|
||||
}
|
||||
|
||||
#
|
||||
# Baseline 2.9: Policy 1
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "Cloud video recording SHOULD be disabled in the global (org-wide default) meeting policy",
|
||||
"Control" : "Teams 2.9",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingPolicy"],
|
||||
"ActualValue" : "PowerShell Error",
|
||||
"ReportDetails" : "PowerShell Error",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
count(input.meeting_policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.9: Policy 2
|
||||
#--
|
||||
PoliciesAllowingOutsideRegionStorage[Policy.Identity] {
|
||||
Policy := input.meeting_policies[_]
|
||||
Policy.AllowCloudRecording == true
|
||||
Policy.AllowRecordingStorageOutsideRegion == true
|
||||
}
|
||||
|
||||
tests[{
|
||||
"Requirement" : "For all meeting polices that allow cloud recording, recordings SHOULD be stored inside the country of that agency's tenant",
|
||||
"Control" : "Teams 2.9",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingPolicy"],
|
||||
"ActualValue" : Policies,
|
||||
"ReportDetails" : ReportDetailsArray(Status, Policies, String),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policies := PoliciesAllowingOutsideRegionStorage
|
||||
String := "meeting policy(ies) found that allow cloud recording and storage outside of the tenant's region:"
|
||||
Status := count(Policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.10 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.10: Policy 1
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "Record an event SHOULD be set to Organizer can record",
|
||||
"Control" : "Teams 2.10",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingBroadcastPolicy"],
|
||||
"ActualValue" : Policy.BroadcastRecordingMode,
|
||||
"ReportDetails" : ReportDetailsBoolean(Status),
|
||||
"RequirementMet" : Status
|
||||
}] {
|
||||
Policy := input.broadcast_policies[_]
|
||||
Policy.Identity == "Global" # Filter: this control only applies to the Global policy
|
||||
Status := Policy.BroadcastRecordingMode == "UserOverride"
|
||||
}
|
||||
|
||||
#
|
||||
# Baseline 2.10: Policy 1
|
||||
#--
|
||||
tests[{
|
||||
"Requirement" : "Record an event SHOULD be set to Organizer can record",
|
||||
"Control" : "Teams 2.10",
|
||||
"Criticality" : "Should",
|
||||
"Commandlet" : ["Get-CsTeamsMeetingBroadcastPolicy"],
|
||||
"ActualValue" : "PowerShell Error",
|
||||
"ReportDetails" : "PowerShell Error",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
count(input.broadcast_policies) == 0
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.11 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.11: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "A DLP solution SHALL be enabled",
|
||||
"Control" : "Teams 2.11",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.11: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Agencies SHOULD use either the native DLP solution offered by Microsoft or a DLP solution that offers comparable services",
|
||||
"Control" : "Teams 2.11",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.11: Policy 3
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "The DLP solution SHALL protect Personally Identifiable Information (PII) and sensitive information, as defined by the agency. At a minimum, the sharing of credit card numbers, taxpayer Identification Numbers (TIN), and Social Security Numbers (SSN) via email SHALL be restricted",
|
||||
"Control" : "Teams 2.11",
|
||||
"Criticality" : "Shall/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.12 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.12: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Attachments included with Teams messages SHOULD be scanned for malware",
|
||||
"Control" : "Teams 2.12",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.12: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Users SHOULD be prevented from opening or downloading files detected as malware",
|
||||
"Control" : "Teams 2.12",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
|
||||
#################
|
||||
# Baseline 2.13 #
|
||||
#################
|
||||
|
||||
#
|
||||
# Baseline 2.13: Policy 1
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "URL comparison with a block-list SHOULD be enabled",
|
||||
"Control" : "Teams 2.13",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.13: Policy 2
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "Direct download links SHOULD be scanned for malware",
|
||||
"Control" : "Teams 2.13",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
|
||||
#
|
||||
# Baseline 2.13: Policy 3
|
||||
#--
|
||||
# At this time we are unable to test because settings are configured in M365 Defender or using a third-party app
|
||||
tests[{
|
||||
"Requirement" : "User click tracking SHOULD be enabled",
|
||||
"Control" : "Teams 2.13",
|
||||
"Criticality" : "Should/3rd Party",
|
||||
"Commandlet" : [],
|
||||
"ActualValue" : [],
|
||||
"ReportDetails" : "Custom implementation allowed. If you are using Defender to fulfill this requirement, run the Defender version of this script. Otherwise, use a 3rd party tool OR manually check",
|
||||
"RequirementMet" : false
|
||||
}] {
|
||||
true
|
||||
}
|
||||
#--
|
||||
136
SetUp.ps1
Normal file
136
SetUp.ps1
Normal file
@@ -0,0 +1,136 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
This script installs the required Powershell modules used by the
|
||||
assessment tool
|
||||
.DESCRIPTION
|
||||
Installs the modules required to support SCuBAGear. If the Force
|
||||
switch is set then any existing module will be re-installed even if
|
||||
already at latest version. If the SkipUpdate switch is set then any
|
||||
existing module will not be updated to th latest version.
|
||||
.EXAMPLE
|
||||
.\Setup.ps1
|
||||
.NOTES
|
||||
Executing the script with no switches set will install the latest
|
||||
version of a module if not already installed.
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $false, HelpMessage = 'Installs a given module and overrides warning messages about module installation conflicts. If a module with the same name already exists on the computer, Force allows for multiple versions to be installed. If there is an existing module with the same name and version, Force overwrites that version')]
|
||||
[switch]
|
||||
$Force,
|
||||
|
||||
[Parameter(HelpMessage = 'If specified then modules will not be updated to latest version')]
|
||||
[switch]
|
||||
$SkipUpdate,
|
||||
|
||||
[Parameter(HelpMessage = 'Do not automatically trust the PSGallery repository for module installation')]
|
||||
[switch]
|
||||
$DoNotAutoTrustRepository,
|
||||
|
||||
[Parameter(HelpMessage = 'Do not download OPA')]
|
||||
[switch]
|
||||
$NoOPA
|
||||
)
|
||||
|
||||
# Set preferences for writing messages
|
||||
$DebugPreference = "Continue"
|
||||
$InformationPreference = "Continue"
|
||||
|
||||
if (-not $DoNotAutoTrustRepository) {
|
||||
$Policy = Get-PSRepository -Name "PSGallery" | Select-Object -Property -InstallationPolicy
|
||||
|
||||
if ($($Policy.InstallationPolicy) -ne "Trusted") {
|
||||
Set-PSRepository -Name "PSGallery" -InstallationPolicy Trusted
|
||||
Write-Information -MessageData "Setting PSGallery repository to trusted."
|
||||
}
|
||||
}
|
||||
|
||||
# Start a stopwatch to time module installation elapsed time
|
||||
$Stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
|
||||
$RequiredModulesPath = Join-Path -Path $PSScriptRoot -ChildPath "PowerShell\ScubaGear\RequiredVersions.ps1"
|
||||
if (Test-Path -Path $RequiredModulesPath) {
|
||||
. $RequiredModulesPath
|
||||
}
|
||||
|
||||
if ($ModuleList) {
|
||||
# Add PowerShellGet to beginning of ModuleList for installing required modules.
|
||||
$ModuleList = ,@{
|
||||
ModuleName = 'PowerShellGet'
|
||||
ModuleVersion = [version] '2.1.0'
|
||||
MaximumVersion = [version] '2.99.99999'
|
||||
} + $ModuleList
|
||||
}
|
||||
else
|
||||
{
|
||||
throw "Required modules list is required."
|
||||
}
|
||||
|
||||
foreach ($Module in $ModuleList) {
|
||||
|
||||
$ModuleName = $Module.ModuleName
|
||||
|
||||
if (Get-Module -ListAvailable -Name $ModuleName) {
|
||||
$HighestInstalledVersion = (Get-Module -ListAvailable -Name $ModuleName | Sort-Object Version -Descending | Select-Object Version -First 1).Version
|
||||
$LatestVersion = (Find-Module -Name $ModuleName).Version
|
||||
|
||||
if ($HighestInstalledVersion -ge $LatestVersion) {
|
||||
Write-Debug "${ModuleName}:${HighestInstalledVersion} already has latest installed."
|
||||
|
||||
if ($Force -eq $true) {
|
||||
Install-Module -Name $ModuleName `
|
||||
-Force `
|
||||
-AllowClobber `
|
||||
-Scope CurrentUser `
|
||||
-MaximumVersion $Module.MaximumVersion
|
||||
Write-Information -MessageData "Re-installing module to latest acceptable version: ${ModuleName}"
|
||||
}
|
||||
}
|
||||
else {
|
||||
if ($SkipUpdate -eq $true) {
|
||||
Write-Debug "Skipping update for ${ModuleName}:${HighestInstalledVersion} to newer version ${LatestVersion}."
|
||||
}
|
||||
else {
|
||||
Install-Module -Name $ModuleName `
|
||||
-Force `
|
||||
-AllowClobber `
|
||||
-Scope CurrentUser `
|
||||
-MaximumVersion $Module.MaximumVersion
|
||||
$MaxInstalledVersion = (Get-Module -ListAvailable -Name $ModuleName | Sort-Object Version -Descending | Select-Object Version -First 1).Version
|
||||
Write-Information -MessageData " ${ModuleName}:${HighestInstalledVersion} updated to version ${MaxInstalledVersion}."
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
Install-Module -Name $ModuleName `
|
||||
-AllowClobber `
|
||||
-Scope CurrentUser `
|
||||
-MaximumVersion $Module.MaximumVersion
|
||||
$MaxInstalledVersion = (Get-Module -ListAvailable -Name $ModuleName | Sort-Object Version -Descending | Select-Object Version -First 1).Version
|
||||
Write-Information -MessageData "Installed the latest acceptable version of ${ModuleName} version ${MaxInstalledVersion}"
|
||||
}
|
||||
}
|
||||
|
||||
if ($NoOPA -eq $true) {
|
||||
Write-Debug "Skipping Download for OPA."
|
||||
}
|
||||
else {
|
||||
$DebugPreference = 'Continue'
|
||||
try {
|
||||
$ScriptDir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
|
||||
. $ScriptDir\OPA.ps1
|
||||
}
|
||||
catch {
|
||||
Write-Error "An error occurred: cannot call OPA download script"
|
||||
}
|
||||
}
|
||||
|
||||
# Stop the clock and report total elapsed time
|
||||
$Stopwatch.stop()
|
||||
|
||||
Write-Debug "ScubaGear setup time elapsed: $([math]::Round($stopwatch.Elapsed.TotalSeconds,0)) seconds."
|
||||
|
||||
$DebugPreference = "SilentlyContinue"
|
||||
$InformationPreference = "SilentlyContinue"
|
||||
1956
Testing/Functional/Auto/ExtremeTest.txt
Normal file
1956
Testing/Functional/Auto/ExtremeTest.txt
Normal file
File diff suppressed because it is too large
Load Diff
41
Testing/Functional/Auto/MinimumTest.txt
Normal file
41
Testing/Functional/Auto/MinimumTest.txt
Normal file
@@ -0,0 +1,41 @@
|
||||
aad
|
||||
defender
|
||||
exo
|
||||
onedrive
|
||||
sharepoint
|
||||
teams
|
||||
aad, defender
|
||||
aad, exo
|
||||
aad, onedrive
|
||||
aad, sharepoint
|
||||
aad, teams
|
||||
defender, exo
|
||||
defender, onedrive
|
||||
defender, sharepoint
|
||||
defender, teams
|
||||
exo, onedrive
|
||||
exo, sharepoint
|
||||
exo, teams
|
||||
onedrive, sharepoint
|
||||
onedrive, teams
|
||||
sharepoint, teams
|
||||
aad, defender, exo
|
||||
aad, defender, onedrive
|
||||
aad, defender, sharepoint
|
||||
aad, defender, teams
|
||||
defender, exo, onedrive
|
||||
defender, exo, sharepoint
|
||||
defender, exo, teams
|
||||
exo, onedrive, sharepoint
|
||||
exo, onedrive, teams
|
||||
onedrive, sharepoint, teams
|
||||
aad, defender, exo, onedrive
|
||||
aad, defender, exo, sharepoint
|
||||
aad, defender, exo, teams
|
||||
defender, exo, onedrive, sharepoint
|
||||
defender, exo, onedrive, teams
|
||||
exo, onedrive, sharepoint, teams
|
||||
aad, defender, exo, onedrive, sharepoint
|
||||
aad, defender, exo, onedrive, teams
|
||||
defender, exo, onedrive, sharepoint, teams
|
||||
aad, defender, exo, onedrive, sharepoint, teams
|
||||
6
Testing/Functional/Auto/SimpleTest.txt
Normal file
6
Testing/Functional/Auto/SimpleTest.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
aad
|
||||
defender
|
||||
exo
|
||||
onedrive
|
||||
sharepoint
|
||||
teams
|
||||
63
Testing/Functional/RegoCachedProviderTesting.ps1
Normal file
63
Testing/Functional/RegoCachedProviderTesting.ps1
Normal file
@@ -0,0 +1,63 @@
|
||||
#
|
||||
# For Rego testing with a static provider JSON.
|
||||
# When pure Rego testing it makes sense to export the provider only once.
|
||||
#
|
||||
# DO NOT confuse this script with the Rego Unit tests script
|
||||
|
||||
#
|
||||
# The tenant name in the report will display Rego Testing which IS intentional.
|
||||
# This is so that this test script can be run on any cached provider JSON
|
||||
#
|
||||
|
||||
# Set $true for the first run of this script
|
||||
# then set this to be $false each subsequent run
|
||||
param (
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive", '*', IgnoreCase = $false)]
|
||||
[string[]]
|
||||
$ProductNames = '*', # The specific products that you want the tool to assess.
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$OutPath = ".\Testing\Functional\Reports", # output directory
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet($true, $false)]
|
||||
[boolean]
|
||||
$LogIn = $false, # Set $true to authenticate yourself to a tenant or if you are already authenticated set to $false to avoid reauthentication
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet($true, $false)]
|
||||
[boolean]
|
||||
$ExportProvider = $true,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet($true, $false)]
|
||||
[boolean]
|
||||
$Quiet = $True # Supress report poping up after run
|
||||
)
|
||||
|
||||
$M365Environment = "gcc"
|
||||
$OPAPath = "./" # Path to OPA Executable
|
||||
|
||||
$RunCachedParams = @{
|
||||
'ExportProvider' = $ExportProvider;
|
||||
'Login' = $Login;
|
||||
'ProductNames' = $ProductNames;
|
||||
'M365Environment' = $M365Environment;
|
||||
'OPAPath' = $OPAPath;
|
||||
'OutPath' = $OutPath;
|
||||
'Quiet' = $Quiet;
|
||||
}
|
||||
|
||||
Set-Location $(Split-Path -Path $PSScriptRoot | Split-Path)
|
||||
$ManifestPath = Join-Path -Path "./PowerShell" -ChildPath "ScubaGear"
|
||||
Remove-Module "ScubaGear" -ErrorAction "SilentlyContinue" # For dev work
|
||||
#######
|
||||
Import-Module $ManifestPath -ErrorAction Stop
|
||||
Invoke-RunCached @RunCachedParams
|
||||
42
Testing/Functional/SmokeTest/Sample-SmokeTestAutomation.json
Normal file
42
Testing/Functional/SmokeTest/Sample-SmokeTestAutomation.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"_comment": "All comments should be removed. Add a test tenant object to execute the smoke test against it.",
|
||||
"TestTenants": [
|
||||
{
|
||||
"_comment": "The test tenant needs to provide the following info to support the smoke test.",
|
||||
"TestTenant01": {
|
||||
"_comment1": "DomainName maps to Organization parameter of Invoke-SCuBA",
|
||||
"DomainName": "sample001.onmicrosoft.com",
|
||||
"_comment2": "DisplayName is the Tenant Display Name as expected in the baseline report.",
|
||||
"DisplayName": "Sample 001",
|
||||
"_comment3": "A base 64 encoded PFX certiificate for a service principal.",
|
||||
"CertificateB64": "-----BEGIN CERTIFICATE-----\r\nMIIK...A=\r\n-----END CERTIFICATE-----\r\n\r\n",
|
||||
"_comment4": "AppId maps to AppId parameter of Invoke-SCuBA. The application ID of the application registration",
|
||||
"AppId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"_comment5": "M365Enc maps to -M365Environment parameter of Invoke-SCuBA ",
|
||||
"M365Env": "commercial",
|
||||
"_comment6": "The password for the provided certificate, CertificateB64.",
|
||||
"CertificatePassword": "sample01secret"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TestTenant002": {
|
||||
"DomainName": "sample002.onmicrosoft.com",
|
||||
"DisplayName": "Sample 002",
|
||||
"CertificateB64": "-----BEGIN CERTIFICATE-----\r\nMIIK...A=\r\n-----END CERTIFICATE-----\r\n\r\n",
|
||||
"AppId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"M365Env": "gcc",
|
||||
"CertificatePassword": "sample02secret"
|
||||
}
|
||||
},
|
||||
{
|
||||
"TestTenant003": {
|
||||
"DomainName": "sample003.onmicrosoft.com",
|
||||
"DisplayName": "Sample 003",
|
||||
"CertificateB64": "-----BEGIN CERTIFICATE-----\r\nMIIK...A=\r\n-----END CERTIFICATE-----\r\n\r\n",
|
||||
"AppId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
"M365Env": "commercial",
|
||||
"CertificatePassword": "sample03secret"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
82
Testing/Functional/SmokeTest/SmokeTest001.Tests.ps1
Normal file
82
Testing/Functional/SmokeTest/SmokeTest001.Tests.ps1
Normal file
@@ -0,0 +1,82 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Test script to verify Invoke-SCuBA file outputs.
|
||||
.DESCRIPTION
|
||||
Test script to execute Invoke-SCuBA against a given tenant using a service
|
||||
principal. Verifies that all expected products (i.e., files) are generated.
|
||||
.PARAMETER Thumbprint
|
||||
Thumbprint of the certificate associated with the Service Principal.
|
||||
.PARAMETER Organization
|
||||
The tenant domain name for the organization.
|
||||
.PARAMETER AppId
|
||||
The Application Id associated with the Service Principal and certificate.
|
||||
.EXAMPLE
|
||||
$TestContainer = New-PesterContainer -Path "SmokeTest001.Tests.ps1" -Data @{ Thumbprint = $Thumbprint; Organization = "cisaent.onmicrosoft.com"; AppId = $AppId }
|
||||
Invoke-Pester -Container $TestContainer -Output Detailed
|
||||
.EXAMPLE
|
||||
Invoke-Pester -Script .\Testing\Functional\SmokeTest\SmokeTest001.Tests.ps1 -Output Detailed
|
||||
|
||||
#>
|
||||
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Thumbprint', Justification = 'False positive as rule does not scan child scopes')]
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Organization', Justification = 'False positive as rule does not scan child scopes')]
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AppId', Justification = 'False positive as rule does not scan child scopes')]
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'M365Environment', Justification = 'False positive as rule does not scan child scopes')]
|
||||
[CmdletBinding(DefaultParameterSetName='Manual')]
|
||||
param (
|
||||
[Parameter(Mandatory = $true, ParameterSetName = 'Auto')]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$Thumbprint,
|
||||
[Parameter(Mandatory = $true, ParameterSetName = 'Auto')]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$Organization,
|
||||
[Parameter(Mandatory = $true, ParameterSetName = 'Auto')]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$AppId,
|
||||
[Parameter(ParameterSetName = 'Auto')]
|
||||
[Parameter(ParameterSetName = 'Manual')]
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$M365Environment = 'gcc'
|
||||
)
|
||||
|
||||
$ScubaModulePath = Join-Path -Path $PSScriptRoot -ChildPath "../../../PowerShell/ScubaGear/ScubaGear.psd1"
|
||||
Import-Module $ScubaModulePath
|
||||
|
||||
Describe "Smoke Test: Generate Output" {
|
||||
Context "Invoke Scuba for $Organization" {
|
||||
BeforeAll {
|
||||
if ($PSCmdlet.ParameterSetName -eq 'Manual'){
|
||||
Invoke-SCuBA -ProductNames "*" -M365Environment $M365Environment
|
||||
}
|
||||
else {
|
||||
Invoke-SCuBA -CertificateThumbprint $Thumbprint -AppID $AppId -Organization $Organization -ProductNames "*" -M365Environment $M365Environment
|
||||
}
|
||||
$ReportFolders = Get-ChildItem . -directory -Filter "M365BaselineConformance*" | Sort-Object -Property LastWriteTime -Descending
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'OutputFolder',
|
||||
Justification = 'Variable is used in another scope')]
|
||||
$OutputFolder = $ReportFolders[0]
|
||||
}
|
||||
It "Item, <Item>, exists" -ForEach @(
|
||||
@{Item = 'BaselineReports.html'; ItemType = 'Leaf'},
|
||||
@{Item = 'TestResults.json'; ItemType = 'Leaf'},
|
||||
@{Item = 'TestResults.csv'; ItemType = 'Leaf'},
|
||||
@{Item = 'ProviderSettingsExport.json'; ItemType = 'Leaf'},
|
||||
@{Item = 'IndividualReports'; ItemType = 'Container'},
|
||||
@{Item = 'IndividualReports/AADReport.html'; ItemType = 'Leaf'},
|
||||
@{Item = 'IndividualReports/DefenderReport.html'; ItemType = 'Leaf'},
|
||||
@{Item = 'IndividualReports/EXOReport.html'; ItemType = 'Leaf'},
|
||||
@{Item = 'IndividualReports/OneDriveReport.html'; ItemType = 'Leaf'},
|
||||
@{Item = 'IndividualReports/PowerPlatformReport.html'; ItemType = 'Leaf'},
|
||||
@{Item = 'IndividualReports/SharePointReport.html'; ItemType = 'Leaf'},
|
||||
@{Item = 'IndividualReports/TeamsReport.html'; ItemType = 'Leaf'},
|
||||
@{Item = 'IndividualReports/images'; ItemType = 'Container'}
|
||||
){
|
||||
Test-Path -Path "./$OutputFolder/$Item" -PathType $ItemType |
|
||||
Should -Be $true
|
||||
} }
|
||||
}
|
||||
118
Testing/Functional/SmokeTest/SmokeTest002.Tests.ps1
Normal file
118
Testing/Functional/SmokeTest/SmokeTest002.Tests.ps1
Normal file
@@ -0,0 +1,118 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Test script to verify Invoke-SCuBA generates valid HTML products.
|
||||
.DESCRIPTION
|
||||
Test script to test Scuba HTML reports validity.
|
||||
.PARAMETER OrganizationDomain
|
||||
The Organizations domain name (e.g., abd.onmicrosoft.com)
|
||||
.PARAMETER OrganizationName
|
||||
The Organizations friendly name (e.g., The ABC Corporation)
|
||||
.EXAMPLE
|
||||
$TestContainer = New-PesterContainer -Path "SmokeTest002.Tests.ps1" -Data @{ OrganizationDomain = "cisaent.onmicrosoft.com"; OrganizationName = "Cybersecurity and Infrastructure Security Agency" }
|
||||
Invoke-Pester -Container $TestContainer -Output Detailed
|
||||
.NOTES
|
||||
The test expects the Scuba output files to exists from a previous run of Invoke-Scuba for the same tenant and all products.
|
||||
|
||||
#>
|
||||
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'OrganizationDomain', Justification = 'False positive as rule does not scan child scopes')]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$OrganizationDomain,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]
|
||||
$OrganizationName
|
||||
)
|
||||
|
||||
Import-Module Selenium
|
||||
|
||||
Describe -Tag "UI","Chrome" -Name "Test Report with <Browser> for $OrganizationName" -ForEach @(
|
||||
@{ Browser = "Chrome"; Driver = Start-SeChrome -Arguments @('start-maximized') 2>$null }
|
||||
){
|
||||
BeforeAll {
|
||||
$ReportFolders = Get-ChildItem . -directory -Filter "M365BaselineConformance*" | Sort-Object -Property LastWriteTime -Descending
|
||||
$OutputFolder = $ReportFolders[0]
|
||||
$BaselineReports = Join-Path -Path $OutputFolder -ChildPath 'BaselineReports.html'
|
||||
#$script:url = ([System.Uri](Get-Item $BaselineReports).FullName).AbsoluteUri
|
||||
$script:url = (Get-Item $BaselineReports).FullName
|
||||
Open-SeUrl $script:url -Driver $Driver 2>$null
|
||||
}
|
||||
|
||||
Context "Check Main HTML" {
|
||||
BeforeAll {
|
||||
$TenantDataElement = Get-SeElement -Driver $Driver -Wait -ClassName "tenantdata"
|
||||
$TenantDataRows = Get-SeElement -Target $TenantDataElement -By TagName "tr"
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'TenantDataColumns',
|
||||
Justification = 'Variable is used in another scope')]
|
||||
$TenantDataColumns = Get-SeElement -Target $TenantDataRows[1] -By TagName "td" }
|
||||
It "Verify Tenant"{
|
||||
|
||||
$Tenant = $TenantDataColumns[0].Text
|
||||
$Tenant | Should -Be $OrganizationName -Because $Tenant
|
||||
}
|
||||
|
||||
It "Verify Domain"{
|
||||
$Domain = $TenantDataColumns[1].Text
|
||||
$Domain | Should -Be $OrganizationDomain -Because "Domain is $Domain"
|
||||
}
|
||||
}
|
||||
|
||||
Context "Navigation to detailed reports" {
|
||||
It "Navigate to <Product> (<LinkText>) details" -ForEach @(
|
||||
@{Product = "aad"; LinkText = "Azure Active Directory"}
|
||||
@{Product = "defender"; LinkText = "Microsoft 365 Defender"}
|
||||
@{Product = "onedrive"; LinkText = "OneDrive for Business"}
|
||||
@{Product = "exo"; LinkText = "Exchange Online"}
|
||||
@{Product = "powerplatform"; LinkText = "Microsoft Power Platform"}
|
||||
@{Product = "sharepoint"; LinkText = "SharePoint Online"}
|
||||
@{Product = "teams"; LinkText = "Microsoft Teams"}
|
||||
){
|
||||
$DetailLink = Get-SeElement -Driver $Driver -Wait -By LinkText $LinkText
|
||||
$DetailLink | Should -Not -BeNullOrEmpty
|
||||
Invoke-SeClick -Element $DetailLink
|
||||
|
||||
Open-SeUrl -Back -Driver $Driver
|
||||
}
|
||||
}
|
||||
|
||||
Context "Verify Table are populated" {
|
||||
BeforeEach{
|
||||
Open-SeUrl $script:url -Driver $Driver 2>$null
|
||||
}
|
||||
It "Check <Product> (<LinkText>) tables" -ForEach @(
|
||||
@{Product = "aad"; LinkText = "Azure Active Directory"}
|
||||
@{Product = "defender"; LinkText = "Microsoft 365 Defender"}
|
||||
@{Product = "onedrive"; LinkText = "OneDrive for Business"}
|
||||
@{Product = "exo"; LinkText = "Exchange Online"}
|
||||
@{Product = "powerplatform"; LinkText = "Microsoft Power Platform"}
|
||||
@{Product = "sharepoint"; LinkText = "SharePoint Online"}
|
||||
@{Product = "teams"; LinkText = "Microsoft Teams"}
|
||||
){
|
||||
$DetailLink = Get-SeElement -Driver $Driver -Wait -By LinkText $LinkText
|
||||
$DetailLink | Should -Not -BeNullOrEmpty
|
||||
Invoke-SeClick -Element $DetailLink
|
||||
|
||||
$Tables = Get-SeElement -Driver $Driver -By TagName 'table'
|
||||
$Tables.Count | Should -BeGreaterThan 1
|
||||
|
||||
ForEach ($Table in $Tables){
|
||||
$Row = Get-SeElement -Element $Table -By TagName 'tr'
|
||||
$Row.Count | Should -BeGreaterThan 0
|
||||
|
||||
ForEach ($Row in $Rows){
|
||||
$RowHeaders = Get-SeElement -Element $Row -By TagName 'th'
|
||||
$RowHeaders.Count | Should -BeExactly 1
|
||||
$RowData = Get-SeElement -Element $Row -By TagName 'td'
|
||||
$RowData.Count | Should -BeGreaterThan 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Stop-SeDriver -Driver $Driver 2>$null
|
||||
}
|
||||
}
|
||||
88
Testing/Functional/SmokeTest/SmokeTestUtils.ps1
Normal file
88
Testing/Functional/SmokeTest/SmokeTestUtils.ps1
Normal file
@@ -0,0 +1,88 @@
|
||||
function New-ServicePrincipalCertificate{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Add certificate into 'My' certificate store of current user.
|
||||
|
||||
.DESCRIPTION
|
||||
This script adds a certificate into the 'My' certificate store of the current user.
|
||||
|
||||
.PARAMETER EncodedCertificate
|
||||
A base 64 encoded PFX certificate
|
||||
|
||||
.PARAMETER CertificatePassword
|
||||
The password of the certificate
|
||||
|
||||
.OUTPUTS
|
||||
Thumbprint of the added certificate. <System.String>
|
||||
|
||||
.EXAMPLE
|
||||
$CertPwd = ConvertTo-SecureString -String $PlainTextPassword -Force -AsPlainText
|
||||
$M365Env = $TestTenant.M365Env
|
||||
try {
|
||||
$Result = New-ServicePrincipalCertificate `
|
||||
-EncodedCertificate $TestTenant.CertificateB64 `
|
||||
-CertificatePassword $CertPwd
|
||||
$Thumbprint = $Result[-1]
|
||||
}
|
||||
catch {
|
||||
Write-Output "Failed to install certificate for $OrgName"
|
||||
}
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Object[]]$EncodedCertificate,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[SecureString]$CertificatePassword
|
||||
)
|
||||
|
||||
Set-Content -Path .\ScubaExecutionCert.txt -Value $EncodedCertificate
|
||||
certutil -decode .\ScubaExecutionCert.txt .\ScubaExecutionCert.pfx
|
||||
$Certificate = Import-PfxCertificate -FilePath .\ScubaExecutionCert.pfx -CertStoreLocation Cert:\CurrentUser\My -Password $CertificatePassword
|
||||
$Thumbprint = ([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate).Thumbprint
|
||||
Remove-Item -Path .\ScubaExecutionCert.txt
|
||||
Remove-Item -Path .\ScubaExecutionCert.pfx
|
||||
return $Thumbprint
|
||||
}
|
||||
|
||||
function Remove-MyCertificates{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Remove all certificates from 'My' certificate store of current user.
|
||||
|
||||
.DESCRIPTION
|
||||
This script removes all certificates from the 'My' certificxate store of the current user.
|
||||
|
||||
.EXAMPLE
|
||||
Remove-MyCertificates
|
||||
#>
|
||||
Get-ChildItem Cert:\CurrentUser\My | ForEach-Object {
|
||||
Remove-Item -Path $_.PSPath -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
function Install-SmokeTestExternalDependencies{
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Install dependencies on GitHub runner to support smoke test.
|
||||
|
||||
.DESCRIPTION
|
||||
This script installs dependencies needed by the SCuBA smoke test. For example, Selenium and the Open Policy Agent.
|
||||
|
||||
.EXAMPLE
|
||||
Install-SmokeTestExternalDependencies
|
||||
#>
|
||||
#Workaround till update to version 2.0+
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'PNPPOWERSHELL_UPDATECHECK',
|
||||
Justification = 'Variable defined outside this scope')]
|
||||
$PNPPOWERSHELL_UPDATECHECK = 'Off'
|
||||
Install-Module -Name "PnP.PowerShell" -RequiredVersion 1.12 -Force
|
||||
./SetUp.ps1 -SkipUpdate
|
||||
|
||||
#Import Selenium and update drivers
|
||||
Install-Module Selenium
|
||||
Testing/Functional/SmokeTest/UpdateSelenium.ps1
|
||||
}
|
||||
113
Testing/Functional/SmokeTest/UpdateSelenium.ps1
Normal file
113
Testing/Functional/SmokeTest/UpdateSelenium.ps1
Normal file
@@ -0,0 +1,113 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Installs Chrome Web Driver on local machine
|
||||
|
||||
.DESCRIPTION
|
||||
This script installs the required web driver needed for the current Chrome Browser installed on the machine.
|
||||
|
||||
.PARAMETER rootRegistry
|
||||
The root location in registry to check version of currently installed apps
|
||||
|
||||
.PARAMETER chromeRegistryPath
|
||||
The direct registry location for Chrome (to check version)
|
||||
|
||||
.PARAMETER webDriversPath
|
||||
The local path for all web drivers
|
||||
|
||||
.PARAMETER chromeDriverPath
|
||||
The direct Chrome driver path
|
||||
|
||||
.PARAMETER chromeDriverWebsite
|
||||
The Chrome web driver downloads page
|
||||
|
||||
.PARAMETER chromeDriverUrlBase
|
||||
URL base to ubild direct download link for Chrome driver
|
||||
|
||||
.PARAMETER chromeDriverUrlEnd
|
||||
Chrome driver download ending (to finish building the URL)
|
||||
|
||||
#>
|
||||
param (
|
||||
$registryRoot = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths",
|
||||
$chromeRegistryPath = "$registryRoot\chrome.exe",
|
||||
$webDriversPath = "C:\Program Files\WindowsPowerShell\Modules\Selenium\3.0.1\assemblies",
|
||||
$chromeDriverPath = "$($webDriversPath)\chromedriver.exe",
|
||||
$chromeDriverWebsite = "https://chromedriver.chromium.org/downloads",
|
||||
$chromeDriverUrlBase = "https://chromedriver.storage.googleapis.com",
|
||||
$chromeDriverUrlEnd = "chromedriver_win32.zip"
|
||||
)
|
||||
function Get-LocalDriverVersion{
|
||||
param(
|
||||
$pathToDriver # direct path to the driver
|
||||
)
|
||||
$processInfo = New-Object System.Diagnostics.ProcessStartInfo # need to pass the switch & catch the output, hence ProcessStartInfo is used
|
||||
|
||||
$processInfo.FileName = $pathToDriver
|
||||
$processInfo.RedirectStandardOutput = $true # need to catch the output - the version
|
||||
$processInfo.Arguments = "-v"
|
||||
$processInfo.UseShellExecute = $false # hide execution
|
||||
|
||||
$process = New-Object System.Diagnostics.Process
|
||||
|
||||
$process.StartInfo = $processInfo
|
||||
$process.Start() | Out-Null
|
||||
$process.WaitForExit() # run synchronously, we need to wait for result
|
||||
$processStOutput = $process.StandardOutput.ReadToEnd()
|
||||
|
||||
return ($processStOutput -split " ")[1] # ... while Chrome on 2nd place
|
||||
}
|
||||
|
||||
function Confirm-NeedForUpdate{
|
||||
param(
|
||||
$v1,
|
||||
$v2
|
||||
)
|
||||
Write-Debug -Message "v1: $v1; v2: $v2"
|
||||
return ([System.Version]$v2).Major -lt ([System.Version]$v1).Major
|
||||
}
|
||||
|
||||
$DebugPreference = 'Continue'
|
||||
#$DebugPreference = 'SilentlyContinue'
|
||||
|
||||
# firstly check which browser versions are installed (from registry)
|
||||
$chromeVersion = (Get-Item (Get-ItemProperty $chromeRegistryPath).'(Default)').VersionInfo.ProductVersion
|
||||
Write-Debug -Message "Chrome driver version(registery): $chromeVersion"
|
||||
|
||||
# check which driver versions are installed
|
||||
$chromeDriverVersion = Get-LocalDriverVersion -pathToDriver $chromeDriverPath
|
||||
|
||||
if (Confirm-NeedForUpdate $chromeVersion $chromeDriverVersion){
|
||||
Write-Debug -Message "Need to update chrome driver from $chromeDriverVersion to $chromeVersion"
|
||||
|
||||
# find exact matching version
|
||||
$chromeDriverAvailableVersions = (Invoke-RestMethod $chromeDriverWebsite) -split " " | Where-Object {$_ -like "*href=*?path=*"} | ForEach-Object {$_.replace("href=","").replace('"','')}
|
||||
$versionLink = $chromeDriverAvailableVersions | Where-Object {$_ -like "*$chromeVersion/*"}
|
||||
|
||||
# if cannot find (e.g. it's too new to have a web driver), look for relevant major version
|
||||
if (!$versionLink){
|
||||
$browserMajorVersion = $chromeVersion.Substring(0, $chromeVersion.IndexOf("."))
|
||||
$versionLink = $chromeDriverAvailableVersions | Where-Object {$_ -like "*$browserMajorVersion.*"}
|
||||
}
|
||||
|
||||
# in case of multiple links, take the first only
|
||||
if ($versionLink.Count -gt 1){
|
||||
$versionLink = $versionLink[0]
|
||||
}
|
||||
|
||||
# build tge download URL according to found version and download URL schema
|
||||
$version = ($versionLink -split"=" | Where-Object {$_ -like "*.*.*.*/"}).Replace('/','')
|
||||
$downloadLink = "$chromeDriverUrlBase/$version/$chromeDriverUrlEnd"
|
||||
|
||||
# download the file
|
||||
Invoke-WebRequest $downloadLink -OutFile "chromeNewDriver.zip"
|
||||
|
||||
# epand archive and replace the old file
|
||||
Expand-Archive "chromeNewDriver.zip" -DestinationPath "chromeNewDriver\" -Force
|
||||
Remove-Item -Path "$($webDriversPath)\chromedriver.exe" -Force
|
||||
Move-Item "chromeNewDriver/chromedriver.exe" -Destination "$($webDriversPath)\chromedriver.exe" -Force
|
||||
|
||||
# clean-up
|
||||
Remove-Item "chromeNewDriver.zip" -Force
|
||||
Remove-Item "chromeNewDriver" -Recurse -Force
|
||||
}
|
||||
#endregion MAIN SCRIPT
|
||||
485
Testing/RunFunctionalTests.ps1
Normal file
485
Testing/RunFunctionalTests.ps1
Normal file
@@ -0,0 +1,485 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Test SCuBA tool against various outputs for functional testing.
|
||||
|
||||
.DESCRIPTION
|
||||
This script executes prexisting provider exports against the Rego code and compares output against saved runs for
|
||||
regression testing.
|
||||
|
||||
To run the test on the Rego test results, the user MUST have a folder called BasicRegressionTests saved somewhere in
|
||||
their home directory (e.g., Downloads, Documents, Desktop). The BasicRegressionTests folder holds sub folders for each
|
||||
provider that is being regression tested based on the product name designation used in ScubaGear. These include aad,
|
||||
defender, exo, onedrive, powerplatform, sharepoint, and teams. Each subfolder contains a pair of files: the provider JSON
|
||||
and test results JSON. These files MUST be generated using the main branch and are used as master copy references to
|
||||
compare against output generated by new runs of ScubaGear by the functional testing tool. Each file pair must be renamed
|
||||
using the following naming convention:
|
||||
- SettingsExport.json renamed to <Provider>ProviderExport-<tennant>-<mmddyyyy>
|
||||
- TestResults.json renamed to <Provider>TestResults-<tennant>-<mmddyyyy>
|
||||
|
||||
EXAMPLE
|
||||
- AADProviderExport-contoso-01052023
|
||||
- AADTestResults-contoso-01052023
|
||||
|
||||
.OUTPUTS
|
||||
Text output that indicates how many tests were consistent or different from the saved test results.
|
||||
|
||||
.EXAMPLE
|
||||
.\RunFunctionalTests.ps1
|
||||
Running against all Rego regression tests is default, no flags necessary.
|
||||
|
||||
.EXAMPLE
|
||||
.\RunFunctionalTests.ps1 -p teams,exo,defender,aad
|
||||
Runs all test cases for specified products. Products must be specified with -p parameter.
|
||||
Valid product names are: aad, defender, exo, onedrive, powerplatform, sharepoint, teams, and '*'.
|
||||
Runs all products on default.
|
||||
|
||||
.EXAMPLE
|
||||
.\RunFunctionalTests.ps1 -t Rego -p *
|
||||
To run a specific type of test, must indicate test with -t. Possible types are: Rego, Full
|
||||
Runs Rego regression test on default.
|
||||
|
||||
.EXAMPLE
|
||||
.\RunFunctionalTests.ps1 -a Simple
|
||||
To run a predefined set of tests, must indicate type with -a. Possible types are: Simple, Minimum, Extreme
|
||||
CAUTION when using Extreme, there are 1957 test cases. Can be used when running against tenant or Rego regression test
|
||||
|
||||
.EXAMPLE
|
||||
.\RunFunctionalTests.ps1 -o .\Functional\Reports
|
||||
Enter the file path for the SCuBA working directory. This is where the ProviderExport, TestResults, and Report will be generated by the tool.
|
||||
The default path is .\Functional\Reports.
|
||||
|
||||
.EXAMPLE
|
||||
.\RunFunctionalTests.ps1 -s .\Functional\Archive
|
||||
Enter the file path for where the test results from the Rego regression test will be saved. The default path is .\Functional\Archive.
|
||||
|
||||
.EXAMPLE
|
||||
.\RunFunctionalTests.ps1 -i .\BasicRegressionTests
|
||||
Enter the directory path where the saved provider exports & test results are for the rego test. The default path is .\Functional\BasicRegressionTests
|
||||
|
||||
.EXAMPLE
|
||||
.\RunFunctionalTests.ps1 -v
|
||||
Outputs the verbose results for the test.
|
||||
|
||||
.EXAMPLE
|
||||
.\RunFunctionalTests.ps1 -q $false
|
||||
Choose to supress the reports from open immediately after generation.
|
||||
#>
|
||||
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'VerboseOutput',
|
||||
Justification = 'variable is used in another scope')]
|
||||
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
<#
|
||||
.PARAMETER Products
|
||||
Takes a comma seperated list of product names to run the script
|
||||
against: 'teams', 'exo', 'defender', 'aad', 'powerplatform', 'sharepoint', 'onedrive', '*'. Runs all on default.
|
||||
#>
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet('teams', 'exo', 'defender', 'aad', 'powerplatform', 'sharepoint', 'onedrive', '*', IgnoreCase = $false)]
|
||||
[Alias('p')]
|
||||
[string[]]$Products = '*',
|
||||
|
||||
<#
|
||||
.PARAMETER TestType
|
||||
Takes the user's selection of test type: Rego, Full. Runs Rego on default.
|
||||
#>
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet('Rego', 'Full')]
|
||||
[Alias('t')]
|
||||
[string]$TestType = 'Rego',
|
||||
|
||||
<#
|
||||
.PARAMETER Auto
|
||||
Takes the user's selection of auto test type: Simple, Minimum, Extreme.
|
||||
#>
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet('Simple', 'Minimum', 'Extreme')]
|
||||
[Alias('a')]
|
||||
[string]$Auto = '',
|
||||
|
||||
<#
|
||||
.PARAMETER Out
|
||||
Takes the user's selection of SCuBA's working directory.
|
||||
#>
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('o')]
|
||||
[string]$Out = '.\Functional\Reports',
|
||||
|
||||
<#
|
||||
.PARAMETER Save
|
||||
Takes the user's selection of where test results from regression test is to be saved.
|
||||
#>
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('s')]
|
||||
[string]$Save = '.\Functional\Archive',
|
||||
|
||||
<#
|
||||
.PARAMETER RegressionTests
|
||||
Takes the directory path to the Regression Tests.
|
||||
#>
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[Alias('i')]
|
||||
[string]$RegressionTests = (Join-Path -Path $Home -ChildPath 'BasicRegressionTests'),
|
||||
|
||||
<#
|
||||
.PARAMETER VerboseOutput
|
||||
Prints the verbose output.
|
||||
#>
|
||||
[Parameter(Mandatory = $false)]
|
||||
[Alias('v')]
|
||||
[switch]$VerboseOutput,
|
||||
|
||||
<#
|
||||
.PARAMETER Quiet
|
||||
Runs SCuBA in silent mode so the reports do not open immediately after generation.
|
||||
#>
|
||||
[Parameter(Mandatory = $false)]
|
||||
[Alias('q')]
|
||||
[switch]$Quiet
|
||||
)
|
||||
|
||||
function Compare-Results {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$Filename
|
||||
)
|
||||
|
||||
$ResultRegression = $Filename -replace 'ProviderExport', 'TestResults'
|
||||
$RegressionJson = Get-Content $ResultRegression | ConvertFrom-Json
|
||||
$TestResultFile = Get-ChildItem $Out -Filter *.json | Where-Object { $_.Name -match 'TestResults' } | Select-Object Fullname
|
||||
$ResultNew = Get-SavedFilename $ResultRegression
|
||||
Copy-Item -Path $TestResultFile.Fullname -Destination $ResultNew
|
||||
|
||||
if (Confirm-FileExists $ResultNew) {
|
||||
$NewJson = Get-Content $ResultNew | ConvertFrom-Json
|
||||
|
||||
if (($RegressionJson | ConvertTo-Json -Compress) -eq ($NewJson | ConvertTo-Json -Compress)) {
|
||||
return "`n`t$(Split-Path -Path $ResultRegression -Leaf -Resolve) : CONSISTENT"
|
||||
}
|
||||
else {
|
||||
try {
|
||||
code --diff $ResultRegression $ResultNew
|
||||
}
|
||||
catch {
|
||||
Compare-Object (($RegressionJson | ConvertTo-Json) -split '\r?\n') (($NewJson | ConvertTo-Json) -split '\r?\n')
|
||||
Write-Output "`n==== $(Split-Path -Path $ResultRegression -Leaf -Resolve) vs $(Split-Path -Path $ResultNew -Leaf -Resolve) ====`n" | Out-Host
|
||||
}
|
||||
}
|
||||
|
||||
return "`n`t$(Split-Path -Path $ResultRegression -Leaf -Resolve) : DIFFERENT"
|
||||
}
|
||||
}
|
||||
|
||||
function Confirm-FileExists {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$Filename
|
||||
)
|
||||
|
||||
if (Test-Path -Path $Filename -PathType Leaf) {
|
||||
return $true
|
||||
}
|
||||
else {
|
||||
Write-Warning "$Filename not found`nSkipping......`n" | Out-Host
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-SavedFilename {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$Filepath
|
||||
)
|
||||
|
||||
$Filename = Split-Path -Path $Filepath -Leaf -Resolve
|
||||
$Date = Get-Date -Format 'MMddyyyy'
|
||||
$NewFilename = $Filename -replace '[0-9]+\.json', ($Date + '.json')
|
||||
|
||||
return Join-Path -Path (Get-Item $Save) -ChildPath $NewFilename
|
||||
}
|
||||
|
||||
function Get-ProviderExportFiles {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$FilePath
|
||||
)
|
||||
|
||||
try {
|
||||
$TestFiles = (Get-ChildItem $FilePath -ErrorAction Stop | Where-Object { $_.Name -match 'ProviderExport' } | Select-Object FullName).FullName
|
||||
return $true, $TestFiles
|
||||
}
|
||||
catch {
|
||||
Write-Warning "$Product is missing, no files for Rego test found`nSkipping......`n" | Out-Host
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function Write-RegoOutput {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet('aad', 'defender', 'exo', 'onedrive', 'powerplatform', 'sharepoint', 'teams', '*', IgnoreCase = $false)]
|
||||
[string[]]$Products,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string[]]$RegoResults
|
||||
)
|
||||
|
||||
if ($VerboseOutput.IsPresent) {
|
||||
Write-Output "`n`t=== Testing @($($Products -join ",")) ===$($RegoResults[2])"
|
||||
}
|
||||
elseif ($Result[3] -ne "") {
|
||||
Write-Output "`n`t=== Testing @($($Products -join ",")) ===$($RegoResults[3])"
|
||||
}
|
||||
}
|
||||
|
||||
function Read-AutoFile {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$Filename
|
||||
)
|
||||
|
||||
$Result = @(0, 0)
|
||||
$LogIn = $true
|
||||
if ($TestType -eq 'Full') {
|
||||
if ($Quiet.IsPresent -eq $false) {
|
||||
$Quiet = Confirm-UserSelection 'Do you want reports to open immediately after generation [y/n]'
|
||||
}
|
||||
}
|
||||
|
||||
if (Confirm-FileExists $Filename) {
|
||||
foreach ($Products in Get-Content $Filename) {
|
||||
if ($TestType -eq 'Full') {
|
||||
Invoke-Full $Products -LogIn $LogIn -Silent $Quiet
|
||||
$LogIn = $false
|
||||
}
|
||||
elseif ($TestType -eq 'Rego') {
|
||||
$Result = Invoke-Rego -Products $Products -PassCount $Result[0] -TotalCount $Result[1]
|
||||
}
|
||||
}
|
||||
if (($TestType -eq 'Rego') -and ($Result[1] -gt 0)) {
|
||||
Write-RegoOutput $Products $Result
|
||||
Write-Output "`n`tCONSISTENT $($Result[0])/$($Result[1])`n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Rego {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet('aad', 'defender', 'exo', 'onedrive', 'powerplatform', 'sharepoint', 'teams', '*', IgnoreCase = $false)]
|
||||
[string[]]$Products,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[int]$PassCount,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[int]$TotalCount
|
||||
)
|
||||
|
||||
$ExportFilename = Join-Path -Path $Out -ChildPath 'ProviderSettingsExport.json'
|
||||
$VerboseOutput = ' '
|
||||
$FailString = ' '
|
||||
|
||||
foreach ($Product in $Products) {
|
||||
$FilePath = Join-Path -Path $RegressionTests -ChildPath $Product
|
||||
$FilesFound = Get-ProviderExportFiles $FilePath
|
||||
|
||||
if ($FilesFound[0]) {
|
||||
$TotalCount += $FilesFound[1].Length
|
||||
|
||||
foreach ($File in $FilesFound[1]) {
|
||||
|
||||
if (Confirm-FileExists $File) {
|
||||
Copy-Item -Path $File -Destination $ExportFilename
|
||||
|
||||
if (Confirm-FileExists $ExportFilename) {
|
||||
try {
|
||||
.\Functional\RegoCachedProviderTesting.ps1 -ProductNames $Product -ExportProvider $false -OutPath $Out
|
||||
}
|
||||
catch {
|
||||
Set-Location $PSScriptRoot
|
||||
Write-Error "Unknown problem running '.\Functional\RegoCachedProviderTesting.ps1', please report."
|
||||
exit
|
||||
}
|
||||
Set-Location $PSScriptRoot
|
||||
$ResultString = Compare-Results $File
|
||||
|
||||
if ($ResultString.Contains('CONSISTENT')) {
|
||||
$PassCount += 1
|
||||
}
|
||||
else {
|
||||
$FailString += $ResultString
|
||||
}
|
||||
|
||||
$VerboseOutput += $ResultString
|
||||
Remove-Item $ExportFilename
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $PassCount, $TotalCount, $VerboseOutput, $FailString
|
||||
}
|
||||
|
||||
function Invoke-Full {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet('aad', 'defender', 'exo', 'onedrive', 'powerplatform', 'sharepoint', 'teams', '*', IgnoreCase = $false)]
|
||||
[string[]]$Products,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet($true, $false)]
|
||||
[boolean]
|
||||
$LogIn = $false,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet($true, $false)]
|
||||
[boolean]
|
||||
$Quiet = $True
|
||||
)
|
||||
try {
|
||||
.\Functional\RegoCachedProviderTesting.ps1 -ProductNames $Products -OutPath $Out -LogIn $LogIn -Quiet $Quiet
|
||||
}
|
||||
catch {
|
||||
Set-Location $PSScriptRoot
|
||||
Write-Error "Unknown problem running '.\Functional\RegoCachedProviderTesting.ps1', please report."
|
||||
exit
|
||||
}
|
||||
Set-Location $PSScriptRoot
|
||||
|
||||
}
|
||||
|
||||
function Invoke-Auto {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidateSet('Simple', 'Minimum', 'Extreme')]
|
||||
[string]$Auto
|
||||
)
|
||||
|
||||
$Filename = ''
|
||||
|
||||
switch ($Auto) {
|
||||
'Extreme' {
|
||||
Write-Warning "File has 1957 tests!`n" | Out-Host
|
||||
if ((Confirm-UserSelection "Do you wish to continue [y/n]?") -eq $false) {
|
||||
Write-Output "Canceling....."
|
||||
exit
|
||||
}
|
||||
Write-Output "Continuing.....`nEnter Ctrl+C to cancel`n"
|
||||
|
||||
$Filename = "Functional\Auto\ExtremeTest.txt"
|
||||
}
|
||||
'Minimum' {
|
||||
$Filename = "Functional\Auto\MinimumTest.txt"
|
||||
}
|
||||
'Simple' {
|
||||
$Filename = "Functional\Auto\SimpleTest.txt"
|
||||
}
|
||||
Default {
|
||||
Write-Error "Uknown auto test '$Auto'"
|
||||
}
|
||||
}
|
||||
|
||||
Read-AutoFile $Filename
|
||||
}
|
||||
|
||||
function Confirm-Continue {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[String]$Prompt
|
||||
)
|
||||
|
||||
$Choice = Read-Host -Prompt $Prompt
|
||||
|
||||
if (($Choice -ne 'y') -or ($Choice -ne 'yes')) {
|
||||
return $true
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
function New-Folders {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[String]$Folder
|
||||
)
|
||||
|
||||
if ((Test-Path $Folder) -eq $false) {
|
||||
New-Item $Folder -ItemType Directory
|
||||
}
|
||||
}
|
||||
|
||||
function Get-AbsolutePath {
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$FilePath
|
||||
)
|
||||
|
||||
$NewFilePath = (Get-ChildItem -Recurse -Filter $(Split-Path -Path $FilePath -Leaf) -Directory -ErrorAction SilentlyContinue -Path $(Split-Path -Path $FilePath)).FullName
|
||||
|
||||
if ($null -eq $NewFilePath) {
|
||||
Write-Error "$FilePath NOT FOUND" | Out-Host
|
||||
exit
|
||||
}
|
||||
return $NewFilePath
|
||||
}
|
||||
|
||||
New-Folders $Out
|
||||
$Out = Get-AbsolutePath $Out
|
||||
|
||||
if ($Products[0] -eq '*') {
|
||||
[string[]] $Products = ((Get-ChildItem -Path 'Unit\Rego' -Recurse -Directory -Force -ErrorAction SilentlyContinue |
|
||||
Select-Object Name).Name).toLower()
|
||||
}
|
||||
|
||||
if ($Auto -ne '') {
|
||||
if ($TestType -eq 'Full') {
|
||||
Write-Output "COMING SOON: Disabled until defender bug is fixed"
|
||||
exit
|
||||
}
|
||||
Invoke-Auto $Auto
|
||||
}
|
||||
|
||||
elseif ($TestType -eq 'Rego') {
|
||||
New-Folders $Save
|
||||
$Save = Get-AbsolutePath $Save
|
||||
$RegressionTests = Get-AbsolutePath $RegressionTests
|
||||
$Result = Invoke-Rego -Products $Products -PassCount 0 -TotalCount 0
|
||||
|
||||
if ($Result[1] -gt 0) {
|
||||
Write-RegoOutput $Products $Result
|
||||
Write-Output "`n`tCONSISTENT $($Result[0])/$($Result[1])`n"
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
Write-Output "COMING SOON: Disabled until defender bug is fixed"
|
||||
exit
|
||||
Invoke-Full -Products Products -LogIn $true -Silent $Quiet
|
||||
}
|
||||
218
Testing/RunUnitTests.ps1
Normal file
218
Testing/RunUnitTests.ps1
Normal file
@@ -0,0 +1,218 @@
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter()]
|
||||
[ValidateSet('AAD','Defender','EXO','OneDrive','PowerPlatform','Sharepoint','Teams')]
|
||||
[string[]]$p = "",
|
||||
[Parameter()]
|
||||
[string[]]$b = "",
|
||||
[Parameter()]
|
||||
[string[]]$t = "",
|
||||
[Parameter()]
|
||||
[switch]$h,
|
||||
[Parameter()]
|
||||
[switch]$v
|
||||
)
|
||||
|
||||
$ScriptName = $MyInvocation.MyCommand
|
||||
$FilePath = ".\Unit\Rego"
|
||||
|
||||
function Show-Menu {
|
||||
Write-Output "`n`t==================================== Flags ===================================="
|
||||
Write-Output "`n`t-h`tshows help menu"
|
||||
Write-Output "`n`t-p`tproduct name, can take a comma-separated list of product names"
|
||||
Write-Output "`n`t-b`tbaseline item number, can take a comma-separated list of item numbers"
|
||||
Write-Output "`n`t-t`ttest name, can take a comma-separated list of test names"
|
||||
Write-Output "`n`t-v`tverbose, verbose opa output"
|
||||
Write-Output "`n`t==================================== Usage ===================================="
|
||||
Write-Output "`n`tRuning all tests is default, no flags are necessary"
|
||||
Write-Output "`t.\$ScriptName"
|
||||
Write-Output "`n`tTo run all test cases for specified products, must indicate products with -p"
|
||||
Write-Output "`t.\$ScriptName [-p] <products>"
|
||||
Write-Output "`n`tTo run all test cases in baseline item numbers, must indicate product with -p"
|
||||
Write-Output "`tand baseline item numbers with -b"
|
||||
Write-Output "`t.\$ScriptName [-p] <product> [-b] <baseline numbers>"
|
||||
Write-Output "`n`tTo run test case for specified baseline item number must indicate product with -p,"
|
||||
Write-Output "`tbaseline item numberwith -b, and test cases with -t"
|
||||
Write-Output "`t.\$ScriptName [-p] <product> [-b] <baseline number> [-t] <test names>"
|
||||
Write-Output "`n`tVerbose flag can be added to any test at beginning or end of command line"
|
||||
Write-Output "`t.\$ScriptName [-v]"
|
||||
Write-Output "`n`t==================================== Examples ===================================="
|
||||
Write-Output "`n`t.\$ScriptName -p AAD, Defender, OneDrive"
|
||||
Write-Output "`n`t.\$ScriptName -p AAD -b 01, 2, 10"
|
||||
Write-Output "`n`t.\$ScriptName -p AAD -b 01 -t test_IncludeApplications_Incorrect, test_Conditions_Correct"
|
||||
Write-Output "`n`t.\$ScriptName -p AAD -v"
|
||||
Write-Output "`n`t.\$ScriptName -v -p AAD -b 01 -t test_IncludeApplications_Incorrect`n"
|
||||
exit
|
||||
}
|
||||
|
||||
function Get-ErrorMsg {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[string[]]$Flag
|
||||
)
|
||||
|
||||
$FontColor = $host.ui.RawUI.ForegroundColor
|
||||
$BackgroundColor = $host.ui.RawUI.BackgroundColor
|
||||
$host.ui.RawUI.ForegroundColor = "Red"
|
||||
$host.ui.RawUI.BackgroundColor = "Black"
|
||||
switch ($Flag[0]) {
|
||||
TestNameFlagsMissing {
|
||||
Write-Output "ERROR: Missing value(s) to run opa for specific test case(s)"
|
||||
Write-Output ".\$ScriptName [-p] <product> [-b] <baseline numbers> [-t] <test names>`n"
|
||||
}
|
||||
BaselineItemFlagMissing {
|
||||
Write-Output "ERROR: Missing value(s) to run opa for specific baseline item(s)"
|
||||
Write-Output ".\$ScriptName [-p] <product> [-b] <baseline numbers>`n"
|
||||
}
|
||||
BaselineItemNumber {
|
||||
Write-Output "ERROR: Unrecognized number '$b'"
|
||||
Write-Output "Must be an integer (1, 2, 3, ...) or baseline syntax (01, 02, 03..09, 10, ...)`n"
|
||||
}
|
||||
FileIOError {
|
||||
Write-Output "ERROR: '$($Flag[1])' not found`n"
|
||||
}
|
||||
Default {
|
||||
Write-Output "ERROR: Unknown`n"
|
||||
}
|
||||
}
|
||||
$host.ui.RawUI.ForegroundColor = $FontColor
|
||||
$host.ui.RawUI.BackgroundColor = $BackgroundColor
|
||||
exit
|
||||
}
|
||||
|
||||
function Invoke-Product {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter()]
|
||||
[string]$Flag
|
||||
)
|
||||
|
||||
foreach($Product in $p) {
|
||||
Write-Output "`n==== Testing $Product ===="
|
||||
$Directory = Join-Path -Path $FilePath -ChildPath $Product
|
||||
..\opa_windows_amd64.exe test ..\Rego\ $Directory $Flag
|
||||
}
|
||||
Write-Output ""
|
||||
}
|
||||
|
||||
function Get-Baseline {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[string] $Baseline
|
||||
)
|
||||
|
||||
$Tens = @('01','02','03','04','05','06','07','08','09')
|
||||
if(($Baseline -match "^\d+$") -or ($Baseline -in $Tens)) {
|
||||
if ([int]$Baseline -lt 10) {
|
||||
$Baseline = $Tens[[int]$Baseline-1]
|
||||
}
|
||||
return $true, $Baseline
|
||||
}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Invoke-BaselineItem {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter()]
|
||||
[string]$Flag,
|
||||
[Parameter()]
|
||||
[string]$Product
|
||||
)
|
||||
|
||||
Write-Output "`n==== Testing $Product ===="
|
||||
foreach($Baseline in $b) {
|
||||
$Result = Get-Baseline $Baseline
|
||||
if($Result[0]){
|
||||
$Baseline = $Result[1]
|
||||
$Filename = Get-ChildItem $(Join-Path -Path $FilePath -ChildPath $Product) |
|
||||
Where-Object {$_.Name -match $('Config2_'+$Baseline+'_test.rego')} | Select-Object Fullname
|
||||
|
||||
if(Test-Path -Path $Filename.Fullname -PathType Leaf) {
|
||||
Write-Output "`nTesting Baseline $Baseline"
|
||||
..\opa_windows_amd64.exe test ..\Rego\ .\$($Filename.Fullname) $Flag
|
||||
}
|
||||
else {
|
||||
Get-ErrorMsg FileIOError, $Filename
|
||||
}
|
||||
}
|
||||
else {
|
||||
Get-ErrorMsg BaselineItemNumber
|
||||
}
|
||||
}
|
||||
Write-Output ""
|
||||
}
|
||||
|
||||
function Invoke-TestName {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter()]
|
||||
[string]$Flag,
|
||||
[Parameter()]
|
||||
[string]$Product,
|
||||
[Parameter()]
|
||||
[string]$Baseline
|
||||
)
|
||||
|
||||
$Result = Get-Baseline $Baseline
|
||||
if($Result[0]){
|
||||
$Baseline = $Result[1]
|
||||
$Filename = Get-ChildItem $(Join-Path -Path $FilePath -ChildPath $Product) |
|
||||
Where-Object {$_.Name -match $('Config2_'+$Baseline+'_test.rego')} | Select-Object Fullname
|
||||
|
||||
if(Test-Path -Path $Filename.Fullname -PathType Leaf) {
|
||||
Write-Output "`n==== Testing $Product Baseline $Baseline ===="
|
||||
|
||||
foreach($Test in $t) {
|
||||
Write-Output "`nTesting $Test"
|
||||
..\opa_windows_amd64.exe test ..\Rego\ .\$($Filename.Fullname) -r $Test $Flag
|
||||
}
|
||||
}
|
||||
else {
|
||||
Get-ErrorMsg FileIOError, $Filename
|
||||
}
|
||||
}
|
||||
else {
|
||||
Get-ErrorMsg BaselineItemNumber
|
||||
}
|
||||
Write-Output ""
|
||||
}
|
||||
|
||||
$pEmpty = $p[0] -eq ""
|
||||
$bEmpty = $b[0] -eq ""
|
||||
$tEmpty = $t[0] -eq ""
|
||||
$Flag = ""
|
||||
|
||||
if ($h.IsPresent) {
|
||||
Show-Menu
|
||||
}
|
||||
if ($v.IsPresent) {
|
||||
$Flag = "-v"
|
||||
}
|
||||
if($pEmpty -and $bEmpty -and $tEmpty) {
|
||||
$p = @('AAD','Defender','EXO','OneDrive','PowerPlatform','Sharepoint','Teams')
|
||||
Invoke-Product -Flag $Flag
|
||||
}
|
||||
elseif((-not $pEmpty) -and (-not $bEmpty) -and (-not $tEmpty)) {
|
||||
if (($p.Count -gt 1) -or ($b.Count -gt 1)) {
|
||||
Write-Output "**WARNING** can only take 1 argument for each: product & baseline item`n...Running test for $($p[0]) and $($b[0]) only"
|
||||
}
|
||||
|
||||
Invoke-TestName -Flag $Flag -Product $p[0] -Baseline $b[0]
|
||||
}
|
||||
elseif((-not $pEmpty) -and (-not $bEmpty) -and $tEmpty) {
|
||||
if ($p.Count -gt 1) {
|
||||
Write-Output "**WARNING** can only take 1 argument for product`n...Running test for $($p[0]) only"
|
||||
}
|
||||
Invoke-BaselineItem -Flag $Flag -Product $p[0]
|
||||
}
|
||||
elseif((-not $pEmpty) -and $bEmpty -and $tEmpty) {
|
||||
Invoke-Product -Flag $Flag
|
||||
}
|
||||
elseif($pEmpty -or $bEmpty -and (-not $tEmpty)) {
|
||||
Get-ErrorMsg TestNameFlagsMissing
|
||||
}
|
||||
elseif($pEmpty -and (-not $bEmpty) -and $tEmpty) {
|
||||
Get-ErrorMsg BaselineItemFlagMissing
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "../../../../PowerShell/ScubaGear/Modules/Connection/ConnectHelpers.psm1") -Function 'Connect-DefenderHelper' -Force
|
||||
|
||||
InModuleScope ConnectHelpers {
|
||||
Describe -Tag 'Connection' -Name 'Connect-DefenderHelper' {
|
||||
BeforeAll {
|
||||
Mock -CommandName Connect-IPPSSession -MockWith {}
|
||||
}
|
||||
context 'Without Service Principal'{
|
||||
It 'Invalid M365nvironment parameter' {
|
||||
{Connect-DefenderHelper -M365Environment 'invalid_parameter'} | Should -Throw
|
||||
}
|
||||
It 'Invokes for commercial environment' {
|
||||
Connect-DefenderHelper -M365Environment 'commercial'
|
||||
Should -Invoke -CommandName Connect-IPPSSession -Times 1 -ParameterFilter {$ErrorAction -eq 'Stop' -And $CertificateThumbprint -eq $null}
|
||||
}
|
||||
It 'Invokes for gcc enviorment' {
|
||||
Connect-DefenderHelper -M365Environment 'gcc'
|
||||
Should -Invoke -CommandName Connect-IPPSSession -Times 1 -ParameterFilter {$ErrorAction -eq 'Stop' -And $CertificateThumbprint -eq $null}
|
||||
}
|
||||
It 'Invokes for gcchigh environment' {
|
||||
Connect-DefenderHelper -M365Environment 'gcchigh'
|
||||
Should -Invoke -CommandName Connect-IPPSSession -Times 1 `
|
||||
-ParameterFilter {
|
||||
$ErrorAction -eq 'Stop' -And
|
||||
$CertificateThumbprint -eq $null -And
|
||||
$ConnectionUri -eq 'https://ps.compliance.protection.office365.us/powershell-liveid' -and
|
||||
$AzureADAuthorizationEndpointUri -eq 'https://login.microsoftonline.us/common'
|
||||
}
|
||||
}
|
||||
It 'Invokes for dod environment' {
|
||||
Connect-DefenderHelper -M365Environment 'dod'
|
||||
Should -Invoke -CommandName Connect-IPPSSession -Times 1 `
|
||||
-ParameterFilter {
|
||||
$ErrorAction -eq 'Stop' -And
|
||||
$CertificateThumbprint -eq $null -And
|
||||
$ConnectionUri -eq 'https://l5.ps.compliance.protection.office365.us/powershell-liveid' -and
|
||||
$AzureADAuthorizationEndpointUri -eq 'https://login.microsoftonline.us/common'
|
||||
}
|
||||
}
|
||||
}
|
||||
context 'With Service Principal'{
|
||||
It 'Invoke with Service Principal parameters'{
|
||||
$sp = @{
|
||||
CertThumbprintParams = @{
|
||||
CertificateThumbprint = 'A thumbprint';
|
||||
AppID = 'My Id';
|
||||
Organization = 'My Organization';
|
||||
}
|
||||
}
|
||||
Connect-DefenderHelper -M365Environment 'commercial' -ServicePrincipalParams $sp
|
||||
Should -Invoke -CommandName Connect-IPPSSession -Times 1 -ParameterFilter {$ErrorAction -eq 'Stop' -And $CertificateThumbprint -eq 'A thumbprint'}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AfterAll {
|
||||
Remove-Module ConnectHelpers -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "../../../../PowerShell/ScubaGear/Modules/Connection/ConnectHelpers.psm1") -Function 'Connect-EXOHelper' -Force
|
||||
|
||||
InModuleScope ConnectHelpers {
|
||||
Describe -Tag 'Connection' -Name 'Connect-EXOHelper' -ForEach @(
|
||||
@{Endpoint = 'commercial'}
|
||||
@{Endpoint = 'gcc'}
|
||||
@{Endpoint = 'gcchigh'}
|
||||
@{Endpoint = 'dod'}
|
||||
){
|
||||
BeforeAll {
|
||||
Mock Connect-ExchangeOnline -MockWith {}
|
||||
}
|
||||
It 'When connecting interactively to <Endpoint> endpoint, connects to Exchange Online' {
|
||||
{Connect-EXOHelper -M365Environment $Endpoint} | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
AfterAll {
|
||||
Remove-Module ConnectHelpers -ErrorAction SilentlyContinue
|
||||
}
|
||||
64
Testing/Unit/PowerShell/Connection/Connect-Tenant.Tests.ps1
Normal file
64
Testing/Unit/PowerShell/Connection/Connect-Tenant.Tests.ps1
Normal file
@@ -0,0 +1,64 @@
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "../../../../PowerShell/ScubaGear/Modules/Connection/Connection.psm1") -Function 'Connect-Tenant' -Force
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "../../../../PowerShell/ScubaGear/Modules/Connection/ConnectHelpers.psm1") -Force
|
||||
|
||||
InModuleScope Connection {
|
||||
Describe -Tag 'Connection' -Name "Connect-Tenant" -ForEach @(
|
||||
@{Endpoint = 'commercial'}
|
||||
@{Endpoint = 'gcc'}
|
||||
@{Endpoint = 'gcchigh'}
|
||||
@{Endpoint = 'dod'}
|
||||
){
|
||||
BeforeAll {
|
||||
Mock Connect-MgGraph -MockWith {}
|
||||
Mock Connect-PnPOnline -MockWith {}
|
||||
Mock Connect-SPOService -MockWith {}
|
||||
Mock Connect-MicrosoftTeams -MockWith {}
|
||||
Mock Add-PowerAppsAccount -MockWith {}
|
||||
function Connect-EXOHelper {}
|
||||
Mock Connect-EXOHelper -MockWith {}
|
||||
Mock Select-MgProfile -MockWith {}
|
||||
Mock Get-MgProfile -MockWith {
|
||||
[pscustomobject]@{
|
||||
Name = "alpha";
|
||||
}
|
||||
}
|
||||
Mock Get-MgOrganization -MockWith {
|
||||
return [pscustomobject]@{
|
||||
DisplayName = "DisplayName";
|
||||
Name = "DomainName";
|
||||
Id = "TenantId";
|
||||
VerifiedDomains = @(
|
||||
@{
|
||||
isInitial = $false;
|
||||
Name = "example.onmicrosoft.com"
|
||||
},
|
||||
@{
|
||||
isInitial = $true;
|
||||
Name = "contoso.onmicrosoft.com"
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Mock -CommandName Write-Progress {
|
||||
}
|
||||
}
|
||||
It 'With Endpoint: <Endpoint>; ProductNames: <ProductNames>' -ForEach @(
|
||||
@{ProductNames = "aad"}
|
||||
@{ProductNames = "defender"}
|
||||
@{ProductNames = "exo"}
|
||||
@{ProductNames = "onedrive"}
|
||||
@{ProductNames = "powerplatform"}
|
||||
@{ProductNames = "sharepoint"}
|
||||
@{ProductNames = "teams"}
|
||||
@{ProductNames = "aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams"}
|
||||
|
||||
){
|
||||
$FailedAuthList = Connect-Tenant -ProductNames $ProductNames -M365Environment $Endpoint
|
||||
$FailedAuthList.Length | Should -Be 0
|
||||
}
|
||||
}
|
||||
}
|
||||
AfterAll {
|
||||
Remove-Module Connection -ErrorAction SilentlyContinue
|
||||
Remove-Module ConnectHelper -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "../../../../PowerShell/ScubaGear/Modules/Connection/Connection.psm1") -Function 'Disconnect-SCuBATenant' -Force
|
||||
|
||||
InModuleScope Connection {
|
||||
Describe -Tag 'Connection' -Name 'Disconnect-SCuBATenant' {
|
||||
BeforeAll {
|
||||
Mock Disconnect-MgGraph -MockWith {}
|
||||
Mock Disconnect-ExchangeOnline -MockWith {}
|
||||
Mock Disconnect-SPOService -MockWith {}
|
||||
Mock Disconnect-PnPOnline -MockWith {}
|
||||
Mock Remove-PowerAppsAccount -MockWith {}
|
||||
Mock Disconnect-MicrosoftTeams -MockWith {}
|
||||
Mock -CommandName Write-Progress {}
|
||||
}
|
||||
It 'Disconnects from Microsoft Graph' {
|
||||
Disconnect-SCuBATenant -ProductNames 'aad'
|
||||
Should -Invoke -CommandName Disconnect-MgGraph -Times 1 -Exactly
|
||||
}
|
||||
It 'Disconnects from Exchange Online' {
|
||||
Disconnect-SCuBATenant -ProductNames 'exo'
|
||||
Should -Invoke -CommandName Disconnect-ExchangeOnline -Times 1 -Exactly
|
||||
}
|
||||
It 'Disconnects from Defender (Exchange Online and Security & Compliance)' {
|
||||
{Disconnect-SCuBATenant -ProductNames 'defender'} | Should -Not -Throw
|
||||
}
|
||||
It 'Disconnects from One Drive (SharePoint Online)' {
|
||||
{Disconnect-SCuBATenant -ProductNames 'onedrive'} | Should -Not -Throw
|
||||
}
|
||||
It 'Disconnects from Power Platform' {
|
||||
{Disconnect-SCuBATenant -ProductNames 'powerplatform'} | Should -Not -Throw
|
||||
}
|
||||
It 'Disconnects from SharePoint Online' {
|
||||
{Disconnect-SCuBATenant -ProductNames 'sharepoint'} | Should -Not -Throw
|
||||
}
|
||||
It 'Disconnects from Microsoft Teams' {
|
||||
{Disconnect-SCuBATenant -ProductNames 'sharepoint'} | Should -Not -Throw
|
||||
}
|
||||
It 'Disconnects from all products' {
|
||||
{Disconnect-SCuBATenant} | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Connection -ErrorAction SilentlyContinue
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
111
Testing/Unit/PowerShell/CreateReport/New-Report.Tests.ps1
Normal file
111
Testing/Unit/PowerShell/CreateReport/New-Report.Tests.ps1
Normal file
@@ -0,0 +1,111 @@
|
||||
BeforeAll {
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '../../../../PowerShell/ScubaGear/Modules/CreateReport')
|
||||
New-Item -Path (Join-Path -Path $PSScriptRoot -ChildPath "./CreateReportStubs") -Name "CreateReportUnitFolder" -ErrorAction SilentlyContinue -ItemType Directory | Out-Null
|
||||
New-Item -Path (Join-Path -Path $PSScriptRoot -ChildPath "./CreateReportStubs/CreateReportUnitFolder") -Name "IndividualReports" -ErrorAction SilentlyContinue -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
Describe -Tag CreateReport -Name 'New-Report' {
|
||||
Context "Light mode case" {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ProductNames')]
|
||||
$ProductNames = @("teams", "exo", "defender", "aad", "powerplatform", "sharepoint", "onedrive")
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ArgToProd')]
|
||||
$ArgToProd = @{
|
||||
teams = "Teams";
|
||||
exo = "EXO";
|
||||
defender = "Defender";
|
||||
aad = "AAD";
|
||||
powerplatform = "PowerPlatform";
|
||||
sharepoint = "SharePoint";
|
||||
onedrive = "OneDrive";
|
||||
}
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ProdToFullName')]
|
||||
$ProdToFullName = @{
|
||||
Teams = "Microsoft Teams";
|
||||
EXO = "Exchange Online";
|
||||
Defender = "Microsoft 365 Defender";
|
||||
AAD = "Azure Active Directory";
|
||||
PowerPlatform = "Microsoft Power Platform";
|
||||
SharePoint = "SharePoint Online";
|
||||
OneDrive = "OneDrive for Business";
|
||||
}
|
||||
$IndividualReportPath = (Join-Path -Path $PSScriptRoot -ChildPath "./CreateReportStubs/CreateReportUnitFolder/IndividualReports")
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'CreateReportParams')]
|
||||
$CreateReportParams = @{
|
||||
'IndividualReportPath' = $IndividualReportPath;
|
||||
'OutPath' = (Join-Path -Path $PSScriptRoot -ChildPath "./CreateReportStubs");
|
||||
'OutProviderFileName' = "ProviderSettingsExport";
|
||||
'OutRegoFileName' = "TestResults";
|
||||
'DarkMode' = $false;
|
||||
}
|
||||
}
|
||||
It 'Creates a report for Azure Active Directory' {
|
||||
$ProductName = 'aad'
|
||||
$CreateReportParams += @{
|
||||
'BaselineName' = $ArgToProd[$ProductName];
|
||||
'FullName' = $ProdToFullName[$ProductName];
|
||||
}
|
||||
New-Report @CreateReportParams
|
||||
Test-Path -Path "$($IndividualReportPath)/$($ArgToProd[$ProductName])Report.html" -PathType leaf | Should -Be $true
|
||||
}
|
||||
It 'Creates a report for Microsoft Defender for Office 365' {
|
||||
$ProductName = 'defender'
|
||||
$CreateReportParams += @{
|
||||
'BaselineName' = $ArgToProd[$ProductName];
|
||||
'FullName' = $ProdToFullName[$ProductName];
|
||||
}
|
||||
New-Report @CreateReportParams
|
||||
Test-Path -Path "$($IndividualReportPath)/$($ArgToProd[$ProductName])Report.html" -PathType leaf | Should -Be $true
|
||||
}
|
||||
It 'Creates a report for Exchange Online' {
|
||||
$ProductName = 'exo'
|
||||
$CreateReportParams += @{
|
||||
'BaselineName' = $ArgToProd[$ProductName];
|
||||
'FullName' = $ProdToFullName[$ProductName];
|
||||
}
|
||||
New-Report @CreateReportParams
|
||||
Test-Path -Path "$($IndividualReportPath)/$($ArgToProd[$ProductName])Report.html" -PathType leaf | Should -Be $true
|
||||
}
|
||||
It 'Creates a report for One Drive for Business' {
|
||||
$ProductName = 'onedrive'
|
||||
$CreateReportParams += @{
|
||||
'BaselineName' = $ArgToProd[$ProductName];
|
||||
'FullName' = $ProdToFullName[$ProductName];
|
||||
}
|
||||
New-Report @CreateReportParams
|
||||
Test-Path -Path "$($IndividualReportPath)/$($ArgToProd[$ProductName])Report.html" -PathType leaf | Should -Be $true
|
||||
}
|
||||
It 'Creates a report for Power Platform' {
|
||||
$ProductName = 'powerplatform'
|
||||
$CreateReportParams += @{
|
||||
'BaselineName' = $ArgToProd[$ProductName];
|
||||
'FullName' = $ProdToFullName[$ProductName];
|
||||
}
|
||||
New-Report @CreateReportParams
|
||||
Test-Path -Path "$($IndividualReportPath)/$($ArgToProd[$ProductName])Report.html" -PathType leaf | Should -Be $true
|
||||
}
|
||||
It 'Creates a report for SharePoint Online' {
|
||||
$ProductName = 'sharepoint'
|
||||
$CreateReportParams += @{
|
||||
'BaselineName' = $ArgToProd[$ProductName];
|
||||
'FullName' = $ProdToFullName[$ProductName];
|
||||
}
|
||||
New-Report @CreateReportParams
|
||||
Test-Path -Path "$($IndividualReportPath)/$($ArgToProd[$ProductName])Report.html" -PathType leaf | Should -Be $true
|
||||
}
|
||||
It 'Creates a report for Microsoft Teams' {
|
||||
$ProductName = 'teams'
|
||||
$CreateReportParams += @{
|
||||
'BaselineName' = $ArgToProd[$ProductName];
|
||||
'FullName' = $ProdToFullName[$ProductName];
|
||||
}
|
||||
New-Report @CreateReportParams
|
||||
Test-Path -Path "$($IndividualReportPath)/$($ArgToProd[$ProductName])Report.html" -PathType leaf | Should -Be $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module CreateReport -ErrorAction SilentlyContinue
|
||||
Remove-Item -Recurse -Force -Path (Join-Path -Path $PSScriptRoot -ChildPath "./CreateReportStubs/CreateReportUnitFolder") -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function 'Connect-Tenant' -Force
|
||||
|
||||
InModuleScope Orchestrator {
|
||||
Describe -Tag 'Orchestrator' -Name 'Invoke-Connection' {
|
||||
Context 'When interactively connecting to commercial Endpoints' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ConnectParams')]
|
||||
$ConnectParams = @{
|
||||
LogIn = $true
|
||||
M365Environment = "commercial"
|
||||
BoundParameters = @{}
|
||||
}
|
||||
function Connect-Tenant {}
|
||||
Mock -ModuleName Orchestrator Connect-Tenant -MockWith {@()}
|
||||
}
|
||||
It 'With -ProductNames "aad", connects to Microsoft Graph' {
|
||||
$ConnectParams += @{
|
||||
ProductNames = 'aad'
|
||||
}
|
||||
Invoke-Connection @ConnectParams
|
||||
Should -Invoke -CommandName Connect-Tenant -Times 1 -Exactly
|
||||
$FailedAuthList.Length | Should -Be 0
|
||||
}
|
||||
It 'With -ProductNames "defender", connects to Microsoft Defender for Office 365' {
|
||||
$ConnectParams += @{
|
||||
ProductNames = 'defender'
|
||||
}
|
||||
$FailedAuthList = Invoke-Connection @ConnectParams
|
||||
$FailedAuthList.Length | Should -Be 0
|
||||
}
|
||||
It 'With -ProductNames "exo", connects to Exchange Online' {
|
||||
$ConnectParams += @{
|
||||
ProductNames = 'exo'
|
||||
}
|
||||
$FailedAuthList = Invoke-Connection @ConnectParams
|
||||
$FailedAuthList.Length | Should -Be 0
|
||||
}
|
||||
It 'With -ProductNames "onedrive", connects to One Drive for Business' {
|
||||
$ConnectParams += @{
|
||||
ProductNames = 'onedrive'
|
||||
}
|
||||
$FailedAuthList = Invoke-Connection @ConnectParams
|
||||
$FailedAuthList.Length | Should -Be 0
|
||||
}
|
||||
It 'With -ProductNames "powerplatform", connects to Power Platform' {
|
||||
$ConnectParams += @{
|
||||
ProductNames = 'powerplatform'
|
||||
}
|
||||
$FailedAuthList = Invoke-Connection @ConnectParams
|
||||
$FailedAuthList.Length | Should -Be 0
|
||||
}
|
||||
It 'With -ProductNames "sharepoint", connects to SharePoint Online' {
|
||||
$ConnectParams += @{
|
||||
ProductNames = 'sharepoint'
|
||||
}
|
||||
$FailedAuthList = Invoke-Connection @ConnectParams
|
||||
$FailedAuthList.Length | Should -Be 0
|
||||
}
|
||||
It 'With -ProductNames "teams", connects to Microsoft Teams' {
|
||||
$ConnectParams += @{
|
||||
ProductNames = 'teams'
|
||||
}
|
||||
$FailedAuthList = Invoke-Connection @ConnectParams
|
||||
$FailedAuthList.Length | Should -Be 0
|
||||
}
|
||||
It 'authenticates to all products' {
|
||||
$ConnectParams += @{
|
||||
ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams")
|
||||
}
|
||||
$FailedAuthList = Invoke-Connection @ConnectParams
|
||||
$FailedAuthList.Length | Should -Be 0
|
||||
}
|
||||
}
|
||||
Context 'When -Login $false' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ConnectParams')]
|
||||
$ConnectParams = @{
|
||||
LogIn = $false
|
||||
M365Environment = "gcc"
|
||||
BoundParameters = @{}
|
||||
}
|
||||
function Connect-Tenant {}
|
||||
Mock -ModuleName Orchestrator Connect-Tenant -MockWith {@()}
|
||||
}
|
||||
It 'does not authenticate' {
|
||||
$ConnectParams += @{
|
||||
ProductNames = 'aad'
|
||||
}
|
||||
Invoke-Connection @ConnectParams
|
||||
Should -Invoke -CommandName Connect-Tenant -Times 0 -Exactly
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function 'Get-FileEncoding' -Force
|
||||
|
||||
Describe -Tag 'Orchestrator' -Name 'Get-FileEncoding' {
|
||||
InModuleScope Orchestrator {
|
||||
It 'Gets utf8 file encoding according to current PS version with no errors' {
|
||||
$PSVersion = $PSVersionTable.PSVersion
|
||||
if ($PSVersion -ge [System.Version]"6.0"){
|
||||
Get-FileEncoding | Should -Be 'utf8NoBom'
|
||||
}
|
||||
else{
|
||||
Get-FileEncoding | Should -Be 'utf8'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function 'Get-ServicePrincipalParams'
|
||||
|
||||
Describe -Tag 'Orchestrator' -Name 'Get-ServicePrincipalParams' {
|
||||
InModuleScope Orchestrator {
|
||||
It 'Returns a CertficateThumbprint PSObject with no errors' {
|
||||
$BoundParameters = @{
|
||||
CertificateThumbprint = 'WPOEALFN425A';
|
||||
AppID = '34289UFAHWFALL';
|
||||
Organization = 'example.onmicrosoft.com';
|
||||
}
|
||||
{Get-ServicePrincipalParams -BoundParameters $BoundParameters} | Should -Not -Throw
|
||||
}
|
||||
It 'Throws an error if no correct Service Principal Params are passed in' {
|
||||
$BoundParameters = @{
|
||||
a = 'a';
|
||||
}
|
||||
{Get-ServicePrincipalParams -BoundParameters $BoundParameters} | Should -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
259
Testing/Unit/PowerShell/Orchestrator/Get-TenantDetail.Tests.ps1
Normal file
259
Testing/Unit/PowerShell/Orchestrator/Get-TenantDetail.Tests.ps1
Normal file
@@ -0,0 +1,259 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function 'Get-TenantDetail'
|
||||
|
||||
InModuleScope Orchestrator {
|
||||
BeforeAll {
|
||||
function Get-AADTenantDetail {}
|
||||
Mock -ModuleName Orchestrator Get-AADTenantDetail {
|
||||
'{"DisplayName": "displayName"}'
|
||||
}
|
||||
function Get-TeamsTenantDetail {}
|
||||
Mock -ModuleName Orchestrator Get-TeamsTenantDetail {
|
||||
'{"DisplayName": "displayName"}'
|
||||
}
|
||||
function Get-PowerPlatformTenantDetail {}
|
||||
Mock -ModuleName Orchestrator Get-PowerPlatformTenantDetail {
|
||||
'{"DisplayName": "displayName"}'
|
||||
}
|
||||
function Get-EXOTenantDetail {}
|
||||
Mock -ModuleName Orchestrator Get-PowerPlatformTenantDetail {
|
||||
'{"DisplayName": "displayName"}'
|
||||
}
|
||||
function Test-SCuBAValidJson {
|
||||
param (
|
||||
[string]
|
||||
$Json
|
||||
)
|
||||
$ValidJson = $true
|
||||
try {
|
||||
ConvertFrom-Json $Json -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
$ValidJson = $false;
|
||||
}
|
||||
$ValidJson
|
||||
}
|
||||
}
|
||||
Describe -Tag 'Orchestrator' -Name 'Get-TenantDetail' {
|
||||
Context 'When connecting to commercial Endpoints' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'M365Environment')]
|
||||
$M365Environment = 'commercial'
|
||||
}
|
||||
It 'With -ProductNames "aad", returns valid JSON' {
|
||||
$ProductNames = @('aad')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "exo", returns valid JSON' {
|
||||
$ProductNames = @('exo')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "defender", returns valid JSON' {
|
||||
$ProductNames = @('defender')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "onedrive", returns valid JSON' {
|
||||
$ProductNames = @('onedrive')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "powerplatform", returns valid JSON' {
|
||||
$ProductNames = @('powerplatform')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "sharepoint", returns valid JSON' {
|
||||
$ProductNames = @('sharepoint')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "teams", returns valid JSON' {
|
||||
$ProductNames = @('teams')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With all products, returns valid JSON' {
|
||||
$ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams")
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
}
|
||||
Context 'When connecting to GCC Endpoints' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'M365Environment')]
|
||||
$M365Environment = 'gcc'
|
||||
}
|
||||
It 'With -ProductNames "aad", returns valid JSON' {
|
||||
$ProductNames = @('aad')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "exo", returns valid JSON' {
|
||||
$ProductNames = @('exo')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "defender", returns valid JSON' {
|
||||
$ProductNames = @('defender')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "onedrive", returns valid JSON' {
|
||||
$ProductNames = @('onedrive')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "powerplatform", returns valid JSON' {
|
||||
$ProductNames = @('powerplatform')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "sharepoint", returns valid JSON' {
|
||||
$ProductNames = @('sharepoint')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "teams", returns valid JSON' {
|
||||
$ProductNames = @('teams')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With all products, returns valid JSON' {
|
||||
$ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams")
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
}
|
||||
Context 'When connecting to GCC High Endpoints' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'M365Environment')]
|
||||
$M365Environment = 'gcchigh'
|
||||
}
|
||||
It 'With -ProductNames "aad", returns valid JSON' {
|
||||
$ProductNames = @('aad')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "exo", returns valid JSON' {
|
||||
$ProductNames = @('exo')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "defender", returns valid JSON' {
|
||||
$ProductNames = @('defender')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "onedrive", returns valid JSON' {
|
||||
$ProductNames = @('onedrive')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "powerplatform", returns valid JSON' {
|
||||
$ProductNames = @('powerplatform')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "sharepoint", returns valid JSON' {
|
||||
$ProductNames = @('sharepoint')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "teams", returns valid JSON' {
|
||||
$ProductNames = @('teams')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With all products, returns valid JSON' {
|
||||
$ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams")
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
}
|
||||
Context 'When connecting to DOD Endpoints' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'M365Environment')]
|
||||
$M365Environment = 'dod'
|
||||
}
|
||||
It 'With -ProductNames "aad", returns valid JSON' {
|
||||
$ProductNames = @('aad')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "exo", returns valid JSON' {
|
||||
$ProductNames = @('exo')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "defender", returns valid JSON' {
|
||||
$ProductNames = @('defender')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "onedrive", returns valid JSON' {
|
||||
$ProductNames = @('onedrive')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "powerplatform", returns valid JSON' {
|
||||
$ProductNames = @('powerplatform')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "sharepoint", returns valid JSON' {
|
||||
$ProductNames = @('sharepoint')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With -ProductNames "teams", returns valid JSON' {
|
||||
$ProductNames = @('teams')
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It 'With all products, returns valid JSON' {
|
||||
$ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams")
|
||||
$Json = Get-TenantDetail -M365Environment $M365Environment -ProductNames $ProductNames
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function Import-Resources
|
||||
|
||||
Describe -Tag 'Orchestrator' -Name 'Import-Resources' {
|
||||
InModuleScope Orchestrator {
|
||||
It 'Imports all helper functions with no errors' {
|
||||
{Import-Resources} | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function 'Invoke-ProviderList' -Force
|
||||
|
||||
InModuleScope Orchestrator {
|
||||
Describe -Tag 'Orchestrator' -Name 'Invoke-ProviderList' {
|
||||
BeforeAll {
|
||||
function Export-AADProvider {}
|
||||
Mock -ModuleName Orchestrator Export-AADProvider {}
|
||||
function Export-EXOProvider {}
|
||||
Mock -ModuleName Orchestrator Export-EXOProvider {}
|
||||
function Export-DefenderProvider {}
|
||||
Mock -ModuleName Orchestrator Export-DefenderProvider {}
|
||||
function Export-PowerPlatformProvider {}
|
||||
Mock -ModuleName Orchestrator Export-PowerPlatformProvider {}
|
||||
function Export-OneDriveProvider {}
|
||||
Mock -ModuleName Orchestrator Export-OneDriveProvider {}
|
||||
function Export-SharePointProvider {}
|
||||
Mock -ModuleName Orchestrator Export-SharePointProvider {}
|
||||
function Export-TeamsProvider {}
|
||||
Mock -ModuleName Orchestrator Export-TeamsProvider {}
|
||||
function Get-FileEncoding {}
|
||||
Mock -ModuleName Orchestrator Get-FileEncoding {}
|
||||
|
||||
Mock -CommandName Write-Progress {}
|
||||
Mock -CommandName Join-Path {"."}
|
||||
Mock -CommandName Set-Content {}
|
||||
Mock -CommandName Get-TimeZone {}
|
||||
}
|
||||
Context 'When running the providers on commercial tenants' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ProviderParameters')]
|
||||
$ProviderParameters = @{
|
||||
OutFolderPath = "./output";
|
||||
OutProviderFileName = "ProviderSettingsExport";
|
||||
M365Environment = "commercial";
|
||||
TenantDetails = '{"DisplayName": "displayName"}';
|
||||
ModuleVersion = '1.0';
|
||||
BoundParameters = @{};
|
||||
}
|
||||
}
|
||||
It 'With -ProductNames "aad", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("aad")
|
||||
}
|
||||
{Invoke-ProviderList @ProviderParameters} | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "defender", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("defender")
|
||||
}
|
||||
{Invoke-ProviderList @ProviderParameters} | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "exo", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("exo")
|
||||
}
|
||||
{Invoke-ProviderList @ProviderParameters} | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "onedrive", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("onedrive")
|
||||
}
|
||||
{Invoke-ProviderList @ProviderParameters} | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "powerplatform", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("powerplatform")
|
||||
}
|
||||
{Invoke-ProviderList @ProviderParameters} | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "sharepoint", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("sharepoint")
|
||||
}
|
||||
{Invoke-ProviderList @ProviderParameters} | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "teams", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("teams")
|
||||
}
|
||||
{Invoke-ProviderList @ProviderParameters} | Should -Not -Throw
|
||||
}
|
||||
It 'With all products, should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams")
|
||||
}
|
||||
{Invoke-ProviderList @ProviderParameters} | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function 'Invoke-ReportCreation' -Force
|
||||
|
||||
InModuleScope Orchestrator {
|
||||
Describe -Tag 'Orchestrator' -Name 'Invoke-ReportCreation' {
|
||||
BeforeAll {
|
||||
function New-Report {}
|
||||
Mock -ModuleName Orchestrator New-Report {}
|
||||
function Pluralize {}
|
||||
Mock -ModuleName Orchestrator Pluralize {}
|
||||
|
||||
Mock -CommandName Write-Progress {}
|
||||
Mock -CommandName Join-Path { "." }
|
||||
Mock -CommandName Out-File {}
|
||||
Mock -CommandName ConvertTo-Html {}
|
||||
Mock -CommandName Copy-Item {}
|
||||
Mock -CommandName Get-Content {}
|
||||
Mock -CommandName Add-Type {}
|
||||
Mock -CommandName Invoke-Item {}
|
||||
|
||||
}
|
||||
Context 'When creating the reports from Provider and OPA results JSON' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ProviderParameters')]
|
||||
$ProviderParameters = @{
|
||||
TenantDetails = '{"DisplayName": "displayName"}';
|
||||
DarkMode = $false;
|
||||
ModuleVersion = '1.0';
|
||||
OutFolderPath = "./"
|
||||
OutProviderFileName = "ProviderSettingsExport"
|
||||
OutRegoFileName = "TestResults"
|
||||
OutReportName = "BaselineReports"
|
||||
}
|
||||
}
|
||||
It 'With -ProductNames "aad", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("aad")
|
||||
}
|
||||
{ Invoke-ReportCreation @ProviderParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "defender", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("defender")
|
||||
}
|
||||
{ Invoke-ReportCreation @ProviderParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "exo", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("exo")
|
||||
}
|
||||
{ Invoke-ReportCreation @ProviderParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "onedrive", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("onedrive")
|
||||
}
|
||||
{ Invoke-ReportCreation @ProviderParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "powerplatform", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("powerplatform")
|
||||
}
|
||||
{ Invoke-ReportCreation @ProviderParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "sharepoint", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("sharepoint")
|
||||
}
|
||||
{ Invoke-ReportCreation @ProviderParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "teams", should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("teams")
|
||||
}
|
||||
{ Invoke-ReportCreation @ProviderParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With all products, should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams")
|
||||
}
|
||||
{ Invoke-ReportCreation @ProviderParameters } | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
Context 'When creating the reports with -Quiet True' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'ProviderParameters')]
|
||||
$ProviderParameters = @{
|
||||
DarkMode = $false;
|
||||
TenantDetails = '{"DisplayName": "displayName"}';
|
||||
ModuleVersion = '1.0';
|
||||
OutFolderPath = "./"
|
||||
OutProviderFileName = "ProviderSettingsExport"
|
||||
OutRegoFileName = "TestResults"
|
||||
OutReportName = "BaselineReports"
|
||||
Quiet = $true
|
||||
}
|
||||
}
|
||||
It 'With all products, should not throw' {
|
||||
$ProviderParameters += @{
|
||||
ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams")
|
||||
}
|
||||
{ Invoke-ReportCreation @ProviderParameters } | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
138
Testing/Unit/PowerShell/Orchestrator/Invoke-RunCached.Tests.ps1
Normal file
138
Testing/Unit/PowerShell/Orchestrator/Invoke-RunCached.Tests.ps1
Normal file
@@ -0,0 +1,138 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function 'Invoke-RunCached' -Force
|
||||
|
||||
InModuleScope Orchestrator {
|
||||
Describe -Tag 'Orchestrator' -Name 'Invoke-RunCached' {
|
||||
BeforeAll {
|
||||
function Remove-Resources {}
|
||||
Mock -ModuleName Orchestrator Remove-Resources {}
|
||||
function Import-Resources {}
|
||||
Mock -ModuleName Orchestrator Import-Resources {}
|
||||
function Invoke-Connection {}
|
||||
Mock -ModuleName Orchestrator Invoke-Connection { @() }
|
||||
function Get-TenantDetail {}
|
||||
Mock -ModuleName Orchestrator Get-TenantDetail { '{"DisplayName": "displayName"}' }
|
||||
function Invoke-ProviderList {}
|
||||
Mock -ModuleName Orchestrator Invoke-ProviderList {}
|
||||
function Invoke-RunRego {}
|
||||
Mock -ModuleName Orchestrator Invoke-RunRego {}
|
||||
function Invoke-ReportCreation {}
|
||||
Mock -ModuleName Orchestrator Invoke-ReportCreation {}
|
||||
function Disconnect-SCuBATenant {}
|
||||
Mock -ModuleName Orchestrator Disconnect-SCuBATenant
|
||||
|
||||
Mock -CommandName New-Item {}
|
||||
Mock -CommandName Get-Content {}
|
||||
}
|
||||
Context 'When checking the conformance of commercial tenants' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'SplatParams')]
|
||||
$SplatParams = @{
|
||||
M365Environment = 'commercial'
|
||||
}
|
||||
}
|
||||
It 'Given -ProductNames aad should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("aad")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames defender should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("defender")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames exo should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("exo")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames onedrive should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("onedrive")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames powerplatform should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("powerplatform")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames teams should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("teams")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames * should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("*")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
Context 'When omitting the export of the commercial tenant provider json' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'SplatParams')]
|
||||
$SplatParams = @{
|
||||
M365Environment = 'commercial'
|
||||
ExportProvider = $false
|
||||
}
|
||||
}
|
||||
It 'Given -ProductNames aad should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("aad")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames defender should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("defender")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames exo should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("exo")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames onedrive should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("onedrive")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames powerplatform should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("powerplatform")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames teams should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("teams")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames * should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("*")
|
||||
}
|
||||
{Invoke-RunCached @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
Context 'When checking module version' {
|
||||
It 'Given -Version should not throw' {
|
||||
{Invoke-RunCached -Version} | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function 'Invoke-RunRego' -Force
|
||||
|
||||
InModuleScope Orchestrator {
|
||||
Describe -Tag 'Orchestrator' -Name 'Invoke-RunRego' {
|
||||
BeforeAll {
|
||||
function Invoke-Rego {}
|
||||
Mock -ModuleName Orchestrator Invoke-Rego
|
||||
function Get-FileEncoding {}
|
||||
Mock -ModuleName Orchestrator Get-FileEncoding
|
||||
|
||||
Mock -CommandName Write-Progress {}
|
||||
Mock -CommandName Join-Path { "." }
|
||||
Mock -CommandName Set-Content {}
|
||||
Mock -CommandName ConvertTo-Json {}
|
||||
Mock -CommandName ConvertTo-Csv {}
|
||||
}
|
||||
Context 'When running the rego on a provider json' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'RunRegoParameters')]
|
||||
$RunRegoParameters = @{
|
||||
OPAPath = "./"
|
||||
ParentPath = "./"
|
||||
OutFolderPath = "./"
|
||||
OutProviderFileName = "ProviderSettingsExport"
|
||||
OutRegoFileName = "TestResults"
|
||||
}
|
||||
}
|
||||
It 'With -ProductNames "aad", should not throw' {
|
||||
$RunRegoParameters += @{
|
||||
ProductNames = @("aad")
|
||||
}
|
||||
{ Invoke-RunRego @RunRegoParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "defender", should not throw' {
|
||||
$RunRegoParameters += @{
|
||||
ProductNames = @("defender")
|
||||
}
|
||||
{ Invoke-RunRego @RunRegoParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "exo", should not throw' {
|
||||
$RunRegoParameters += @{
|
||||
ProductNames = @("exo")
|
||||
}
|
||||
{ Invoke-RunRego @RunRegoParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "onedrive", should not throw' {
|
||||
$RunRegoParameters += @{
|
||||
ProductNames = @("onedrive")
|
||||
}
|
||||
{ Invoke-RunRego @RunRegoParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "powerplatform", should not throw' {
|
||||
$RunRegoParameters += @{
|
||||
ProductNames = @("powerplatform")
|
||||
}
|
||||
{ Invoke-RunRego @RunRegoParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "sharepoint", should not throw' {
|
||||
$RunRegoParameters += @{
|
||||
ProductNames = @("sharepoint")
|
||||
}
|
||||
{ Invoke-RunRego @RunRegoParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With -ProductNames "teams", should not throw' {
|
||||
$RunRegoParameters += @{
|
||||
ProductNames = @("teams")
|
||||
}
|
||||
{ Invoke-RunRego @RunRegoParameters } | Should -Not -Throw
|
||||
}
|
||||
It 'With all products, should not throw' {
|
||||
$RunRegoParameters += @{
|
||||
ProductNames = @("aad", "defender", "exo", "onedrive", "powerplatform", "sharepoint", "teams")
|
||||
}
|
||||
{ Invoke-RunRego @RunRegoParameters } | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
93
Testing/Unit/PowerShell/Orchestrator/Invoke-Scuba.Tests.ps1
Normal file
93
Testing/Unit/PowerShell/Orchestrator/Invoke-Scuba.Tests.ps1
Normal file
@@ -0,0 +1,93 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function 'Invoke-SCuBA' -Force
|
||||
|
||||
InModuleScope Orchestrator {
|
||||
Describe -Tag 'Orchestrator' -Name 'Invoke-Scuba' {
|
||||
BeforeAll {
|
||||
function Remove-Resources {}
|
||||
Mock -ModuleName Orchestrator Remove-Resources {}
|
||||
function Import-Resources {}
|
||||
Mock -ModuleName Orchestrator Import-Resources {}
|
||||
function Invoke-Connection {}
|
||||
Mock -ModuleName Orchestrator Invoke-Connection { @() }
|
||||
function Get-TenantDetail {}
|
||||
Mock -ModuleName Orchestrator Get-TenantDetail { '{"DisplayName": "displayName"}' }
|
||||
function Invoke-ProviderList {}
|
||||
Mock -ModuleName Orchestrator Invoke-ProviderList {}
|
||||
function Invoke-RunRego {}
|
||||
Mock -ModuleName Orchestrator Invoke-RunRego {}
|
||||
function Invoke-ReportCreation {}
|
||||
Mock -ModuleName Orchestrator Invoke-ReportCreation {}
|
||||
function Disconnect-SCuBATenant {}
|
||||
Mock -ModuleName Orchestrator Disconnect-SCuBATenant {}
|
||||
|
||||
Mock -CommandName New-Item {}
|
||||
Mock -CommandName Copy-Item {}
|
||||
}
|
||||
Context 'When checking the conformance of commercial tenants' {
|
||||
BeforeAll {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'SplatParams')]
|
||||
$SplatParams = @{
|
||||
M365Environment = 'commercial';
|
||||
}
|
||||
}
|
||||
It 'Given -ProductNames aad should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("aad")
|
||||
}
|
||||
{Invoke-Scuba @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames defender should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("defender")
|
||||
}
|
||||
{Invoke-Scuba @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames exo should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("exo")
|
||||
}
|
||||
{Invoke-Scuba @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames onedrive should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("onedrive")
|
||||
}
|
||||
{Invoke-Scuba @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames powerplatform should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("powerplatform")
|
||||
}
|
||||
{Invoke-Scuba @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames teams should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("teams")
|
||||
}
|
||||
{Invoke-Scuba @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames * should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("*")
|
||||
}
|
||||
{Invoke-Scuba @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
It 'Given -ProductNames * and -DisconnectOnExit should not throw' {
|
||||
$SplatParams += @{
|
||||
ProductNames = @("*")
|
||||
DisconnectOnExit = $true
|
||||
}
|
||||
{Invoke-Scuba @SplatParams} | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
Context 'When checking module version' {
|
||||
It 'Given -Version should not throw' {
|
||||
{Invoke-Scuba -Version} | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
17
Testing/Unit/PowerShell/Orchestrator/Pluralize-Tests.ps1
Normal file
17
Testing/Unit/PowerShell/Orchestrator/Pluralize-Tests.ps1
Normal file
@@ -0,0 +1,17 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function 'Pluralize'
|
||||
|
||||
Describe -Tag 'Orchestrator' -Name 'Pluralize' {
|
||||
InModuleScope Orchestrator {
|
||||
It 'Chooses the plural noun' {
|
||||
(Pluralize -SingularNoun "warning" -PluralNoun "warnings" -Count 2) | Should -eq "warnings"
|
||||
}
|
||||
It 'Chooses the singular noun' {
|
||||
(Pluralize -SingularNoun "warning" -PluralNoun "warnings" -Count 1) | Should -eq "warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
$OrchestratorPath = '../../../../PowerShell/ScubaGear/Modules/Orchestrator.psm1'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath $OrchestratorPath) -Function Remove-Resources
|
||||
|
||||
Describe -Tag 'Orchestrator' -Name 'Remove-Resources' {
|
||||
InModuleScope Orchestrator {
|
||||
It 'Removes all helper modules with no errors' {
|
||||
{Remove-Resources} | Should -Not -Throw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module Orchestrator -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"GrantControls":{
|
||||
"AuthenticationStrength":{
|
||||
"AllowedCombinations":null,
|
||||
"CombinationConfigurations":null,
|
||||
"CreatedDateTime":null,
|
||||
"Description":null,
|
||||
"DisplayName":null,
|
||||
"Id":null,
|
||||
"ModifiedDateTime":null,
|
||||
"PolicyType":null,
|
||||
"RequirementsSatisfied":null
|
||||
},
|
||||
"BuiltInControls":[
|
||||
|
||||
],
|
||||
"CustomAuthenticationFactors":[
|
||||
|
||||
],
|
||||
"Operator":"OR",
|
||||
"TermsOfUse":[
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"Conditions": {
|
||||
"Applications":{
|
||||
"ApplicationFilter":{
|
||||
"Mode":null,
|
||||
"Rule":null
|
||||
},
|
||||
"ExcludeApplications":[
|
||||
|
||||
],
|
||||
"IncludeApplications":[
|
||||
|
||||
],
|
||||
"IncludeAuthenticationContextClassReferences":[
|
||||
|
||||
],
|
||||
"IncludeUserActions":[
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"Conditions":{
|
||||
"UserRiskLevels":[
|
||||
|
||||
],
|
||||
"SignInRiskLevels":[
|
||||
|
||||
],
|
||||
"Platforms":{
|
||||
"ExcludePlatforms":null,
|
||||
"IncludePlatforms":null
|
||||
},
|
||||
"Locations":{
|
||||
"ExcludeLocations":null,
|
||||
"IncludeLocations":null
|
||||
},
|
||||
"ClientAppTypes":[
|
||||
"all"
|
||||
],
|
||||
"Devices":{
|
||||
"DeviceFilter":{
|
||||
"Mode":null,
|
||||
"Rule":null
|
||||
},
|
||||
"ExcludeDeviceStates":null,
|
||||
"ExcludeDevices":null,
|
||||
"IncludeDeviceStates":null,
|
||||
"IncludeDevices":null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"SessionControls":{
|
||||
"ApplicationEnforcedRestrictions":{
|
||||
"IsEnabled":null
|
||||
},
|
||||
"CloudAppSecurity":{
|
||||
"CloudAppSecurityType":null,
|
||||
"IsEnabled":null
|
||||
},
|
||||
"ContinuousAccessEvaluation":{
|
||||
"Mode":null
|
||||
},
|
||||
"DisableResilienceDefaults":null,
|
||||
"PersistentBrowser":{
|
||||
"IsEnabled":null,
|
||||
"Mode":null
|
||||
},
|
||||
"SignInFrequency":{
|
||||
"AuthenticationType":null,
|
||||
"FrequencyInterval":null,
|
||||
"IsEnabled":null,
|
||||
"Type":null,
|
||||
"Value":null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"Conditions": {
|
||||
"Users":{
|
||||
"ExcludeGroups":[
|
||||
|
||||
],
|
||||
"ExcludeGuestsOrExternalUsers":{
|
||||
"ExternalTenants":{
|
||||
"MembershipKind":null
|
||||
},
|
||||
"GuestOrExternalUserTypes":null
|
||||
},
|
||||
"ExcludeRoles":[
|
||||
|
||||
],
|
||||
"ExcludeUsers":[
|
||||
|
||||
],
|
||||
"IncludeGroups":[
|
||||
|
||||
],
|
||||
"IncludeGuestsOrExternalUsers":{
|
||||
"ExternalTenants":{
|
||||
"MembershipKind":null
|
||||
},
|
||||
"GuestOrExternalUserTypes":null
|
||||
},
|
||||
"IncludeRoles":[
|
||||
|
||||
],
|
||||
"IncludeUsers":[
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<#
|
||||
# Due to how the Error handling was implemented, mocked API calls have to be mocked inside a
|
||||
# mocked CommandTracker class
|
||||
#>
|
||||
|
||||
$ProviderPath = '../../../../../PowerShell/ScubaGear/Modules/Providers'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportAADProvider.psm1") -Function Export-AADProvider -Force
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ProviderHelpers/CommandTracker.psm1") -Force
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ProviderHelpers/AADConditionalAccessHelper.psm1") -Force
|
||||
|
||||
InModuleScope -ModuleName ExportAADProvider {
|
||||
Describe -Tag 'ExportAADProvider' -Name "Export-AADProvider" {
|
||||
BeforeAll {
|
||||
class MockCommandTracker {
|
||||
[string[]]$SuccessfulCommands = @()
|
||||
[string[]]$UnSuccessfulCommands = @()
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command, [hashtable]$CommandArgs) {
|
||||
# This is where you decide where you mock functions called by CommandTracker :)
|
||||
try {
|
||||
switch ($Command) {
|
||||
"Get-MgIdentityConditionalAccessPolicy" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-MgSubscribedSku" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{
|
||||
ServicePlans = @(
|
||||
@{
|
||||
ProvisioningStatus = 'Success'
|
||||
}
|
||||
)
|
||||
ServicePlanName = 'AAD_PREMIUM_P2'
|
||||
}
|
||||
}
|
||||
"Get-PrivilegedUser" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-PrivilegedRole" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-MgPolicyAuthorizationPolicy" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-MgDirectorySetting" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-MgPolicyAdminConsentRequestPolicy" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
default {
|
||||
throw "ERROR you forgot to create a mock method for this cmdlet: $($Command)"
|
||||
}
|
||||
}
|
||||
$Result = @()
|
||||
$this.SuccessfulCommands += $Command
|
||||
return $Result
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error running $($Command). $($_)"
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
$Result = @()
|
||||
return $Result
|
||||
}
|
||||
}
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command) {
|
||||
return $this.TryCommand($Command, @{})
|
||||
}
|
||||
|
||||
[void] AddSuccessfulCommand([string]$Command) {
|
||||
$this.SuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[void] AddUnSuccessfulCommand([string]$Command) {
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[string[]] GetUnSuccessfulCommands() {
|
||||
return $this.UnSuccessfulCommands
|
||||
}
|
||||
|
||||
[string[]] GetSuccessfulCommands() {
|
||||
return $this.SuccessfulCommands
|
||||
}
|
||||
}
|
||||
function Get-CommandTracker {}
|
||||
Mock -ModuleName 'ExportAADProvider' Get-CommandTracker {
|
||||
return [MockCommandTracker]::New()
|
||||
}
|
||||
|
||||
class MockCapTracker {
|
||||
[string] ExportCapPolicies([System.Object]$Caps) {
|
||||
return "[]"
|
||||
}
|
||||
}
|
||||
function Get-CapTracker {}
|
||||
Mock -ModuleName 'ExportAADProvider' Get-CapTracker -MockWith {
|
||||
return [MockCapTracker]::New()
|
||||
}
|
||||
|
||||
function Test-SCuBAValidProviderJson {
|
||||
param (
|
||||
[string]
|
||||
$Json
|
||||
)
|
||||
$Json = $Json.TrimEnd(",")
|
||||
$Json = "{$($Json)}"
|
||||
$ValidJson = $true
|
||||
try {
|
||||
ConvertFrom-Json $Json -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
$ValidJson = $false;
|
||||
}
|
||||
$ValidJson
|
||||
}
|
||||
}
|
||||
It "With a AAD P2 license, returns valid JSON" {
|
||||
$Json = Export-AADProvider
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
}
|
||||
}
|
||||
AfterAll {
|
||||
Remove-Module ExportAADProvider -Force -ErrorAction 'SilentlyContinue'
|
||||
Remove-Module CommandTracker -Force -ErrorAction 'SilentlyContinue'
|
||||
Remove-Module AADConditionalAccessHelper -Force -ErrorAction 'SilentlyContinue'
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
BeforeAll {
|
||||
$ClassPath = (Join-Path -Path $PSScriptRoot -ChildPath "./../../../../../PowerShell/ScubaGear/Modules/Providers/ProviderHelpers/AADConditionalAccessHelper.psm1")
|
||||
Import-Module $ClassPath
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'CapHelper')]
|
||||
$CapHelper = Get-CapTracker
|
||||
}
|
||||
|
||||
Describe "GetIncludedUsers" {
|
||||
BeforeEach {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'Cap')]
|
||||
$Cap = Get-Content (Join-Path -Path $PSScriptRoot -ChildPath "./CapSnippets/Users.json") | ConvertFrom-Json
|
||||
}
|
||||
It "returns 'None' when no users are included" {
|
||||
$Cap.Conditions.Users.IncludeUsers += "None"
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersIncluded | Should -Be "None"
|
||||
}
|
||||
|
||||
It "handles including single users" {
|
||||
$Cap.Conditions.Users.IncludeUsers += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersIncluded | Should -Be "1 specific user"
|
||||
}
|
||||
|
||||
It "handles including multiple users" {
|
||||
$Cap.Conditions.Users.IncludeUsers += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.IncludeUsers += "baaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersIncluded | Should -Be "2 specific users"
|
||||
}
|
||||
|
||||
It "handles including single groups" {
|
||||
$Cap.Conditions.Users.IncludeGroups += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersIncluded | Should -Be "1 specific group"
|
||||
}
|
||||
|
||||
It "handles including multiple groups" {
|
||||
$Cap.Conditions.Users.IncludeGroups += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.IncludeGroups += "baaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersIncluded | Should -Be "2 specific groups"
|
||||
}
|
||||
|
||||
It "handles including single roles" {
|
||||
$Cap.Conditions.Users.IncludeRoles += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersIncluded | Should -Be "1 specific role"
|
||||
}
|
||||
|
||||
It "handles including multiple roles" {
|
||||
$Cap.Conditions.Users.IncludeRoles += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.IncludeRoles += "baaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersIncluded | Should -Be "2 specific roles"
|
||||
}
|
||||
|
||||
It "handles including users, groups, and roles simultaneously" {
|
||||
$Cap.Conditions.Users.IncludeUsers += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.IncludeRoles += "baaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.IncludeRoles += "caaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.IncludeGroups += "daaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.IncludeGroups += "eaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.IncludeGroups += "faaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersIncluded | Should -Be "1 specific user, 2 specific roles, 3 specific groups"
|
||||
}
|
||||
|
||||
It "returns 'All' when all users are included" {
|
||||
$Cap.Conditions.Users.IncludeUsers += "all"
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersIncluded | Should -Be "All"
|
||||
}
|
||||
|
||||
It "handles including single type of external user" {
|
||||
$Cap.Conditions.Users.IncludeGuestsOrExternalUsers.ExternalTenants.MembershipKind = "all"
|
||||
$Cap.Conditions.Users.IncludeGuestsOrExternalUsers.GuestOrExternalUserTypes = "internalGuest"
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersIncluded | Should -Be "Local guest users"
|
||||
}
|
||||
|
||||
It "handles including all types of guest users" {
|
||||
$Cap.Conditions.Users.IncludeGuestsOrExternalUsers.ExternalTenants.MembershipKind = "all"
|
||||
$Cap.Conditions.Users.IncludeGuestsOrExternalUsers.GuestOrExternalUserTypes = "b2bCollaborationGuest,b2bCollaborationMember,b2bDirectConnectUser,internalGuest,serviceProvider,otherExternalUser"
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap)) -Join ", "
|
||||
$UsersIncluded | Should -Be "B2B collaboration guest users, B2B collaboration member users, B2B direct connect users, Local guest users, Service provider users, Other external users"
|
||||
}
|
||||
|
||||
It "handles empty input" {
|
||||
$Cap = @{}
|
||||
$UsersIncluded = $($CapHelper.GetIncludedUsers($Cap) 3>$null) -Join ", " # 3>$null to surpress the warning
|
||||
# message as it is expected in this case
|
||||
$UsersIncluded | Should -Be ""
|
||||
}
|
||||
}
|
||||
|
||||
Describe "GetExcludedUsers" {
|
||||
BeforeEach {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'Cap')]
|
||||
$Cap = Get-Content (Join-Path -Path $PSScriptRoot -ChildPath "./CapSnippets/Users.json") | ConvertFrom-Json
|
||||
}
|
||||
It "returns 'None' when no users are included" {
|
||||
$UsersExcluded = $($CapHelper.GetExcludedUsers($Cap)) -Join ", "
|
||||
$UsersExcluded | Should -Be "None"
|
||||
}
|
||||
|
||||
It "handles excluding single users" {
|
||||
$Cap.Conditions.Users.ExcludeUsers += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersExcluded = $($CapHelper.GetExcludedUsers($Cap)) -Join ", "
|
||||
$UsersExcluded | Should -Be "1 specific user"
|
||||
}
|
||||
|
||||
It "handles excluding multiple users" {
|
||||
$Cap.Conditions.Users.ExcludeUsers += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.ExcludeUsers += "baaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersExcluded = $($CapHelper.GetExcludedUsers($Cap)) -Join ", "
|
||||
$UsersExcluded | Should -Be "2 specific users"
|
||||
}
|
||||
|
||||
It "handles excluding single groups" {
|
||||
$Cap.Conditions.Users.ExcludeGroups += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersExcluded = $($CapHelper.GetExcludedUsers($Cap)) -Join ", "
|
||||
$UsersExcluded | Should -Be "1 specific group"
|
||||
}
|
||||
|
||||
It "handles excluding multiple groups" {
|
||||
$Cap.Conditions.Users.ExcludeGroups += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.ExcludeGroups += "baaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersExcluded = $($CapHelper.GetExcludedUsers($Cap)) -Join ", "
|
||||
$UsersExcluded | Should -Be "2 specific groups"
|
||||
}
|
||||
|
||||
It "handles excluding single roles" {
|
||||
$Cap.Conditions.Users.ExcludeRoles += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersExcluded = $($CapHelper.GetExcludedUsers($Cap)) -Join ", "
|
||||
$UsersExcluded | Should -Be "1 specific role"
|
||||
}
|
||||
|
||||
It "handles excluding multiple roles" {
|
||||
$Cap.Conditions.Users.ExcludeRoles += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.ExcludeRoles += "baaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersExcluded = $($CapHelper.GetExcludedUsers($Cap)) -Join ", "
|
||||
$UsersExcluded | Should -Be "2 specific roles"
|
||||
}
|
||||
|
||||
It "handles excluding users, groups, and roles simultaneously" {
|
||||
$Cap.Conditions.Users.ExcludeUsers += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.ExcludeRoles += "baaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.ExcludeRoles += "caaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.ExcludeGroups += "daaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.ExcludeGroups += "eaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Users.ExcludeGroups += "faaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$UsersExcluded = $($CapHelper.GetExcludedUsers($Cap)) -Join ", "
|
||||
$UsersExcluded | Should -Be "1 specific user, 2 specific roles, 3 specific groups"
|
||||
}
|
||||
|
||||
It "handles excluding all types of external users" {
|
||||
$Cap.Conditions.Users.ExcludeGuestsOrExternalUsers.ExternalTenants.MembershipKind = "all"
|
||||
$Cap.Conditions.Users.ExcludeGuestsOrExternalUsers.GuestOrExternalUserTypes = "b2bCollaborationGuest,b2bCollaborationMember,b2bDirectConnectUser,internalGuest,serviceProvider,otherExternalUser"
|
||||
$UsersExcluded = $($CapHelper.GetExcludedUsers($Cap)) -Join ", "
|
||||
$UsersExcluded | Should -Be "B2B collaboration guest users, B2B collaboration member users, B2B direct connect users, Local guest users, Service provider users, Other external users"
|
||||
}
|
||||
|
||||
It "handles excluding a single type of external user" {
|
||||
$Cap.Conditions.Users.ExcludeGuestsOrExternalUsers.ExternalTenants.MembershipKind = "all"
|
||||
$Cap.Conditions.Users.ExcludeGuestsOrExternalUsers.GuestOrExternalUserTypes = "serviceProvider"
|
||||
$UsersExcluded = $($CapHelper.GetExcludedUsers($Cap)) -Join ", "
|
||||
$UsersExcluded | Should -Be "Service provider users"
|
||||
}
|
||||
|
||||
It "handles empty input" {
|
||||
$Cap = @{}
|
||||
$UsersExcluded = $($CapHelper.GetExcludedUsers($Cap) 3>$null) -Join ", " # 3>$null to surpress the warning
|
||||
# message as it is expected in this case
|
||||
$UsersExcluded | Should -Be ""
|
||||
}
|
||||
}
|
||||
|
||||
Describe "GetApplications" {
|
||||
BeforeEach {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'Cap')]
|
||||
$Cap = Get-Content (Join-Path -Path $PSScriptRoot -ChildPath "./CapSnippets/Apps.json") | ConvertFrom-Json
|
||||
}
|
||||
It "handles including all apps" {
|
||||
$Cap.Conditions.Applications.IncludeApplications += "All"
|
||||
$Apps = $($CapHelper.GetApplications($Cap))
|
||||
$Apps[0] | Should -Be "Policy applies to: apps"
|
||||
$Apps[1] | Should -Be "Apps included: All"
|
||||
$Apps[2] | Should -Be "Apps excluded: None"
|
||||
}
|
||||
|
||||
It "handles including/excluding no apps" {
|
||||
$Cap.Conditions.Applications.IncludeApplications += "None"
|
||||
$Apps = $($CapHelper.GetApplications($Cap))
|
||||
$Apps[0] | Should -Be "Policy applies to: apps"
|
||||
$Apps[1] | Should -Be "Apps included: None"
|
||||
$Apps[2] | Should -Be "Apps excluded: None"
|
||||
}
|
||||
|
||||
It "handles including/excluding single specific apps" {
|
||||
$Cap.Conditions.Applications.IncludeApplications += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Applications.ExcludeApplications += "baaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Apps = $($CapHelper.GetApplications($Cap))
|
||||
$Apps[0] | Should -Be "Policy applies to: apps"
|
||||
$Apps[1] | Should -Be "Apps included: 1 specific app"
|
||||
$Apps[2] | Should -Be "Apps excluded: 1 specific app"
|
||||
}
|
||||
|
||||
It "handles including/excluding multiple specific apps" {
|
||||
$Cap.Conditions.Applications.IncludeApplications += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Applications.IncludeApplications += "baaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Applications.IncludeApplications += "caaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Applications.ExcludeApplications += "daaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Cap.Conditions.Applications.ExcludeApplications += "eaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Apps = $($CapHelper.GetApplications($Cap))
|
||||
$Apps[0] | Should -Be "Policy applies to: apps"
|
||||
$Apps[1] | Should -Be "Apps included: 3 specific apps"
|
||||
$Apps[2] | Should -Be "Apps excluded: 2 specific apps"
|
||||
}
|
||||
|
||||
It "handles app filter in include mode" {
|
||||
$Cap.Conditions.Applications.ApplicationFilter.Mode = "include"
|
||||
$Apps = $($CapHelper.GetApplications($Cap))
|
||||
$Apps[0] | Should -Be "Policy applies to: apps"
|
||||
$Apps[1] | Should -Be "Apps included: custom application filter"
|
||||
$Apps[2] | Should -Be "Apps excluded: None"
|
||||
}
|
||||
|
||||
It "handles app filter in exclude mode" {
|
||||
$Cap.Conditions.Applications.ApplicationFilter.Mode = "exclude"
|
||||
$Cap.Conditions.Applications.IncludeApplications += "All"
|
||||
$Apps = $($CapHelper.GetApplications($Cap))
|
||||
$Apps[0] | Should -Be "Policy applies to: apps"
|
||||
$Apps[1] | Should -Be "Apps included: All"
|
||||
$Apps[2] | Should -Be "Apps excluded: custom application filter"
|
||||
}
|
||||
|
||||
It "handles including app filter and specific apps" {
|
||||
$Cap.Conditions.Applications.ApplicationFilter.Mode = "include"
|
||||
$Cap.Conditions.Applications.IncludeApplications += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Apps = $($CapHelper.GetApplications($Cap))
|
||||
$Apps[0] | Should -Be "Policy applies to: apps"
|
||||
$Apps[1] | Should -Be "Apps included: 1 specific app"
|
||||
$Apps[2] | Should -Be "Apps included: custom application filter"
|
||||
$Apps[3] | Should -Be "Apps excluded: None"
|
||||
}
|
||||
|
||||
It "handles excluding app filter and specific apps" {
|
||||
$Cap.Conditions.Applications.ApplicationFilter.Mode = "exclude"
|
||||
$Cap.Conditions.Applications.IncludeApplications += "All"
|
||||
$Cap.Conditions.Applications.ExcludeApplications += "aaaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
|
||||
$Apps = $($CapHelper.GetApplications($Cap))
|
||||
$Apps[0] | Should -Be "Policy applies to: apps"
|
||||
$Apps[1] | Should -Be "Apps included: All"
|
||||
$Apps[2] | Should -Be "Apps excluded: 1 specific app"
|
||||
$Apps[3] | Should -Be "Apps excluded: custom application filter"
|
||||
}
|
||||
|
||||
It "handles registering a device" {
|
||||
$Cap.Conditions.Applications.IncludeUserActions += "urn:user:registerdevice"
|
||||
$Apps = $($CapHelper.GetApplications($Cap))
|
||||
$Apps[0] | Should -Be "Policy applies to: actions"
|
||||
$Apps[1] | Should -Be "User action: Register or join devices"
|
||||
}
|
||||
|
||||
It "handles registering security info" {
|
||||
$Cap.Conditions.Applications.IncludeUserActions += "urn:user:registersecurityinfo"
|
||||
$Apps = $($CapHelper.GetApplications($Cap))
|
||||
$Apps[0] | Should -Be "Policy applies to: actions"
|
||||
$Apps[1] | Should -Be "User action: Register security info"
|
||||
}
|
||||
|
||||
It "handles registering security info" {
|
||||
$Cap.Conditions.Applications.IncludeAuthenticationContextClassReferences += "c1"
|
||||
$Cap.Conditions.Applications.IncludeAuthenticationContextClassReferences += "c3"
|
||||
$Apps = $($CapHelper.GetApplications($Cap))
|
||||
$Apps | Should -Be "Policy applies to: 2 authentication contexts"
|
||||
}
|
||||
|
||||
It "handles empty input" {
|
||||
$Cap = @{}
|
||||
$Apps = $($CapHelper.GetApplications($Cap) 3>$null) -Join ", " # 3>$null to surpress the warning
|
||||
# message as it is expected in this case
|
||||
$Apps | Should -Be ""
|
||||
}
|
||||
}
|
||||
|
||||
Describe "GetConditions" {
|
||||
BeforeEach {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'Cap')]
|
||||
$Cap = Get-Content (Join-Path -Path $PSScriptRoot -ChildPath "./CapSnippets/Conditions.json") | ConvertFrom-Json
|
||||
}
|
||||
It "handles user risk levels" {
|
||||
$Cap.Conditions.UserRiskLevels += "high"
|
||||
$Cap.Conditions.UserRiskLevels += "medium"
|
||||
$Cap.Conditions.UserRiskLevels += "low"
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[0] | Should -Be "User risk levels: high, medium, low"
|
||||
}
|
||||
|
||||
It "handles sign-in risk levels" {
|
||||
$Cap.Conditions.SignInRiskLevels += "low"
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[0] | Should -Be "Sign-in risk levels: low"
|
||||
}
|
||||
|
||||
It "handles including all device platforms" {
|
||||
$Cap.Conditions.Platforms.ExcludePlatforms = @()
|
||||
$Cap.Conditions.Platforms.IncludePlatforms = @("all")
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[0] | Should -Be "Device platforms included: all"
|
||||
$Conditions[1] | Should -Be "Device platforms excluded: none"
|
||||
}
|
||||
|
||||
It "handles including/excluding specific device platforms" {
|
||||
$Cap.Conditions.Platforms.ExcludePlatforms = @("iOS", "macOS", "linux")
|
||||
$Cap.Conditions.Platforms.IncludePlatforms = @("android")
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[0] | Should -Be "Device platforms included: android"
|
||||
$Conditions[1] | Should -Be "Device platforms excluded: iOS, macOS, linux"
|
||||
}
|
||||
|
||||
It "handles including all locations" {
|
||||
$Cap.Conditions.Locations.ExcludeLocations = @()
|
||||
$Cap.Conditions.Locations.IncludeLocations = @("All")
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[0] | Should -Be "Locations included: all locations"
|
||||
$Conditions[1] | Should -Be "Locations excluded: none"
|
||||
}
|
||||
|
||||
It "handles excluding trusted locations" {
|
||||
$Cap.Conditions.Locations.ExcludeLocations = @("AllTrusted")
|
||||
$Cap.Conditions.Locations.IncludeLocations = @("All")
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[0] | Should -Be "Locations included: all locations"
|
||||
$Conditions[1] | Should -Be "Locations excluded: all trusted locations"
|
||||
}
|
||||
|
||||
It "handles including/excluding single custom locations" {
|
||||
$Cap.Conditions.Locations.ExcludeLocations = @("00000000-0000-0000-0000-000000000000")
|
||||
$Cap.Conditions.Locations.IncludeLocations = @("10000000-0000-0000-0000-000000000000")
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[0] | Should -Be "Locations included: 1 specific location"
|
||||
$Conditions[1] | Should -Be "Locations excluded: 1 specific location"
|
||||
}
|
||||
|
||||
It "handles including/excluding multiple custom locations" {
|
||||
$Cap.Conditions.Locations.ExcludeLocations = @()
|
||||
$Cap.Conditions.Locations.ExcludeLocations += @("00000000-0000-0000-0000-000000000000")
|
||||
$Cap.Conditions.Locations.ExcludeLocations += @("10000000-0000-0000-0000-000000000000")
|
||||
$Cap.Conditions.Locations.ExcludeLocations += @("20000000-0000-0000-0000-000000000000")
|
||||
$Cap.Conditions.Locations.IncludeLocations = @()
|
||||
$Cap.Conditions.Locations.IncludeLocations += @("30000000-0000-0000-0000-000000000000")
|
||||
$Cap.Conditions.Locations.IncludeLocations += @("40000000-0000-0000-0000-000000000000")
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[0] | Should -Be "Locations included: 2 specific locations"
|
||||
$Conditions[1] | Should -Be "Locations excluded: 3 specific locations"
|
||||
}
|
||||
|
||||
It "handles including trusted locations" {
|
||||
$Cap.Conditions.Locations.IncludeLocations = @("AllTrusted")
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[0] | Should -Be "Locations included: all trusted locations"
|
||||
$Conditions[1] | Should -Be "Locations excluded: none"
|
||||
}
|
||||
|
||||
It "handles including all client apps" {
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions | Should -Be "Client apps included: all"
|
||||
}
|
||||
|
||||
It "handles including specific client apps" {
|
||||
$Cap.Conditions.ClientAppTypes = @("exchangeActiveSync", "browser",
|
||||
"mobileAppsAndDesktopClients", "other")
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions | Should -Be "Client apps included: Exchange ActiveSync Clients, Browser, Mobile apps and desktop clients, Other clients"
|
||||
}
|
||||
|
||||
It "handles custom client app filter in include mode" {
|
||||
$Cap.Conditions.Devices.DeviceFilter.Mode = "include"
|
||||
$Cap.Conditions.Devices.DeviceFilter.Rule = "device.manufacturer -eq 'helloworld'"
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[1] | Should -Be "Custom device filter in include mode active"
|
||||
}
|
||||
|
||||
It "handles custom client app filter in exclude mode" {
|
||||
$Cap.Conditions.Devices.DeviceFilter.Mode = "exclude"
|
||||
$Cap.Conditions.Devices.DeviceFilter.Rule = "device.manufacturer -eq 'helloworld'"
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[1] | Should -Be "Custom device filter in exclude mode active"
|
||||
}
|
||||
|
||||
It "handles many conditions simultaneously" {
|
||||
$Cap.Conditions.UserRiskLevels += "low"
|
||||
$Cap.Conditions.SignInRiskLevels += "high"
|
||||
$Cap.Conditions.Platforms.ExcludePlatforms = @("android", "iOS", "macOS", "linux")
|
||||
$Cap.Conditions.Platforms.IncludePlatforms = @("all")
|
||||
$Cap.Conditions.Locations.IncludeLocations = @("AllTrusted")
|
||||
$Cap.Conditions.Locations.ExcludeLocations = @()
|
||||
$Cap.Conditions.ClientAppTypes = @("exchangeActiveSync")
|
||||
$Cap.Conditions.Devices.DeviceFilter.Mode = "exclude"
|
||||
$Cap.Conditions.Devices.DeviceFilter.Rule = "device.manufacturer -eq 'helloworld'"
|
||||
$Conditions = $($CapHelper.GetConditions($Cap))
|
||||
$Conditions[0] | Should -Be "User risk levels: low"
|
||||
$Conditions[1] | Should -Be "Sign-in risk levels: high"
|
||||
$Conditions[2] | Should -Be "Device platforms included: all"
|
||||
$Conditions[3] | Should -Be "Device platforms excluded: android, iOS, macOS, linux"
|
||||
$Conditions[4] | Should -Be "Locations included: all trusted locations"
|
||||
$Conditions[5] | Should -Be "Locations excluded: none"
|
||||
$Conditions[6] | Should -Be "Client apps included: Exchange ActiveSync Clients"
|
||||
$Conditions[7] | Should -Be "Custom device filter in exclude mode active"
|
||||
}
|
||||
|
||||
It "handles empty input" {
|
||||
$Cap = @{}
|
||||
$Conditions = $($CapHelper.GetConditions($Cap) 3>$null) -Join ", " # 3>$null to surpress the warning
|
||||
# message as it is expected in this case
|
||||
$Conditions | Should -Be ""
|
||||
}
|
||||
}
|
||||
|
||||
Describe "GetAccessControls" {
|
||||
BeforeEach {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'Cap')]
|
||||
$Cap = Get-Content (Join-Path -Path $PSScriptRoot -ChildPath "./CapSnippets/AccessControls.json") | ConvertFrom-Json
|
||||
}
|
||||
It "handles blocking access" {
|
||||
$Cap.GrantControls.BuiltInControls = @("block")
|
||||
$Controls = $($CapHelper.GetAccessControls($Cap))
|
||||
$Controls | Should -Be "Block access"
|
||||
}
|
||||
|
||||
It "handles requiring single control" {
|
||||
$Cap.GrantControls.BuiltInControls = @("mfa")
|
||||
$Controls = $($CapHelper.GetAccessControls($Cap))
|
||||
$Controls | Should -Be "Allow access but require multifactor authentication"
|
||||
}
|
||||
|
||||
It "handles requiring multiple controls in AND mode" {
|
||||
$Cap.GrantControls.BuiltInControls = @("mfa", "compliantDevice", "domainJoinedDevice",
|
||||
"approvedApplication", "compliantApplication", "passwordChange")
|
||||
$Cap.GrantControls.Operator = "AND"
|
||||
$Controls = $($CapHelper.GetAccessControls($Cap))
|
||||
$Controls | Should -Be "Allow access but require multifactor authentication, device to be marked compliant, Hybrid Azure AD joined device, approved client app, app protection policy, AND password change"
|
||||
}
|
||||
|
||||
It "handles requiring multiple controls in OR mode" {
|
||||
$Cap.GrantControls.BuiltInControls = @("mfa", "compliantDevice", "domainJoinedDevice",
|
||||
"approvedApplication", "compliantApplication", "passwordChange")
|
||||
$Cap.GrantControls.Operator = "OR"
|
||||
$Controls = $($CapHelper.GetAccessControls($Cap))
|
||||
$Controls | Should -Be "Allow access but require multifactor authentication, device to be marked compliant, Hybrid Azure AD joined device, approved client app, app protection policy, OR password change"
|
||||
}
|
||||
|
||||
It "handles using authentication strength (phishing resistant MFA)" {
|
||||
$Cap.GrantControls.AuthenticationStrength.DisplayName = "Phishing resistant MFA"
|
||||
$Controls = $($CapHelper.GetAccessControls($Cap))
|
||||
$Controls | Should -Be "Allow access but require authentication strength (Phishing resistant MFA)"
|
||||
}
|
||||
|
||||
It "handles using both authentication strength and a traditional control" {
|
||||
$Cap.GrantControls.AuthenticationStrength.DisplayName = "Multi-factor authentication"
|
||||
$Cap.GrantControls.BuiltInControls = @("passwordChange")
|
||||
$Cap.GrantControls.Operator = "AND"
|
||||
$Controls = $($CapHelper.GetAccessControls($Cap))
|
||||
$Controls | Should -Be "Allow access but require password change, AND authentication strength (Multi-factor authentication)"
|
||||
}
|
||||
|
||||
It "handles using no access controls" {
|
||||
$Cap.GrantControls.BuiltInControls = $null
|
||||
$Controls = $($CapHelper.GetAccessControls($Cap))
|
||||
$Controls | Should -Be "None"
|
||||
}
|
||||
|
||||
It "handles empty input" {
|
||||
$Cap = @{}
|
||||
$Controls = $($CapHelper.GetAccessControls($Cap) 3>$null) -Join ", " # 3>$null to surpress the warning
|
||||
# message as it is expected in this case
|
||||
$Controls | Should -Be ""
|
||||
}
|
||||
}
|
||||
|
||||
Describe "GetSessionControls" {
|
||||
BeforeEach {
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', 'Cap')]
|
||||
$Cap = Get-Content (Join-Path -Path $PSScriptRoot -ChildPath "./CapSnippets/SessionControls.json") | ConvertFrom-Json
|
||||
}
|
||||
It "handles using no session controls" {
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap))
|
||||
$Controls | Should -Be "None"
|
||||
}
|
||||
|
||||
It "handles using app enforced restrictions" {
|
||||
$Cap.SessionControls.ApplicationEnforcedRestrictions.IsEnabled = $true
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap))
|
||||
$Controls | Should -Be "Use app enforced restrictions"
|
||||
}
|
||||
|
||||
It "handles using conditional access app control with custom policy" {
|
||||
$Cap.SessionControls.CloudAppSecurity.CloudAppSecurityType = "mcasConfigured"
|
||||
$Cap.SessionControls.CloudAppSecurity.IsEnabled = $true
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap))
|
||||
$Controls | Should -Be "Use Conditional Access App Control (Use custom policy)"
|
||||
}
|
||||
|
||||
It "handles using conditional access app control in monitor mode" {
|
||||
$Cap.SessionControls.CloudAppSecurity.CloudAppSecurityType = "monitorOnly"
|
||||
$Cap.SessionControls.CloudAppSecurity.IsEnabled = $true
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap))
|
||||
$Controls | Should -Be "Use Conditional Access App Control (Monitor only)"
|
||||
}
|
||||
|
||||
It "handles using conditional access app control in block mode" {
|
||||
$Cap.SessionControls.CloudAppSecurity.CloudAppSecurityType = "blockDownloads"
|
||||
$Cap.SessionControls.CloudAppSecurity.IsEnabled = $true
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap))
|
||||
$Controls | Should -Be "Use Conditional Access App Control (Block downloads)"
|
||||
}
|
||||
|
||||
It "handles using sign-in frequency every time" {
|
||||
$Cap.SessionControls.SignInFrequency.FrequencyInterval = "everyTime"
|
||||
$Cap.SessionControls.SignInFrequency.IsEnabled = $true
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap))
|
||||
$Controls | Should -Be "Sign-in frequency (every time)"
|
||||
}
|
||||
|
||||
It "handles using sign-in frequency time based" {
|
||||
$Cap.SessionControls.SignInFrequency.FrequencyInterval = "timeBased"
|
||||
$Cap.SessionControls.SignInFrequency.Type = "days"
|
||||
$Cap.SessionControls.SignInFrequency.Value = 10
|
||||
$Cap.SessionControls.SignInFrequency.IsEnabled = $true
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap))
|
||||
$Controls | Should -Be "Sign-in frequency (every 10 days)"
|
||||
}
|
||||
|
||||
It "handles using persistent browser session" {
|
||||
$Cap.SessionControls.PersistentBrowser.IsEnabled = $true
|
||||
$Cap.SessionControls.PersistentBrowser.Mode = "never"
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap))
|
||||
$Controls | Should -Be "Persistent browser session (never persistent)"
|
||||
}
|
||||
|
||||
It "handles using customized continuous access evaluation" {
|
||||
$Cap.SessionControls.ContinuousAccessEvaluation.Mode = "disabled"
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap))
|
||||
$Controls | Should -Be "Customize continuous access evaluation"
|
||||
}
|
||||
|
||||
It "handles disabling resilience defaults" {
|
||||
$Cap.SessionControls.DisableResilienceDefaults = $true
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap))
|
||||
$Controls | Should -Be "Disable resilience defaults"
|
||||
}
|
||||
|
||||
It "handles multiple controls simultaneously" {
|
||||
$Cap.SessionControls.PersistentBrowser.IsEnabled = $true
|
||||
$Cap.SessionControls.PersistentBrowser.Mode = "never"
|
||||
$Cap.SessionControls.DisableResilienceDefaults = $true
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap))
|
||||
$Controls[0] | Should -Be "Persistent browser session (never persistent)"
|
||||
$Controls[1] | Should -Be "Disable resilience defaults"
|
||||
}
|
||||
|
||||
It "handles empty input" {
|
||||
$Cap = @{}
|
||||
$Controls = $($CapHelper.GetSessionControls($Cap) 3>$null) -Join ", " # 3>$null to surpress the warning
|
||||
# message as it is expected in this case
|
||||
$Controls | Should -Be ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
$ProviderPath = '../../../../../PowerShell/ScubaGear/Modules/Providers'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportAADProvider.psm1") -Function 'Get-AADTenantDetail' -Force
|
||||
|
||||
InModuleScope ExportAADProvider {
|
||||
BeforeAll {
|
||||
function Get-MgOrganization {}
|
||||
Mock -ModuleName ExportAADProvider Get-MgOrganization -MockWith {
|
||||
return [pscustomobject]@{
|
||||
DisplayName = "DisplayName";
|
||||
Name = "DomainName";
|
||||
Id = "TenantId";
|
||||
}
|
||||
}
|
||||
function Test-SCuBAValidJson {
|
||||
param (
|
||||
[string]
|
||||
$Json
|
||||
)
|
||||
$ValidJson = $true
|
||||
try {
|
||||
ConvertFrom-Json $Json -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
$ValidJson = $false;
|
||||
}
|
||||
$ValidJson
|
||||
}
|
||||
}
|
||||
Describe -Tag 'AADProvider' -Name "Get-AADTenantDetail" {
|
||||
It "Returns valid JSON" {
|
||||
$Json = Get-AADTenantDetail
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module ExportAADProvider -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
$ProviderPath = '../../../../../PowerShell/ScubaGear/Modules/Providers'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportAADProvider.psm1") -Function 'Get-PrivilegedRole' -Force
|
||||
|
||||
InModuleScope ExportAADProvider {
|
||||
BeforeAll {
|
||||
function Get-MgDirectoryRoleTemplate {}
|
||||
Mock -ModuleName ExportAADProvider Get-MgDirectoryRoleTemplate -MockWith {}
|
||||
function Get-MgPolicyRoleManagementPolicyAssignment {}
|
||||
Mock -ModuleName ExportAADProvider Get-MgPolicyRoleManagementPolicyAssignment -MockWith {}
|
||||
function Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance {}
|
||||
Mock -ModuleName ExportAADProvider Get-MgRoleManagementDirectoryRoleAssignmentScheduleInstance -MockWith {}
|
||||
function Get-MgPolicyRoleManagementPolicyRule {}
|
||||
Mock -ModuleName ExportAADProvider Get-MgPolicyRoleManagementPolicyRule -MockWith {}
|
||||
}
|
||||
Describe -Tag 'AADProvider' -Name "Get-PrivilegedRole" {
|
||||
It "With no premimum license, returns a not null PowerShell object" {
|
||||
{Get-PrivilegedRole} | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It "With premimum license, returns a not null PowerShell object" {
|
||||
{Get-PrivilegedRole -TenantHasPremiumLicense} | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module ExportAADProvider -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
$ProviderPath = '../../../../../PowerShell/ScubaGear/Modules/Providers'
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportAADProvider.psm1") -Function 'Get-PrivilegedUser' -Force
|
||||
|
||||
InModuleScope ExportAADProvider {
|
||||
BeforeAll {
|
||||
function Get-PrivilegedUser {}
|
||||
Mock -ModuleName ExportAADProvider Get-PrivilegedUser -MockWith {}
|
||||
function Get-MgDirectoryRoleMember {}
|
||||
Mock -ModuleName ExportAADProvider Get-MgDirectoryRoleMember -MockWith {}
|
||||
function Get-MgUser {}
|
||||
Mock -ModuleName ExportAADProvider Get-MgUser -MockWith {}
|
||||
function Get-MgGroupMember {}
|
||||
Mock -ModuleName ExportAADProvider Get-MgGroupMember -MockWith {}
|
||||
}
|
||||
Describe -Tag 'AADProvider' -Name "Get-PrivilegedUser" {
|
||||
It "With no premimum license, returns a not null PowerShell object" {
|
||||
{Get-PrivilegedUser} | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
It "With premimum license, returns a not null PowerShell object" {
|
||||
{Get-PrivilegedUser -TenantHasPremiumLicense} | Should -Not -BeNullOrEmpty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module ExportAADProvider -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<#
|
||||
# Due to how the Error handling was implemented, mocked API calls have to be mocked inside a
|
||||
# mocked CommandTracker class
|
||||
#>
|
||||
|
||||
$ProviderPath = "../../../../../PowerShell/ScubaGear/Modules/Providers"
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportDefenderProvider.psm1") -Function Export-DefenderProvider -Force
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ProviderHelpers/CommandTracker.psm1") -Force
|
||||
|
||||
InModuleScope -ModuleName ExportDefenderProvider {
|
||||
Describe -Tag 'ExportDefenderProvider' -Name "Export-DefenderProvider" {
|
||||
BeforeAll {
|
||||
class MockCommandTracker {
|
||||
[string[]]$SuccessfulCommands = @()
|
||||
[string[]]$UnSuccessfulCommands = @()
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command, [hashtable]$CommandArgs) {
|
||||
# This is where you decide where you mock functions called by CommandTracker :)
|
||||
try {
|
||||
switch ($Command) {
|
||||
"Get-AdminAuditLogConfig" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-EOPProtectionPolicyRule" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-MalwareFilterPolicy" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-AntiPhishPolicy" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-HostedContentFilterPolicy" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-AcceptedDomain" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-SafeAttachmentPolicy" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-SafeAttachmentRule" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-SafeLinksPolicy" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-SafeLinksRule" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-AtpPolicyForO365" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-DlpCompliancePolicy" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-DlpComplianceRule" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-ProtectionAlert" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
default {
|
||||
throw "ERROR you forgot to create a mock method for this cmdlet: $($Command)"
|
||||
}
|
||||
}
|
||||
$Result = @()
|
||||
$this.SuccessfulCommands += $Command
|
||||
return $Result
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error running $($Command). $($_)"
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
$Result = @()
|
||||
return $Result
|
||||
}
|
||||
}
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command) {
|
||||
return $this.TryCommand($Command, @{})
|
||||
}
|
||||
|
||||
[void] AddSuccessfulCommand([string]$Command) {
|
||||
$this.SuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[void] AddUnSuccessfulCommand([string]$Command) {
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[string[]] GetUnSuccessfulCommands() {
|
||||
return $this.UnSuccessfulCommands
|
||||
}
|
||||
|
||||
[string[]] GetSuccessfulCommands() {
|
||||
return $this.SuccessfulCommands
|
||||
}
|
||||
}
|
||||
function Get-CommandTracker {}
|
||||
Mock -ModuleName ExportDefenderProvider Get-CommandTracker {
|
||||
return [MockCommandTracker]::New()
|
||||
}
|
||||
function Connect-EXOHelper {}
|
||||
Mock -ModuleName ExportDefenderProvider Connect-EXOHelper -MockWith {}
|
||||
function Connect-DefenderHelper {}
|
||||
Mock -ModuleName ExportDefenderProvider Connect-DefenderHelper -MockWith {}
|
||||
function Get-OrganizationConfig {}
|
||||
Mock -ModuleName ExportDefenderProvider Get-OrganizationConfig -MockWith { [pscustomobject]@{
|
||||
"mockkey" = "mockvalue";
|
||||
} }
|
||||
function Get-SafeAttachmentPolicy {}
|
||||
Mock -ModuleName ExportDefenderProvider Get-SafeAttachmentPolicy -MockWith {}
|
||||
function Test-SCuBAValidProviderJson {
|
||||
param (
|
||||
[string]
|
||||
$Json
|
||||
)
|
||||
$Json = $Json.TrimEnd(",")
|
||||
$Json = "{$($Json)}"
|
||||
$ValidJson = $true
|
||||
try {
|
||||
ConvertFrom-Json $Json -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
$ValidJson = $false;
|
||||
}
|
||||
$ValidJson
|
||||
}
|
||||
}
|
||||
It "When called with -M365Environment 'commercial', returns valid JSON" {
|
||||
$Json = Export-DefenderProvider -M365Environment 'commercial'
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It "When called with -M365Environment 'gcc', returns valid JSON" {
|
||||
$Json = Export-DefenderProvider -M365Environment 'gcc'
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It "When called with -M365Environment 'gcchigh', returns valid JSON" {
|
||||
$Json = Export-DefenderProvider -M365Environment 'gcchigh'
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It "When called with -M365Environment 'dod', returns valid JSON" {
|
||||
$Json = Export-DefenderProvider -M365Environment 'dod'
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
AfterAll {
|
||||
Remove-Module ExportDefenderProvider -Force -ErrorAction SilentlyContinue
|
||||
Remove-Module CommandTracker -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<#
|
||||
# Due to how the Error handling was implemented, mocked API calls have to be mocked inside a
|
||||
# mocked CommandTracker class
|
||||
#>
|
||||
|
||||
$ProviderPath = "../../../../../PowerShell/ScubaGear/Modules/Providers"
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportEXOProvider.psm1") -Function Export-EXOProvider -Force
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ProviderHelpers/CommandTracker.psm1") -Force
|
||||
|
||||
InModuleScope -ModuleName ExportEXOProvider {
|
||||
Describe -Tag 'ExportEXOProvider' -Name "Export-EXOProvider" {
|
||||
BeforeAll {
|
||||
class MockCommandTracker {
|
||||
[string[]]$SuccessfulCommands = @()
|
||||
[string[]]$UnSuccessfulCommands = @()
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command, [hashtable]$CommandArgs) {
|
||||
# This is where you decide where you mock functions called by CommandTracker :)
|
||||
try {
|
||||
switch ($Command) {
|
||||
"Get-RemoteDomain" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-AcceptedDomain" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-ScubaSpfRecords" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-DkimSigningConfig" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-ScubaDkimRecords" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-ScubaDmarcRecords" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-TransportConfig" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-SharingPolicy" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-TransportRule" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-HostedConnectionFilterPolicy" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-OrganizationConfig" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
default {
|
||||
throw "ERROR you forgot to create a mock method for this cmdlet: $($Command)"
|
||||
}
|
||||
}
|
||||
$Result = @()
|
||||
$this.SuccessfulCommands += $Command
|
||||
return $Result
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error running $($Command). $($_)"
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
$Result = @()
|
||||
return $Result
|
||||
}
|
||||
}
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command) {
|
||||
return $this.TryCommand($Command, @{})
|
||||
}
|
||||
|
||||
[void] AddSuccessfulCommand([string]$Command) {
|
||||
$this.SuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[void] AddUnSuccessfulCommand([string]$Command) {
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[string[]] GetUnSuccessfulCommands() {
|
||||
return $this.UnSuccessfulCommands
|
||||
}
|
||||
|
||||
[string[]] GetSuccessfulCommands() {
|
||||
return $this.SuccessfulCommands
|
||||
}
|
||||
}
|
||||
function Get-CommandTracker {}
|
||||
Mock -ModuleName ExportEXOProvider Get-CommandTracker {
|
||||
return [MockCommandTracker]::New()
|
||||
}
|
||||
|
||||
function Test-SCuBAValidProviderJson {
|
||||
param (
|
||||
[string]
|
||||
$Json
|
||||
)
|
||||
$Json = $Json.TrimEnd(",")
|
||||
$Json = "{$($Json)}"
|
||||
$ValidJson = $true
|
||||
try {
|
||||
ConvertFrom-Json $Json -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
$ValidJson = $false;
|
||||
}
|
||||
$ValidJson
|
||||
}
|
||||
}
|
||||
It "When called, returns valid JSON" {
|
||||
$Json = Export-EXOProvider
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
}
|
||||
}
|
||||
AfterAll {
|
||||
Remove-Module ExportEXOProvider -Force -ErrorAction SilentlyContinue
|
||||
Remove-Module CommandTracker -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
$ProviderPath = "../../../../../PowerShell/ScubaGear/Modules/Providers"
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportEXOProvider.psm1") -Function Get-EXOTenantDetail -Force
|
||||
|
||||
InModuleScope ExportEXOProvider {
|
||||
Describe "Get-EXOTenantDetail" {
|
||||
BeforeAll {
|
||||
# empty stub required for mocked cmdlets called directly in the provider
|
||||
function Get-OrganizationConfig {}
|
||||
|
||||
Mock -ModuleName ExportEXOProvider Get-OrganizationConfig -MockWith {
|
||||
return [pscustomobject]@{
|
||||
Name = "name";
|
||||
DisplayName = "DisplayName";
|
||||
}
|
||||
}
|
||||
Mock -CommandName Invoke-WebRequest {
|
||||
return [pscustomobject]@{
|
||||
Content = '{token_endpoint: "this/is/the/token/url"}'
|
||||
}
|
||||
}
|
||||
function Test-SCuBAValidJson {
|
||||
param (
|
||||
[string]
|
||||
$Json
|
||||
)
|
||||
$ValidJson = $true
|
||||
try {
|
||||
ConvertFrom-Json $Json -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
$ValidJson = $false;
|
||||
}
|
||||
$ValidJson
|
||||
}
|
||||
}
|
||||
It "When called with -M365Environment 'commercial', returns valid JSON" {
|
||||
$Json = Get-EXOTenantDetail -M365Environment "commercial"
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It "When called with -M365Environment 'gcc', returns valid JSON" {
|
||||
$Json = Get-EXOTenantDetail -M365Environment "gcc"
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It "When called with -M365Environment 'gcchigh', returns valid JSON" {
|
||||
$Json = Get-EXOTenantDetail -M365Environment "gcchigh"
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It "When called with -M365Environment 'dod', returns valid JSON" {
|
||||
$Json = Get-EXOTenantDetail -M365Environment "dod"
|
||||
$ValidJson = Test-SCuBAValidJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module ExportEXOProvider -Force -ErrorAction SilentlyContinue
|
||||
Remove-Module ExchangeOnlineManagement -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
$ProviderPath = "../../../../../PowerShell/ScubaGear/Modules/Providers"
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportEXOProvider.psm1") -Function Get-ScubaDkimRecords -Force
|
||||
|
||||
InModuleScope 'ExportEXOProvider' {
|
||||
Describe -Tag 'ExportEXOProvider' -Name "Get-ScubaDkimRecords" {
|
||||
It "TODO handles a domain with DKIM" {
|
||||
# Get-ScubaDkimRecords
|
||||
$true | Should -Be $true
|
||||
}
|
||||
|
||||
It "TODO handles a domain without DKIM" {
|
||||
# Get-ScubaDkimRecords
|
||||
$true | Should -Be $true
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
AfterAll {
|
||||
Remove-Module ExportEXOProvider -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
$ProviderPath = "../../../../../PowerShell/ScubaGear/Modules/Providers"
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportEXOProvider.psm1") -Function Get-ScubaDmarcRecords -Force
|
||||
|
||||
InModuleScope 'ExportEXOProvider' {
|
||||
Describe -Tag 'ExportEXOProvider' -Name "Get-ScubaDmarcRecords" {
|
||||
It "TODO return DMARC records" {
|
||||
# Get-ScubaDmarcRecords
|
||||
$true | Should -Be $true
|
||||
}
|
||||
}
|
||||
}
|
||||
AfterAll {
|
||||
Remove-Module ExportEXOProvider -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
$ProviderPath = "../../../../../PowerShell/ScubaGear/Modules/Providers"
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportEXOProvider.psm1") -Function 'Get-ScubaSpfRecords' -Force
|
||||
|
||||
InModuleScope 'ExportEXOProvider' {
|
||||
Describe -Tag 'ExportEXOProvider' -Name "Get-ScubaSpfRecords" {
|
||||
It "TODO return SPF records" {
|
||||
# Get-ScubaSpfRecords
|
||||
$true | Should -Be $true
|
||||
}
|
||||
}
|
||||
}
|
||||
AfterAll {
|
||||
Remove-Module ExportEXOProvider -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<#
|
||||
# Due to how the Error handling was implemented, mocked API calls have to be mocked inside a
|
||||
# mocked CommandTracker class
|
||||
#>
|
||||
|
||||
$ProviderPath = "../../../../../PowerShell/ScubaGear/Modules/Providers"
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportOneDriveProvider.psm1") -Force
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ProviderHelpers/CommandTracker.psm1") -Force
|
||||
|
||||
InModuleScope -ModuleName ExportOneDriveProvider {
|
||||
Describe -Tag 'ExportOneDriveProvider' -Name "Export-OneDriveProvider" {
|
||||
BeforeAll {
|
||||
class MockCommandTracker {
|
||||
[string[]]$SuccessfulCommands = @()
|
||||
[string[]]$UnSuccessfulCommands = @()
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command, [hashtable]$CommandArgs) {
|
||||
# This is where you decide where you mock functions called by CommandTracker :)
|
||||
try {
|
||||
switch ($Command) {
|
||||
{ ($_ -eq "Get-SPOTenantSyncClientRestriction") -or ($_ -eq "Get-PnPTenantSyncClientRestriction") } {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
{ ($_ -eq "Get-SPOTenant") -or ($_ -eq "Get-PnPTenant") } {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
default {
|
||||
throw "ERROR you forgot to create a mock method for this cmdlet: $($Command)"
|
||||
}
|
||||
}
|
||||
$Result = @()
|
||||
$this.SuccessfulCommands += $Command
|
||||
return $Result
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error running $($Command). $($_)"
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
$Result = @()
|
||||
return $Result
|
||||
}
|
||||
}
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command) {
|
||||
return $this.TryCommand($Command, @{})
|
||||
}
|
||||
|
||||
[void] AddSuccessfulCommand([string]$Command) {
|
||||
$this.SuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[void] AddUnSuccessfulCommand([string]$Command) {
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[string[]] GetUnSuccessfulCommands() {
|
||||
return $this.UnSuccessfulCommands
|
||||
}
|
||||
|
||||
[string[]] GetSuccessfulCommands() {
|
||||
return $this.SuccessfulCommands
|
||||
}
|
||||
}
|
||||
function Get-CommandTracker {}
|
||||
Mock -ModuleName ExportOneDriveProvider Get-CommandTracker {
|
||||
return [MockCommandTracker]::New()
|
||||
}
|
||||
function Test-SCuBAValidProviderJson {
|
||||
param (
|
||||
[string]
|
||||
$Json
|
||||
)
|
||||
$Json = $Json.TrimEnd(",")
|
||||
$Json = "{$($Json)}"
|
||||
$ValidJson = $true
|
||||
try {
|
||||
ConvertFrom-Json $Json -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
$ValidJson = $false;
|
||||
}
|
||||
$ValidJson
|
||||
}
|
||||
}
|
||||
It "When running interactively, returns valid JSON" {
|
||||
$Json = Export-OneDriveProvider
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It "When running with Service Principals, returns valid JSON" {
|
||||
$Json = Export-OneDriveProvider -PnPFlag
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll {
|
||||
Remove-Module ExportOneDriveProvider -Force -ErrorAction SilentlyContinue
|
||||
Remove-Module CommandTracker -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<#
|
||||
# Due to how the Error handling was implemented, mocked API calls have to be mocked inside a
|
||||
# mocked CommandTracker class
|
||||
#>
|
||||
|
||||
$ProviderPath = "../../../../../PowerShell/ScubaGear/Modules/Providers"
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ExportPowerPlatformProvider.psm1") -Function Export-PowerPlatformProvider -Force
|
||||
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath "$($ProviderPath)/ProviderHelpers/CommandTracker.psm1") -Force
|
||||
|
||||
InModuleScope -ModuleName ExportPowerPlatformProvider {
|
||||
Describe -Tag 'ExportPowerPlatformProvider' -Name "Export-PowerPlatformProvider" {
|
||||
BeforeAll {
|
||||
class MockCommandTracker {
|
||||
[string[]]$SuccessfulCommands = @()
|
||||
[string[]]$UnSuccessfulCommands = @()
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command, [hashtable]$CommandArgs) {
|
||||
# This is where you decide where you mock functions called by CommandTracker :)
|
||||
try {
|
||||
switch ($Command) {
|
||||
"Get-TenantDetailsFromGraph" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{
|
||||
Domains = @(
|
||||
@{
|
||||
Name = "example.onmicrosoft.com";
|
||||
initial = $true;
|
||||
},
|
||||
@{
|
||||
Name = "contoso.onmicrosoft.com";
|
||||
initial = $false;
|
||||
}
|
||||
);
|
||||
DisplayName = "DisplayName";
|
||||
TenantId = "TenantId";
|
||||
}
|
||||
}
|
||||
"Get-TenantSettings" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-AdminPowerAppEnvironment" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
"Get-AdminPowerAppEnvironment" {
|
||||
$this.SuccessfulCommands += $Command
|
||||
return [pscustomobject]@{}
|
||||
}
|
||||
default {
|
||||
throw "ERROR you forgot to create a mock method for this cmdlet: $($Command)"
|
||||
}
|
||||
}
|
||||
$Result = @()
|
||||
$this.SuccessfulCommands += $Command
|
||||
return $Result
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Error running $($Command). $($_)"
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
$Result = @()
|
||||
return $Result
|
||||
}
|
||||
}
|
||||
|
||||
[System.Object[]] TryCommand([string]$Command) {
|
||||
return $this.TryCommand($Command, @{})
|
||||
}
|
||||
|
||||
[void] AddSuccessfulCommand([string]$Command) {
|
||||
$this.SuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[void] AddUnSuccessfulCommand([string]$Command) {
|
||||
$this.UnSuccessfulCommands += $Command
|
||||
}
|
||||
|
||||
[string[]] GetUnSuccessfulCommands() {
|
||||
return $this.UnSuccessfulCommands
|
||||
}
|
||||
|
||||
[string[]] GetSuccessfulCommands() {
|
||||
return $this.SuccessfulCommands
|
||||
}
|
||||
}
|
||||
function Get-CommandTracker {}
|
||||
Mock -ModuleName ExportPowerPlatformProvider Get-CommandTracker {
|
||||
return [MockCommandTracker]::New()
|
||||
}
|
||||
function Get-DlpPolicy {}
|
||||
Mock -ModuleName ExportPowerPlatformProvider Get-DlpPolicy -MockWith {}
|
||||
function Get-PowerAppTenantIsolationPolicy {}
|
||||
Mock -ModuleName ExportPowerPlatformProvider Get-PowerAppTenantIsolationPolicy -MockWith {}
|
||||
function Test-SCuBAValidProviderJson {
|
||||
param (
|
||||
[string]
|
||||
$Json
|
||||
)
|
||||
$Json = $Json.TrimEnd(",")
|
||||
$Json = "{$($Json)}"
|
||||
$ValidJson = $true
|
||||
try {
|
||||
ConvertFrom-Json $Json -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
$ValidJson = $false;
|
||||
}
|
||||
$ValidJson
|
||||
}
|
||||
}
|
||||
It "When called with -M365Environment 'commercial', returns valid JSON" {
|
||||
Mock -CommandName Invoke-WebRequest {
|
||||
return [pscustomobject]@{
|
||||
Content = '{"tenant_region_scope": "NA","tenant_region_sub_scope": ""}'
|
||||
}
|
||||
}
|
||||
$Json = Export-PowerPlatformProvider -M365Environment 'commercial'
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It "When called with -M365Environment 'gcc', returns valid JSON" {
|
||||
Mock -CommandName Invoke-WebRequest {
|
||||
return [pscustomobject]@{
|
||||
Content = '{"tenant_region_scope": "NA","tenant_region_sub_scope": "GCC"}'
|
||||
}
|
||||
}
|
||||
$Json = Export-PowerPlatformProvider -M365Environment 'gcc'
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It "When called with -M365Environment 'gcchigh', returns valid JSON" {
|
||||
Mock -CommandName Invoke-WebRequest {
|
||||
return [pscustomobject]@{
|
||||
Content = '{"tenant_region_scope": "USGov","tenant_region_sub_scope": "DODCON"}'
|
||||
}
|
||||
}
|
||||
$Json = Export-PowerPlatformProvider -M365Environment 'gcchigh'
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It "When called with -M365Environment 'dod', returns valid JSON" {
|
||||
Mock -CommandName Invoke-WebRequest {
|
||||
return [pscustomobject]@{
|
||||
Content = '{"tenant_region_scope": "USGov","tenant_region_sub_scope": "DOD"}'
|
||||
}
|
||||
}
|
||||
$Json = Export-PowerPlatformProvider -M365Environment 'dod'
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
It "When called with -M365Environment 'commercial', from a non-NA tenant returns valid json" {
|
||||
Mock -CommandName Invoke-WebRequest {
|
||||
return [pscustomobject]@{
|
||||
Content = '{"tenant_region_scope": "EU","tenant_region_sub_scope": ""}'
|
||||
}
|
||||
}
|
||||
$Json = Export-PowerPlatformProvider -M365Environment 'commercial'
|
||||
$ValidJson = Test-SCuBAValidProviderJson -Json $Json | Select-Object -Last 1
|
||||
$ValidJson | Should -Be $true
|
||||
}
|
||||
}
|
||||
}
|
||||
AfterAll {
|
||||
Remove-Module ExportPowerPlatformProvider -Force -ErrorAction SilentlyContinue
|
||||
Remove-Module CommandTracker -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user