Initial commit

This commit is contained in:
Chad-CISA
2023-07-24 10:07:23 -04:00
committed by GitHub
commit 0629173603
230 changed files with 91652 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
**/*.psd1 diff

34
.github/workflows/run_opa_tests.yaml vendored Normal file
View 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

View 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'

View 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
View 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
View 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
View 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*

View 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
View 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 arent about whats right or wrong according to the rules, but about whats 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 isnt 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
View 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
View 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"

View File

@@ -0,0 +1,8 @@
# PSScriptAnalyzerSettings.psd1
@{
Severity=@('Error','Warning','Information')
ExcludeRules=@(
'PSUseSingularNouns',
'PSUseShouldProcessForStateChangingFunctions',
'PSUseOutputTypeCorrectly')
}

View 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)"
}
}

View 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'
)

View 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'
)

View File

@@ -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"}
]
}

View 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'
)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

View File

@@ -0,0 +1,3 @@
window.addEventListener('DOMContentLoaded', (event) => {
mountDarkMode("Parent Report");
});

View 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("&#x2b; Expand all"));
expandAll.title = "Expand all";
expandAll.addEventListener("click", expandAllCAPs);
buttons.appendChild(expandAll);
let collapseAll = document.createElement("button");
collapseAll.appendChild(document.createTextNode("&minus; 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");
});

View 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');
}
}

View File

@@ -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;
}

View 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%;
}

File diff suppressed because it is too large Load Diff

View 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
}

View File

@@ -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
}

View 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
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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()
}

View File

@@ -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()
}

View File

@@ -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'
)

View 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'
)

View 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
}
}

View 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'
}
)

View 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 = ''
}

View 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
View File

@@ -0,0 +1,337 @@
# ScubaGear M365 Secure Configuration Baseline Assessment Tool
Developed by CISA, this assessment tool verifies that an M365 tenants 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**
![ScubaGear App Service Principal API Permissions](/images/appserviceprincipal-api-permssions.png)
**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
![SCuBA Architecture diagram](/images/scuba-architecture.png)
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.
![Power Platform missing license](images/pplatformmissinglicense.PNG)
### 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

File diff suppressed because it is too large Load Diff

1993
Rego/DefenderConfig.rego Normal file

File diff suppressed because it is too large Load Diff

832
Rego/EXOConfig.rego Normal file
View 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
View 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
}
#--

View 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
View 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
View 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
View 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"

File diff suppressed because it is too large Load Diff

View 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

View File

@@ -0,0 +1,6 @@
aad
defender
exo
onedrive
sharepoint
teams

View 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

View 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"
}
}
]
}

View 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
} }
}

View 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
}
}

View 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
}

View 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

View 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
View 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
}

View File

@@ -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
}

View File

@@ -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
}

View 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
}

View File

@@ -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

View 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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View 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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View 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
}

View File

@@ -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
}

View 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
}

View 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
}

View File

@@ -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
}

View File

@@ -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":[
]
}
}

View File

@@ -0,0 +1,22 @@
{
"Conditions": {
"Applications":{
"ApplicationFilter":{
"Mode":null,
"Rule":null
},
"ExcludeApplications":[
],
"IncludeApplications":[
],
"IncludeAuthenticationContextClassReferences":[
],
"IncludeUserActions":[
]
}
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}

View File

@@ -0,0 +1,36 @@
{
"Conditions": {
"Users":{
"ExcludeGroups":[
],
"ExcludeGuestsOrExternalUsers":{
"ExternalTenants":{
"MembershipKind":null
},
"GuestOrExternalUserTypes":null
},
"ExcludeRoles":[
],
"ExcludeUsers":[
],
"IncludeGroups":[
],
"IncludeGuestsOrExternalUsers":{
"ExternalTenants":{
"MembershipKind":null
},
"GuestOrExternalUserTypes":null
},
"IncludeRoles":[
],
"IncludeUsers":[
]
}
}
}

View File

@@ -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'
}

View File

@@ -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 ""
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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